修正项目结构

This commit is contained in:
2026-05-31 04:46:30 +08:00
parent e69168775c
commit c2663c9ddd
7 changed files with 0 additions and 0 deletions
+469
View File
@@ -0,0 +1,469 @@
# 企业级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实现**!🚀