1、业务场景说明
本智慧社区后端接口文档登录模块接口:
GET /captcha:获取 uuid+base64 验证码图片
POST /login:传入 username、password、captcha、uuid,返回 token 与 expire 过期秒数
GET /checkToken:校验 token 有效性返回 ok/error
PUT /sys/user/updatePassword 修改密码
POST /logout 退出登录
传统 session 不适合前后端分离,本项目选择 JWT 做无状态认证,token 放在请求头每次请求携带。
2、JWT 原理
JWT 分为三部分:header、payload、signature。
header:加密算法;
payload:存放用户 id,不要存敏感密码;
signature:秘钥签名防止篡改。
本项目登录返回:token 字符串 + expire 过期时间 1296000 秒。
json
//登录成功返回示例
{
"msg": "登录成功",
"code": 200,
"data": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.xxx",
"expire": 1296000
}
}
3、pom 依赖
<!-- jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
4、Jwt 工具类
@Component
public class JwtUtil {
//密钥,项目放到yml配置
private static final String SECRET = "zhihuishequ2026secretkey123456";
//生成token,传入userId,过期时间秒
public String generateToken(Long userId, long expireSecond){
Date now = new Date();
Date expireDate = new Date(System.currentTimeMillis()+expireSecond*1000);
return Jwts.builder()
.setSubject(userId.toString())
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512,SECRET)
.compact();
}
//解析token获取用户id
public Claims getClaimsByToken(String token){
try {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
}catch (Exception e){
return null;
}
}
//校验token是否合法、过期
public boolean validateToken(String token){
Claims claims = getClaimsByToken(token);
if(claims == null) return false;
return !claims.getExpiration().before(new Date());
}
}
5、验证码接口 /captcha
返回 uuid、base64 图片、验证码 code;redis 存储 uuid 作为 key,value 是验证码,设置过期时间。
@GetMapping("/captcha")
public Result captcha(){
//生成uuid
String uuid = UUID.randomUUID().toString().replace("-","");
//生成图片验证码字符串
String code = kaptchaProducer.createText();
//生成图片,转base64
BufferedImage image = kaptchaProducer.createImage(code);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ImageIO.write(image,"png",stream);
String base64 = "data:image/png;base64,"+ Base64.encodeBase64String(stream.toByteArray());
//存入redis,2分钟过期
redisTemplate.opsForValue().set("captcha:"+uuid,code,2, TimeUnit.MINUTES);
Map<String,Object> map = new HashMap<>();
map.put("uuid",uuid);
map.put("captcha",base64);
map.put("code",code);
return Result.success(map);
}
6、登录接口 /login
接收参数:username、password、captcha、uuid。逻辑:
根据 uuid 从 redis 取出验证码,比对;
查询数据库用户,密码比对(MD5 加密存储,文档示例密码密文);
校验成功调用 jwt 工具生成 token,expire=1296000;
返回 token;删除 redis 验证码。
7、Token 拦截器实现
所有接口除登录、验证码之外,都要校验请求头 token。
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if(token == null || !jwtUtil.validateToken(token)){
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(Result.error(401,"token失效请重新登录")));
return false;
}
//把userId放入request,controller直接获取
Claims claims = jwtUtil.getClaimsByToken(token);
request.setAttribute("userId", Long.parseLong(claims.getSubject()));
return true;
}
}
8、踩坑 & 安全优化总结
JWT 无状态,本身无法主动失效:logout 退出登录方案:redis 维护黑名单 token,有效期等于 jwt 过期时间;
payload 不要存放密码、手机号等敏感信息,只是 base64 可解码;
验证码一定要绑定 uuid,防止暴力破解;
秘钥不要硬编码代码,放到 application.yml;
前端 token 存储建议 localStorage,请求头携带 token;
修改密码成功之后,建议清空旧 token,强制重新登录。
9、拓展思考
JWT vs Session 各自适用场景;token 黑名单实现主动失效方案;refreshToken 刷新 token 优化用户体验。