12 KiB
12 KiB
企业级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;
}
最佳实践
✅ 推荐做法
-
始终使用HTTPS传输Token
# Nginx配置强制HTTPS server { listen 443 ssl; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; } -
Token存储在HttpOnly Cookie中(前端)
// 前端设置Cookie document.cookie = `token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/`; -
设置合理的过期时间
# 短期Token(推荐) jwt.expiration=3600000 # 1小时 # 或使用Refresh Token机制 # Access Token: 15分钟 # Refresh Token: 7天 -
监控黑名单大小
@Scheduled(fixedRate = 3600000) // 每小时 public void monitorBlacklist() { long size = blacklistService.getBlacklistSize(); if (size > 10000) { log.warn("JWT黑名单数量异常: {}", size); } } -
定期轮换密钥
@Scheduled(cron = "0 0 0 1 * ?") // 每月1号凌晨 public void rotateKeys() { // 1. 生成新密钥对 // 2. 执行轮换 JwtKeyManager.rotateKeys(newPublicKey, newPrivateKey); // 3. 通知所有服务更新公钥 // 4. 30天后清除旧密钥 }
❌ 避免的做法
-
❌ 不要在日志中打印完整Token
// 错误 log.info("Token: {}", token); // 正确 log.info("Token JTI: {}", JwtUtil.getJtiFromToken(token)); -
❌ 不要在前端LocalStorage存储敏感Token
// 不安全 localStorage.setItem('token', token); // 推荐 // 使用HttpOnly Cookie -
❌ 不要硬编码密钥
# 错误 - 不要提交到Git jwt.private-key=MIIEvAIBADANBgkqh... # 推荐 - 使用环境变量 jwt.private-key=${JWT_PRIVATE_KEY} -
❌ 不要忽略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即可查看内容(注意:不要在生产环境使用)
📊 性能优化
-
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 -
本地缓存公钥
// JwtKeyManager已使用AtomicReference,线程安全且高性能 RSAPublicKey publicKey = JwtKeyManager.getPublicKey(); -
批量黑名单检查
// 对于高频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实现!🚀