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

470 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 企业级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实现**!🚀