拦截器配置和传统Token vs JWT Token对比

1. 拦截器(AuthInterceptor)详细修改总结

拦截器工作流程总结
  1. 路径筛选
    → 放行白名单路径(如登录接口)
    → 拦截其他所有请求

  2. 认证头解析
    → 验证Authorization头格式
    → 提取纯净Token

  3. JWT认证优先
    → 解析Token获取用户ID
    → 验证Token签名和有效期

  4. 传统认证回退
    → 当JWT失败时启用
    → 验证userId头+Token组合

  5. 业务状态验证
    → 检查用户是否被禁用
    → 返回403状态码拦截请求

  6. 上下文传递
    → 存储用户ID到请求属性
    → 供后续Controller使用

一、操作步骤与主要变更

1.1 只认标准 Authorization 头

  • 原来:有的接口用 token,有的用 Authorization,格式不统一。
  • 现在:统一只从 Authorization 请求头获取,格式为 Bearer xxx,更安全、规范。

1.2 优先支持 JWT 校验,兼容老 token

  • 原来:有的接口用自定义的 SHA-256 token(SecurityUtil),有的用 JWT,混乱且不安全。
  • 现在:
  1. 先用 JWT 方式校验(JwtUtil),提取 userId 并校验有效性。
  1. 如果 JWT 校验失败,再用老的 token 校验(userService.verifyToken),兼容老用户。
  1. 两种方式都失败则返回 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) 这一步会自动做两件事:
  1. 用你配置的密钥(secret)对 header.payload 重新签名,和 token 里的 signature 比较。
  1. 如果 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. 全方位对比矩阵

维度传统TokenJWT 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);
}

优势组合

  1. ⚡ 高频访问:JWT快速验证

  2. 🔒 安全刷新:传统token存数据库可吊销

  3. ⏳ 短时JWT:降低安全风险(建议30分钟过期)

  4. 🛡 长时刷新:传统token用于更新会话(7天有效期)


总结:技术选型指南

场景推荐方案原因
新项目/微服务✅ 纯JWT简化架构,提升性能,天然支持分布式
金融/高安全系统✅ 传统Token需要即时吊销能力,严格控制会话
大型互联网应用✅ 混合方案平衡性能与安全性,最佳用户体验
移动端主导应用✅ JWT+刷新令牌减少网络请求,优化移动端体验
内部管理系统⚠️ 传统Token通常会话量少,吊销需求高于性能需求

📊 行业数据:2023年OAuth2.0实施调研显示,78%的新系统采用JWT作为主要令牌格式,其中62%配合刷新令牌机制实现安全控制。


四、具体操作步骤

  1. 修改拦截器代码
  • 只认 Authorization 头,格式为 Bearer xxx
  • 优先用 JWT 校验,失败再用老 token 校验
  • 校验用户是否被停用
  • 认证通过写入 userId 到 request
  • 增加详细日志
  1. 修改 WebConfig 配置
  • 明确哪些接口不需要认证,其余都要拦截
  1. 修改登录/注册/第三方登录接口
  • 登录成功后统一用 JwtUtil.generateToken(userId) 生成 token 返回给前端
  1. 配置 JWT 密钥
  • 在 application-dev.yml 配置足够长的密钥,保证安全
  1. 数据库 token 字段扩容
  • 如果需要存储 JWT,token 字段长度要设置为 VARCHAR(500) 以上

五、效果

  • 安全性提升:只认标准 Authorization,token 不易伪造
  • 兼容性好:新老用户都能正常访问
  • 维护方便:日志详细,问题易定位
  • 业务安全:用户被禁用后无法访问受保护接口
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值