263 lines
5.9 KiB
Markdown
263 lines
5.9 KiB
Markdown
# JWT企业级加密 - 快速参考卡
|
|
|
|
## 🔑 密钥管理
|
|
|
|
### 生成新密钥对
|
|
```bash
|
|
.\generate-jwt-keys.ps1
|
|
```
|
|
|
|
### 配置密钥
|
|
```properties
|
|
jwt.public-key=YOUR_PUBLIC_KEY_BASE64
|
|
jwt.private-key=YOUR_PRIVATE_KEY_BASE64
|
|
jwt.expiration=86400000
|
|
```
|
|
|
|
---
|
|
|
|
## 💻 代码速查
|
|
|
|
### 生成Token
|
|
```java
|
|
// 基础用法
|
|
String token = JwtUtil.generateToken(userId, username);
|
|
|
|
// 带额外声明
|
|
Map<String, Object> claims = new HashMap<>();
|
|
claims.put("role", "admin");
|
|
String token = JwtUtil.generateToken(userId, username, claims);
|
|
```
|
|
|
|
### 验证Token
|
|
```java
|
|
// 基础验证
|
|
boolean isValid = JwtUtil.validateToken(token);
|
|
|
|
// 完整验证(含黑名单)
|
|
boolean isValid = JwtUtil.validateToken(token, blacklistService);
|
|
```
|
|
|
|
### 提取信息
|
|
```java
|
|
String userId = JwtUtil.getUserIdFromToken(token);
|
|
String username = JwtUtil.getUsernameFromToken(token);
|
|
String jti = JwtUtil.getJtiFromToken(token);
|
|
long remainingTime = JwtUtil.getTokenRemainingTime(token); // 秒
|
|
```
|
|
|
|
### Token注销
|
|
```java
|
|
String jti = JwtUtil.getJtiFromToken(token);
|
|
long remainingTime = JwtUtil.getTokenRemainingTime(token);
|
|
blacklistService.addToBlacklist(jti, remainingTime);
|
|
```
|
|
|
|
### 刷新Token
|
|
```java
|
|
String newToken = JwtUtil.refreshToken(oldToken);
|
|
```
|
|
|
|
---
|
|
|
|
## 🛡️ Claims字段
|
|
|
|
| 字段 | 说明 | 示例 |
|
|
|------|------|------|
|
|
| `jti` | JWT唯一ID | "5f3e2a8b-1b4d-4e..." |
|
|
| `iss` | 签发者 | "uni-login-system" |
|
|
| `aud` | 受众 | "uni-login-client" |
|
|
| `sub` | 主题(用户名) | "admin" |
|
|
| `iat` | 签发时间 | 1716624000 |
|
|
| `nbf` | 生效时间 | 1716623995 |
|
|
| `exp` | 过期时间 | 1716710400 |
|
|
| `userId` | 用户ID(自定义) | "123" |
|
|
|
|
---
|
|
|
|
## ⚠️ 异常类型
|
|
|
|
| 异常类 | HTTP码 | 触发条件 |
|
|
|--------|--------|----------|
|
|
| `JwtTokenExpiredException` | 401 | Token已过期 |
|
|
| `JwtSignatureInvalidException` | 401 | 签名验证失败 |
|
|
| `JwtMalformedException` | 400 | Token格式错误 |
|
|
| `JwtTokenBlacklistedException` | 401 | Token已被注销 |
|
|
|
|
---
|
|
|
|
## 🔧 密钥轮换
|
|
|
|
```java
|
|
// 1. 生成新密钥对
|
|
KeyPair newKeyPair = RsaKeyGenerator.generateKeyPair();
|
|
String newPublicKey = RsaKeyGenerator.encodePublicKey((RSAPublicKey) newKeyPair.getPublic());
|
|
String newPrivateKey = RsaKeyGenerator.encodePrivateKey((RSAPrivateKey) newKeyPair.getPrivate());
|
|
|
|
// 2. 执行轮换(旧密钥仍可用于验证)
|
|
JwtKeyManager.rotateKeys(newPublicKey, newPrivateKey);
|
|
|
|
// 3. 过渡期后清除旧密钥(建议7-30天后)
|
|
JwtKeyManager.clearPreviousKey();
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Redis黑名单
|
|
|
|
```java
|
|
// 加入黑名单
|
|
blacklistService.addToBlacklist(jti, expirationSeconds);
|
|
|
|
// 检查是否在黑名单中
|
|
boolean isBlacklisted = blacklistService.isBlacklisted(jti);
|
|
|
|
// 获取黑名单大小
|
|
long size = blacklistService.getBlacklistSize();
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 拦截器模板
|
|
|
|
```java
|
|
@Component
|
|
public class JwtAuthenticationInterceptor implements HandlerInterceptor {
|
|
|
|
@Autowired
|
|
private TokenBlacklistService blacklistService;
|
|
|
|
@Override
|
|
public boolean preHandle(HttpServletRequest request,
|
|
HttpServletResponse response,
|
|
Object handler) {
|
|
String authHeader = request.getHeader("Authorization");
|
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
|
throw new JwtMalformedException("缺少认证令牌");
|
|
}
|
|
|
|
String token = authHeader.substring(7);
|
|
|
|
if (!JwtUtil.validateToken(token, blacklistService)) {
|
|
throw new JwtSignatureInvalidException("令牌无效");
|
|
}
|
|
|
|
request.setAttribute("userId", JwtUtil.getUserIdFromToken(token));
|
|
request.setAttribute("username", JwtUtil.getUsernameFromToken(token));
|
|
|
|
return true;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 前端使用
|
|
|
|
### 存储Token
|
|
```javascript
|
|
// 推荐:HttpOnly Cookie(后端设置)
|
|
// Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict
|
|
|
|
// 或:LocalStorage(简单场景)
|
|
localStorage.setItem('token', token);
|
|
```
|
|
|
|
### 发送请求
|
|
```javascript
|
|
// Axios
|
|
axios.get('/api/v1/profile', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
// Fetch
|
|
fetch('/api/v1/profile', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
```
|
|
|
|
### 处理401错误
|
|
```javascript
|
|
axios.interceptors.response.use(
|
|
response => response,
|
|
error => {
|
|
if (error.response?.status === 401) {
|
|
// Token过期或无效,跳转登录页
|
|
window.location.href = '/login';
|
|
}
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 🔍 调试技巧
|
|
|
|
### 查看Token内容(不验证)
|
|
```java
|
|
Claims claims = Jwts.parser()
|
|
.build()
|
|
.parseSignedClaims(token)
|
|
.getPayload();
|
|
|
|
System.out.println("JTI: " + claims.getId());
|
|
System.out.println("Subject: " + claims.getSubject());
|
|
System.out.println("Expires: " + claims.getExpiration());
|
|
```
|
|
|
|
### 在线解码
|
|
访问 [jwt.io](https://jwt.io/) 粘贴Token即可查看
|
|
|
|
---
|
|
|
|
## ⚡ 性能优化
|
|
|
|
```properties
|
|
# 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
|
|
```
|
|
|
|
---
|
|
|
|
## 🚫 安全禁忌
|
|
|
|
❌ 不要在日志中打印完整Token
|
|
❌ 不要在前端LocalStorage存储敏感Token
|
|
❌ 不要硬编码私钥到代码中
|
|
❌ 不要忽略Token验证异常
|
|
❌ 不要在HTTP下传输Token
|
|
|
|
✅ 始终使用HTTPS
|
|
✅ 使用HttpOnly Cookie存储
|
|
✅ 定期轮换密钥
|
|
✅ 设置合理的过期时间
|
|
✅ 监控黑名单增长
|
|
|
|
---
|
|
|
|
## 📞 常见问题
|
|
|
|
**Q: Token验证失败?**
|
|
A: 检查公钥/私钥是否匹配,确认Token未损坏
|
|
|
|
**Q: 如何使所有Token失效?**
|
|
A: 修改密码后将该用户的所有JTI加入黑名单
|
|
|
|
**Q: 密钥多久轮换一次?**
|
|
A: 建议30-90天,高安全要求可缩短至7天
|
|
|
|
**Q: Redis宕机怎么办?**
|
|
A: 降级为基础验证(仅验证签名),记录告警
|
|
|
|
---
|
|
|
|
**详细文档**: [JWT_ENTERPRISE_GUIDE.md](JWT_ENTERPRISE_GUIDE.md)
|
|
**代码示例**: [JWT_USAGE_EXAMPLES.md](JWT_USAGE_EXAMPLES.md)
|