1. 拦截器(AuthInterceptor)详细修改总结
拦截器工作流程总结
-
路径筛选
→ 放行白名单路径(如登录接口)
→ 拦截其他所有请求 -
认证头解析
→ 验证Authorization头格式
→ 提取纯净Token -
JWT认证优先
→ 解析Token获取用户ID
→ 验证Token签名和有效期 -
传统认证回退
→ 当JWT失败时启用
→ 验证userId头+Token组合 -
业务状态验证
→ 检查用户是否被禁用
→ 返回403状态码拦截请求 -
上下文传递
→ 存储用户ID到请求属性
→ 供后续Controller使用
一、操作步骤与主要变更
1.1 只认标准 Authorization 头
- 原来:有的接口用 token,有的用 Authorization,格式不统一。
- 现在:统一只从 Authorization 请求头获取,格式为 Bearer xxx,更安全、规范。
1.2 优先支持 JWT 校验,兼容老 token
- 原来:有的接口用自定义的 SHA-256 token(SecurityUtil),有的用 JWT,混乱且不安全。
- 现在:
- 先用 JWT 方式校验(JwtUtil),提取 userId 并校验有效性。
- 如果 JWT 校验失败,再用老的 token 校验(userService.verifyToken),兼容老用户。
- 两种方式都失败则返回 401。
1.3 校验用户是否被停用
- 原来:只校验 token,不校验用户状态,停用用户依然能访问。
- 现在:token 校验通过后,调用 userService.isUserEnabled(userId),如果被停用直接返回 403。
1.4 认证通过后写入 userId
- 认证通过后,将 userId 写入 request 属性,方便后续业务代码获取。
1.5 日志增强
- 增加了详细的日志输出,方便排查 token 校验、用户状态等问题。
1.6 配置拦截器放行路径
- 在 WebConfig 里,明确哪些接口不需要认证,其余接口都要经过拦截器校验。
二、JWT 生成与校验逻辑
2.1 JWT 生成逻辑(登录/注册/第三方登录成功后)
- 使用 JwtUtil.generateToken(userId) 生成 JWT token。
- JWT token 结构:header.payload.signature
- header:算法、类型
- payload:userId、iat(签发时间)、exp(过期时间)等
- signature:用密钥(配置在 application-dev.yml)签名,防篡改
- 生成的 token 比如:eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTY...
2.2 JWT 校验逻辑(拦截器)
项目中的校验流程
AuthInterceptor 里,相关代码大致如下:
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String requestURI = request.getRequestURI(); log.info("拦截器处理请求: {}", requestURI); // 放行登录接口 if (requestURI.contains("/user/login")) { log.info("放行登录接口: {}", requestURI); return true; } // 从Authorization头中获取Bearer token String authorization = request.getHeader("Authorization"); log.info("Authorization头: {}", authorization); if (authorization == null || !authorization.startsWith("Bearer ")) { log.warn("Authorization头无效或缺失"); response.setStatus(401); return false; } // 提取token String token = authorization.substring(7); // 去掉"Bearer "前缀 log.info("提取的token: {}", token); // 尝试JWT验证 Long userId = jwtUtil.getUserIdFromToken(token); log.info("从JWT token中获取的用户ID: {}", userId); if (userId != null) { // JWT token有效,继续验证 boolean isValid = jwtUtil.validateToken(token, userId); log.info("JWT token验证结果: {}", isValid); if (isValid) { // 检查用户是否启用 boolean isEnabled = userService.isUserEnabled(userId); log.info("用户启用状态: {}", isEnabled); if (!isEnabled) { log.warn("用户已被停用, userId: {}", userId); response.setStatus(403); return false; } // 将用户ID存入request属性中 request.setAttribute("userId", userId); log.info("JWT认证成功,用户ID: {}", userId); return true; } } // JWT验证失败,尝试旧的token验证 log.info("JWT验证失败,尝试旧的token验证"); // 从请求头中获取用户ID(兼容旧的方式) String userIdHeader = request.getHeader("userId"); if (userIdHeader != null) { try { Long oldUserId = Long.valueOf(userIdHeader); log.info("从请求头获取的用户ID: {}", oldUserId); // 验证旧的token boolean isValidOldToken = userService.verifyToken(oldUserId, token); log.info("旧token验证结果: {}", isValidOldToken); if (isValidOldToken) { // 检查用户是否启用 boolean isEnabled = userService.isUserEnabled(oldUserId); log.info("用户启用状态: {}", isEnabled); if (!isEnabled) { log.warn("用户已被停用, userId: {}", oldUserId); response.setStatus(403); return false; } // 将用户ID存入request属性中 request.setAttribute("userId", oldUserId); log.info("旧token认证成功,用户ID: {}", oldUserId); return true; } } catch (NumberFormatException e) { log.warn("用户ID格式错误: {}", userIdHeader); } } log.warn("所有token验证方式都失败"); response.setStatus(401); return false; }
String token = authorization.substring(7); // 去掉"Bearer "
Long userId = jwtUtil.getUserIdFromToken(token);
if (userId != null) {
boolean isValid = jwtUtil.validateToken(token, userId);
if (isValid) {
// 认证通过
}
}
2. JwtUtil 里的校验方法
2.1 解析 userId
public Long getUserIdFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims != null ? claims.get("userId", Long.class) : null;
}
- 这一步只是解码 payload,不涉及安全,只是提取信息。
2.2 校验 token(签名+有效期)
public boolean validateToken(String token, Long userId) {
try {
Claims claims = getClaimsFromToken(token);
if (claims == null) return false;
// 校验userId
if (!userId.equals(claims.get("userId", Long.class))) return false;
// 校验过期时间
Date expiration = claims.getExpiration();
if (expiration == null || expiration.before(new Date())) return false;
return true;
} catch (Exception e) {
return false;
}
}
2.3 getClaimsFromToken 的实现
public Claims getClaimsFromToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(getSecretKey()) // 这里用你的密钥
.build()
.parseClaimsJws(token) // 这里会自动校验签名
.getBody();
} catch (Exception e) {
return null;
}
}
3. 关键点:签名校验
- parseClaimsJws(token) 这一步会自动做两件事:
- 用你配置的密钥(secret)对 header.payload 重新签名,和 token 里的 signature 比较。
- 如果 signature 不一致(被篡改/伪造),直接抛异常,token 无效。
- 只有签名校验通过,才会返回 payload(claims),否则直接返回 null。
4. 关键点:有效期校验
- 解析出 payload 后,取出 exp 字段(过期时间),和当前时间比较。
- 如果已过期,也会返回 false。
5. 总结
- 签名校验:parseClaimsJws(token) 自动完成,只有密钥正确、token未被篡改才通过。
- 有效期校验:手动判断 exp 字段是否过期。
- 只有两步都通过,才算认证成功。
6. 伪代码流程
try {
// 1. 签名校验(自动完成)
Claims claims = Jwts.parserBuilder().setSigningKey(secret).build().parseClaimsJws(token).getBody();
// 2. 有效期校验
if (claims.getExpiration().before(new Date())) return false;
// 3. 其他业务校验
return true;
} catch (Exception e) {
// 签名不对/格式不对/过期等都会抛异常
return false;
}
- 从 Authorization 头提取 Bearer token
- 用 JwtUtil.getUserIdFromToken(token) 解析 userId
- 用 JwtUtil.validateToken(token, userId) 校验 token 是否有效(签名、过期等)
- 校验通过则继续,否则尝试老 token 校验
三、与原有 token 机制的区别
机制 | 原有 SecurityUtil token | 现在 JWT token(推荐) |
---|---|---|
生成方式 | SHA-256(userId+时间戳+盐) | JWT标准,包含userId、过期等 |
存储 | 数据库存一份 | 一般不存数据库,仅前端持有 |
校验方式 | 数据库查token比对 | JWT工具类直接校验签名和过期 |
安全性 | 容易伪造、泄露 | 有签名、不可伪造、可设置过期 |
兼容性 | 仅老接口 | 新老接口都支持(拦截器兼容) |
推荐 | ❌ | ✅ |
核心对比:传统Token vs JWT Token
1. 常用性对比
特性 | 传统Token (SecurityUtil风格) | JWT Token | 现代常用度 |
---|---|---|---|
新项目 | ❌ 较少使用 | ✅ 主流选择 | JWT胜出 |
老系统 | ✅ 常见于遗留系统 | ⚠️ 逐步迁移中 | 传统存在 |
微服务 | ❌ 扩展性差 | ✅ 天然适合分布式 | JWT胜出 |
2. 关键差异详解
传统Token实现 (SecurityUtil风格)
// 传统Token生成(服务端) public String generateLegacyToken(Long userId) { String salt = "YourSecretSalt"; long timestamp = System.currentTimeMillis(); String raw = userId + "|" + timestamp + "|" + salt; return Hashing.sha256().hashString(raw, StandardCharsets.UTF_8).toString(); } // 数据库存储(必须) userRepository.saveToken(userId, token); // 拦截器验证代码片段 String storedToken = userService.getUserToken(userId); // 数据库查询 if (!token.equals(storedToken)) { response.setStatus(401); // 验证失败 }
核心特点:
-
📌 强制数据库存储:每个token需持久化
-
⏱ 时间戳防重放:但需额外处理过期逻辑
-
🔒 服务端强控制:可即时吊销token
JWT Token实现
// JWT生成(服务端) public String generateJwtToken(Long userId) { return Jwts.builder() .setSubject(userId.toString()) .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时过期 .signWith(SignatureAlgorithm.HS256, "YourJWTSecret") .compact(); } // 拦截器验证代码片段 Claims claims = Jwts.parser() .setSigningKey("YourJWTSecret") .parseClaimsJws(token) .getBody(); Long userId = Long.parseLong(claims.getSubject()); if (claims.getExpiration().before(new Date())) { response.setStatus(401); // Token过期 }
核心特点:
-
🚫 无状态:服务端不存储token
-
📦 自包含信息:payload可携带用户ID/角色等
-
⏳ 内置过期:标准化过期时间处理
3. 全方位对比矩阵
维度 | 传统Token | JWT Token | 优势方 |
---|---|---|---|
存储位置 | 服务端数据库 | 仅客户端持有 | ✅ JWT |
验证性能 | 每次请求需数据库查询 | 仅需CPU签名验证 | ✅ JWT |
扩展性 | 集群需共享token存储 | 天然支持分布式 | ✅ JWT |
信息携带 | 仅标识作用 | 可携带用户信息/权限(claim) | ✅ JWT |
吊销能力 | 即时吊销(删数据库) | 需特殊实现(黑名单/短有效期) | ✅ 传统 |
标准化程度 | 私有实现 | RFC7519工业标准 | ✅ JWT |
移动端支持 | 需自定义实现 | 原生支持完善 | ✅ JWT |
5. 混合架构实践(最佳方案)
// 结合双方优势的实现 public AuthResponse login(User user) { // 生成JWT作为访问令牌(access_token) String accessToken = jwtUtil.generateAccessToken(user.getId()); // 生成传统token作为刷新令牌(refresh_token) String refreshToken = tokenService.generateRefreshToken(user.getId()); return new AuthResponse(accessToken, refreshToken, 3600); } // 令牌刷新端点 public AuthResponse refreshToken(String refreshToken) { // 数据库验证刷新令牌 Long userId = tokenService.validateRefreshToken(refreshToken); // 签发新访问令牌 String newAccessToken = jwtUtil.generateAccessToken(userId); return new AuthResponse(newAccessToken, null, 3600); }
优势组合:
-
⚡ 高频访问:JWT快速验证
-
🔒 安全刷新:传统token存数据库可吊销
-
⏳ 短时JWT:降低安全风险(建议30分钟过期)
-
🛡 长时刷新:传统token用于更新会话(7天有效期)
总结:技术选型指南
场景 | 推荐方案 | 原因 |
---|---|---|
新项目/微服务 | ✅ 纯JWT | 简化架构,提升性能,天然支持分布式 |
金融/高安全系统 | ✅ 传统Token | 需要即时吊销能力,严格控制会话 |
大型互联网应用 | ✅ 混合方案 | 平衡性能与安全性,最佳用户体验 |
移动端主导应用 | ✅ JWT+刷新令牌 | 减少网络请求,优化移动端体验 |
内部管理系统 | ⚠️ 传统Token | 通常会话量少,吊销需求高于性能需求 |
📊 行业数据:2023年OAuth2.0实施调研显示,78%的新系统采用JWT作为主要令牌格式,其中62%配合刷新令牌机制实现安全控制。
四、具体操作步骤
- 修改拦截器代码
- 只认 Authorization 头,格式为 Bearer xxx
- 优先用 JWT 校验,失败再用老 token 校验
- 校验用户是否被停用
- 认证通过写入 userId 到 request
- 增加详细日志
- 修改 WebConfig 配置
- 明确哪些接口不需要认证,其余都要拦截
- 修改登录/注册/第三方登录接口
- 登录成功后统一用 JwtUtil.generateToken(userId) 生成 token 返回给前端
- 配置 JWT 密钥
- 在 application-dev.yml 配置足够长的密钥,保证安全
- 数据库 token 字段扩容
- 如果需要存储 JWT,token 字段长度要设置为 VARCHAR(500) 以上
五、效果
- 安全性提升:只认标准 Authorization,token 不易伪造
- 兼容性好:新老用户都能正常访问
- 维护方便:日志详细,问题易定位
- 业务安全:用户被禁用后无法访问受保护接口