第一次提交
This commit is contained in:
@@ -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实现**!🚀
|
||||
Reference in New Issue
Block a user