Files
Uni_Login_system/doc/JWT_USAGE_EXAMPLES.md
T
2026-05-31 04:46:30 +08:00

12 KiB
Raw Blame History

企业级JWT使用示例

📖 目录


基本使用

1. 生成Token(登录时)

当前项目已在 UserServiceImpl.login() 中集成:

// src/main/java/com/caiji/uls/service/impl/UserServiceImpl.java
@Override
public LoginRespond login(String username, String password) {
    // ... 验证用户和密码 ...
    
    LoginRespond respond = new LoginRespond();
    respond.setUserId(Long.valueOf(user.getId()));
    respond.setUsername(user.getUsername());
    
    // ✅ 已集成企业级RSA JWT
    respond.setToken(JwtUtil.generateToken(user.getId().toString(), user.getUsername()));
    
    return respond;
}

返回示例:

{
  "code": 200,
  "message": "登录成功",
  "data": {
    "userId": 1,
    "username": "admin",
    "token": "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI1ZjNlMmE4Yi0xYjRkLTRl..."
  }
}

2. 验证Token(拦截器/过滤器中)

创建认证拦截器示例:

package com.caiji.uls.config;

import com.caiji.uls.utils.exception.jwt.*;
import com.caiji.uls.utils.jwt.JwtUtil;
import com.caiji.uls.utils.jwt.TokenBlacklistService;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class JwtAuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenBlacklistService blacklistService;

    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) {
        
        // 从Header获取Token
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            throw new JwtMalformedException("缺少认证令牌");
        }

        String token = authHeader.substring(7); // 去掉"Bearer "前缀

        try {
            // ✅ 完整验证(签名 + 格式 + 黑名单)
            if (!JwtUtil.validateToken(token, blacklistService)) {
                throw new JwtSignatureInvalidException("令牌验证失败");
            }

            // 提取用户信息并存储到请求属性
            String userId = JwtUtil.getUserIdFromToken(token);
            String username = JwtUtil.getUsernameFromToken(token);
            
            request.setAttribute("userId", userId);
            request.setAttribute("username", username);
            
            return true; // 通过验证
            
        } catch (JwtTokenExpiredException e) {
            throw new JwtTokenExpiredException("令牌已过期");
        } catch (JwtTokenBlacklistedException e) {
            throw new JwtTokenBlacklistedException("令牌已被注销");
        } catch (JwtException e) {
            throw new JwtSignatureInvalidException("令牌无效");
        }
    }
}

注册拦截器:

package com.caiji.uls.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private JwtAuthenticationInterceptor jwtAuthenticationInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtAuthenticationInterceptor)
                .addPathPatterns("/api/v1/**")  // 保护所有API
                .excludePathPatterns(
                    "/api/v1/login",     // 排除登录接口
                    "/api/v1/register"   // 排除注册接口
                );
    }
}

3. 在Controller中使用

package com.caiji.uls.controller;

import com.caiji.uls.dto.Uni_Respond;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1")
public class UserController {

    @GetMapping("/profile")
    public Uni_Respond getUserProfile(HttpServletRequest request) {
        // ✅ 从请求属性获取已验证的用户信息
        String userId = (String) request.getAttribute("userId");
        String username = (String) request.getAttribute("username");
        
        // 查询用户详细信息...
        
        Uni_Respond response = new Uni_Respond();
        response.setCode(200);
        response.setMessage("获取成功");
        response.setData(userInfo);
        return response;
    }
}

高级功能

4. Token注销(登出)

@PostMapping("/logout")
public Uni_Respond logout(HttpServletRequest request) {
    String authHeader = request.getHeader("Authorization");
    if (authHeader != null && authHeader.startsWith("Bearer ")) {
        String token = authHeader.substring(7);
        
        try {
            // 获取Token的JTI
            String jti = JwtUtil.getJtiFromToken(token);
            
            // 计算剩余有效期
            long remainingTime = JwtUtil.getTokenRemainingTime(token);
            
            // ✅ 加入黑名单
            blacklistService.addToBlacklist(jti, remainingTime);
            
        } catch (Exception e) {
            // Token可能已过期或无效,忽略
        }
    }
    
    Uni_Respond response = new Uni_Respond();
    response.setCode(200);
    response.setMessage("登出成功");
    return response;
}

5. 带额外声明的Token

// 登录时添加角色信息
Map<String, Object> claims = new HashMap<>();
claims.put("role", "admin");
claims.put("permissions", Arrays.asList("read", "write", "delete"));
claims.put("department", "IT");

String token = JwtUtil.generateToken(userId.toString(), username, claims);

在拦截器中提取:

Claims claims = JwtUtil.getClaimsFromToken(token);
String role = claims.get("role", String.class);
List<String> permissions = claims.get("permissions", List.class);

// 基于角色的访问控制
if ("admin".equals(role)) {
    // 管理员权限
}

6. Token刷新

@PostMapping("/refresh-token")
public Uni_Respond refreshToken(HttpServletRequest request) {
    String authHeader = request.getHeader("Authorization");
    if (authHeader == null || !authHeader.startsWith("Bearer ")) {
        throw new JwtMalformedException("缺少令牌");
    }
    
    String oldToken = authHeader.substring(7);
    
    // 验证旧Token
    if (!JwtUtil.validateToken(oldToken, blacklistService)) {
        throw new JwtSignatureInvalidException("令牌无效");
    }
    
    // ✅ 生成新Token(新的JTI和过期时间)
    String newToken = JwtUtil.refreshToken(oldToken);
    
    // 将旧Token加入黑名单
    String jti = JwtUtil.getJtiFromToken(oldToken);
    long remainingTime = JwtUtil.getTokenRemainingTime(oldToken);
    blacklistService.addToBlacklist(jti, remainingTime);
    
    Map<String, String> data = new HashMap<>();
    data.put("token", newToken);
    
    Uni_Respond response = new Uni_Respond();
    response.setCode(200);
    response.setMessage("刷新成功");
    response.setData(data);
    return response;
}

7. 修改密码后使所有Token失效

@PostMapping("/change-password")
public Uni_Respond changePassword(@RequestBody ChangePasswordRequest req) {
    // 1. 验证旧密码
    // 2. 更新新密码
    
    // 3. ✅ 将所有该用户的Token加入黑名单
    //    (实际项目中需要维护用户ID -> JTI列表的映射)
    Set<String> userTokens = redisTemplate.opsForSet()
        .members("user:tokens:" + userId);
    
    for (String token : userTokens) {
        try {
            String jti = JwtUtil.getJtiFromToken(token);
            blacklistService.addToBlacklist(jti, 86400); // 24小时
        } catch (Exception e) {
            // Token可能已过期
        }
    }
    
    // 4. 清除缓存
    redisTemplate.delete("user:tokens:" + userId);
    
    Uni_Respond response = new Uni_Respond();
    response.setCode(200);
    response.setMessage("密码修改成功,请重新登录");
    return response;
}

最佳实践

推荐做法

  1. 始终使用HTTPS传输Token

    # Nginx配置强制HTTPS
    server {
        listen 443 ssl;
        ssl_certificate /path/to/cert.pem;
        ssl_certificate_key /path/to/key.pem;
    }
    
  2. Token存储在HttpOnly Cookie中(前端)

    // 前端设置Cookie
    document.cookie = `token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/`;
    
  3. 设置合理的过期时间

    # 短期Token(推荐)
    jwt.expiration=3600000  # 1小时
    
    # 或使用Refresh Token机制
    # Access Token: 15分钟
    # Refresh Token: 7天
    
  4. 监控黑名单大小

    @Scheduled(fixedRate = 3600000) // 每小时
    public void monitorBlacklist() {
        long size = blacklistService.getBlacklistSize();
        if (size > 10000) {
            log.warn("JWT黑名单数量异常: {}", size);
        }
    }
    
  5. 定期轮换密钥

    @Scheduled(cron = "0 0 0 1 * ?") // 每月1号凌晨
    public void rotateKeys() {
        // 1. 生成新密钥对
        // 2. 执行轮换
        JwtKeyManager.rotateKeys(newPublicKey, newPrivateKey);
    
        // 3. 通知所有服务更新公钥
        // 4. 30天后清除旧密钥
    }
    

避免的做法

  1. 不要在日志中打印完整Token

    // 错误
    log.info("Token: {}", token);
    
    // 正确
    log.info("Token JTI: {}", JwtUtil.getJtiFromToken(token));
    
  2. 不要在前端LocalStorage存储敏感Token

    // 不安全
    localStorage.setItem('token', token);
    
    // 推荐
    // 使用HttpOnly Cookie
    
  3. 不要硬编码密钥

    # 错误 - 不要提交到Git
    jwt.private-key=MIIEvAIBADANBgkqh...
    
    # 推荐 - 使用环境变量
    jwt.private-key=${JWT_PRIVATE_KEY}
    
  4. 不要忽略Token验证异常

    // 错误
    try {
        JwtUtil.validateToken(token);
    } catch (Exception e) {
        // 静默忽略 - 危险!
    }
    
    // 正确
    if (!JwtUtil.validateToken(token, blacklistService)) {
        throw new UnauthorizedException("认证失败");
    }
    

🔍 调试技巧

查看Token内容(不验证签名)

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

// 解析Token(仅用于调试,不验证签名)
Claims claims = Jwts.parser()
    .build()
    .parseSignedClaims(token)
    .getPayload();

System.out.println("JTI: " + claims.getId());
System.out.println("Subject: " + claims.getSubject());
System.out.println("Issuer: " + claims.getIssuer());
System.out.println("Expiration: " + claims.getExpiration());

在线JWT解码工具

  • jwt.io - 粘贴Token即可查看内容(注意:不要在生产环境使用)

📊 性能优化

  1. Redis连接池配置

    spring.data.redis.lettuce.pool.max-active=20
    spring.data.redis.lettuce.pool.max-idle=10
    spring.data.redis.lettuce.pool.min-idle=5
    
  2. 本地缓存公钥

    // JwtKeyManager已使用AtomicReference,线程安全且高性能
    RSAPublicKey publicKey = JwtKeyManager.getPublicKey();
    
  3. 批量黑名单检查

    // 对于高频API,可以考虑本地LRU缓存
    @Cacheable(value = "blacklist", key = "#jti")
    public boolean isBlacklisted(String jti) {
        return blacklistService.isBlacklisted(jti);
    }
    

🎯 总结

你现在拥有:

  • RSA-256非对称加密
  • 完整的Claims验证(ISS/AUD/NBF/JTI
  • Redis黑名单防重放
  • 密钥轮换机制
  • 分类异常处理
  • 全局异常处理器

这是符合OWASP标准的企业级JWT实现🚀