470 lines
12 KiB
Markdown
470 lines
12 KiB
Markdown
# 企业级JWT使用示例
|
||
|
||
## 📖 目录
|
||
- [基本使用](#基本使用)
|
||
- [高级功能](#高级功能)
|
||
- [最佳实践](#最佳实践)
|
||
|
||
---
|
||
|
||
## 基本使用
|
||
|
||
### 1. 生成Token(登录时)
|
||
|
||
当前项目已在 `UserServiceImpl.login()` 中集成:
|
||
|
||
```java
|
||
// 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;
|
||
}
|
||
```
|
||
|
||
**返回示例:**
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "登录成功",
|
||
"data": {
|
||
"userId": 1,
|
||
"username": "admin",
|
||
"token": "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI1ZjNlMmE4Yi0xYjRkLTRl..."
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 验证Token(拦截器/过滤器中)
|
||
|
||
创建认证拦截器示例:
|
||
|
||
```java
|
||
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("令牌无效");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
注册拦截器:
|
||
|
||
```java
|
||
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中使用
|
||
|
||
```java
|
||
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注销(登出)
|
||
|
||
```java
|
||
@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
|
||
|
||
```java
|
||
// 登录时添加角色信息
|
||
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);
|
||
```
|
||
|
||
在拦截器中提取:
|
||
|
||
```java
|
||
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刷新
|
||
|
||
```java
|
||
@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失效
|
||
|
||
```java
|
||
@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
|
||
# Nginx配置强制HTTPS
|
||
server {
|
||
listen 443 ssl;
|
||
ssl_certificate /path/to/cert.pem;
|
||
ssl_certificate_key /path/to/key.pem;
|
||
}
|
||
```
|
||
|
||
2. **Token存储在HttpOnly Cookie中(前端)**
|
||
```javascript
|
||
// 前端设置Cookie
|
||
document.cookie = `token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/`;
|
||
```
|
||
|
||
3. **设置合理的过期时间**
|
||
```properties
|
||
# 短期Token(推荐)
|
||
jwt.expiration=3600000 # 1小时
|
||
|
||
# 或使用Refresh Token机制
|
||
# Access Token: 15分钟
|
||
# Refresh Token: 7天
|
||
```
|
||
|
||
4. **监控黑名单大小**
|
||
```java
|
||
@Scheduled(fixedRate = 3600000) // 每小时
|
||
public void monitorBlacklist() {
|
||
long size = blacklistService.getBlacklistSize();
|
||
if (size > 10000) {
|
||
log.warn("JWT黑名单数量异常: {}", size);
|
||
}
|
||
}
|
||
```
|
||
|
||
5. **定期轮换密钥**
|
||
```java
|
||
@Scheduled(cron = "0 0 0 1 * ?") // 每月1号凌晨
|
||
public void rotateKeys() {
|
||
// 1. 生成新密钥对
|
||
// 2. 执行轮换
|
||
JwtKeyManager.rotateKeys(newPublicKey, newPrivateKey);
|
||
|
||
// 3. 通知所有服务更新公钥
|
||
// 4. 30天后清除旧密钥
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### ❌ 避免的做法
|
||
|
||
1. **❌ 不要在日志中打印完整Token**
|
||
```java
|
||
// 错误
|
||
log.info("Token: {}", token);
|
||
|
||
// 正确
|
||
log.info("Token JTI: {}", JwtUtil.getJtiFromToken(token));
|
||
```
|
||
|
||
2. **❌ 不要在前端LocalStorage存储敏感Token**
|
||
```javascript
|
||
// 不安全
|
||
localStorage.setItem('token', token);
|
||
|
||
// 推荐
|
||
// 使用HttpOnly Cookie
|
||
```
|
||
|
||
3. **❌ 不要硬编码密钥**
|
||
```properties
|
||
# 错误 - 不要提交到Git
|
||
jwt.private-key=MIIEvAIBADANBgkqh...
|
||
|
||
# 推荐 - 使用环境变量
|
||
jwt.private-key=${JWT_PRIVATE_KEY}
|
||
```
|
||
|
||
4. **❌ 不要忽略Token验证异常**
|
||
```java
|
||
// 错误
|
||
try {
|
||
JwtUtil.validateToken(token);
|
||
} catch (Exception e) {
|
||
// 静默忽略 - 危险!
|
||
}
|
||
|
||
// 正确
|
||
if (!JwtUtil.validateToken(token, blacklistService)) {
|
||
throw new UnauthorizedException("认证失败");
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 调试技巧
|
||
|
||
### 查看Token内容(不验证签名)
|
||
|
||
```java
|
||
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](https://jwt.io/) - 粘贴Token即可查看内容(注意:不要在生产环境使用)
|
||
|
||
---
|
||
|
||
## 📊 性能优化
|
||
|
||
1. **Redis连接池配置**
|
||
```properties
|
||
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. **本地缓存公钥**
|
||
```java
|
||
// JwtKeyManager已使用AtomicReference,线程安全且高性能
|
||
RSAPublicKey publicKey = JwtKeyManager.getPublicKey();
|
||
```
|
||
|
||
3. **批量黑名单检查**
|
||
```java
|
||
// 对于高频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实现**!🚀
|