1. JWT令牌介绍
1.1 什么是JWT?
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。它由三部分组成:头部(Header)、载荷(Payload)和签名(Signature),通过点号(.)分隔。
JWT的主要特点:
- 紧凑性:体积小,可通过URL、POST参数或HTTP头部传输
- 自包含性:包含所有必要信息,减少数据库查询
- 安全性:使用签名确保令牌不被篡改
- 标准化:基于JSON格式,跨语言支持良好
1.2 JWT的结构
一个典型的JWT令牌格式:xxxxx.yyyyy.zzzzz
1. 头部(Header)
json
{
"alg": "HS256",
"typ": "JWT"
}
alg:签名算法(如HS256、RS256)typ:令牌类型,固定为"JWT"
2. 载荷(Payload)
包含声明(claims),分为三类:
- 注册声明:预定义的声明,如iss(签发者)、exp(过期时间)、sub(主题)
- 公共声明:可自定义,但应避免冲突
- 私有声明:自定义声明,用于传递业务数据
3. 签名(Signature)
使用头部指定的算法,对编码后的头部和载荷进行签名,确保令牌完整性。
1.3 JWT的应用场景
- 身份认证:用户登录后,服务器生成JWT返回给客户端
- 授权:访问受保护资源时,客户端携带JWT
- 信息交换:安全地在各方之间传递信息
2. 生成JWT令牌
2.1 准备工作
首先,我们需要添加JWT依赖。以Java Spring Boot为例:
xml
<!-- Maven依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
2.2 生成JWT令牌的步骤
步骤1:创建JWT工具类
java
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
// 密钥(实际项目中应从配置读取)
private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 令牌过期时间(24小时)
private static final long EXPIRATION_TIME = 24 * 60 * 60 * 1000;
/**
* 生成JWT令牌
* @param username 用户名
* @param userId 用户ID
* @param additionalClaims 额外声明
* @return JWT令牌字符串
*/
public static String generateToken(String username, String userId,
Map<String, Object> additionalClaims) {
// 合并声明
Map<String, Object> claims = new HashMap<>();
claims.put("sub", username);
claims.put("userId", userId);
claims.put("iat", new Date()); // 签发时间
if (additionalClaims != null) {
claims.putAll(additionalClaims);
}
// 生成令牌
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SECRET_KEY, SignatureAlgorithm.HS256)
.compact();
}
}
步骤2:生成包含自定义声明的令牌
java
public class JwtDemo {
public static void main(String[] args) {
// 自定义声明
Map<String, Object> customClaims = new HashMap<>();
customClaims.put("role", "admin");
customClaims.put("department", "IT");
customClaims.put("permissions", new String[]{"read", "write", "delete"});
// 生成令牌
String token = JwtUtil.generateToken("zhangsan", "123456", customClaims);
System.out.println("生成的JWT令牌:");
System.out.println(token);
// 解码查看内容(仅解码,不验证)
String[] parts = token.split("\\.");
System.out.println("\n解码后的内容:");
System.out.println("Header: " + new String(java.util.Base64.getUrlDecoder().decode(parts[0])));
System.out.println("Payload: " + new String(java.util.Base64.getUrlDecoder().decode(parts[1])));
}
}
2.3 生成令牌的最佳实践
-
密钥管理
- 使用强密钥(至少256位)
- 定期轮换密钥
- 将密钥存储在安全的地方(如密钥管理服务)
-
声明设计
- 避免存储敏感信息(如密码)
- 控制令牌大小,避免过大
- 使用标准声明提高互操作性
-
过期时间设置
- 访问令牌:较短(如15分钟-1小时)
- 刷新令牌:较长(如7天-30天)
3. 校验JWT令牌
3.1 校验令牌的步骤
步骤1:创建校验工具类
java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
public class JwtValidator {
private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
/**
* 验证并解析JWT令牌
* @param token JWT令牌
* @return 解析后的声明
* @throws Exception 验证失败时抛出异常
*/
public static Claims validateAndParseToken(String token) throws Exception {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 验证令牌是否有效
* @param token JWT令牌
* @return 是否有效
*/
public static boolean isValidToken(String token) {
try {
Claims claims = validateAndParseToken(token);
// 检查过期时间
Date expiration = claims.getExpiration();
if (expiration.before(new Date())) {
return false; // 令牌已过期
}
// 检查签发时间(可选)
Date issuedAt = claims.getIssuedAt();
if (issuedAt.after(new Date())) {
return false; // 签发时间在未来
}
return true;
} catch (Exception e) {
return false; // 签名无效或其他错误
}
}
/**
* 获取令牌中的用户信息
* @param token JWT令牌
* @return 用户信息
*/
public static UserInfo extractUserInfo(String token) {
try {
Claims claims = validateAndParseToken(token);
UserInfo userInfo = new UserInfo();
userInfo.setUsername(claims.getSubject());
userInfo.setUserId(claims.get("userId", String.class));
userInfo.setRole(claims.get("role", String.class));
userInfo.setIssuedAt(claims.getIssuedAt());
userInfo.setExpiration(claims.getExpiration());
return userInfo;
} catch (Exception e) {
return null;
}
}
// 用户信息类
public static class UserInfo {
private String username;
private String userId;
private String role;
private Date issuedAt;
private Date expiration;
// getters and setters
}
}
步骤2:在实际应用中使用校验
java
public class AuthInterceptor {
/**
* 拦截器中的令牌验证
*/
public boolean preHandle(String token) {
// 1. 检查令牌是否存在
if (token == null || token.isEmpty()) {
return false;
}
// 2. 验证令牌格式
if (!token.matches("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$")) {
return false;
}
// 3. 验证令牌有效性
if (!JwtValidator.isValidToken(token)) {
return false;
}
// 4. 提取用户信息并设置到上下文
JwtValidator.UserInfo userInfo = JwtValidator.extractUserInfo(token);
if (userInfo == null) {
return false;
}
// 设置用户信息到线程上下文
SecurityContext.setCurrentUser(userInfo);
return true;
}
}
3.2 校验令牌的常见场景
场景1:API接口鉴权
java
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/profile")
public ResponseEntity<?> getUserProfile(@RequestHeader("Authorization") String authHeader) {
// 提取Bearer令牌
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.status(401).body("未提供有效的认证令牌");
}
String token = authHeader.substring(7);
try {
// 验证令牌
Claims claims = JwtValidator.validateAndParseToken(token);
// 检查权限
String role = claims.get("role", String.class);
if (!"admin".equals(role) && !"user".equals(role)) {
return ResponseEntity.status(403).body("权限不足");
}
// 返回用户信息
Map<String, Object> response = new HashMap<>();
response.put("username", claims.getSubject());
response.put("userId", claims.get("userId"));
response.put("role", role);
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.status(401).body("令牌无效或已过期");
}
}
}
场景2:刷新令牌机制
java
public class TokenRefreshService {
/**
* 刷新访问令牌
* @param refreshToken 刷新令牌
* @return 新的访问令牌
*/
public String refreshAccessToken(String refreshToken) {
try {
// 验证刷新令牌
Claims claims = JwtValidator.validateAndParseToken(refreshToken);
// 检查令牌类型
String tokenType = claims.get("token_type", String.class);
if (!"refresh".equals(tokenType)) {
throw new IllegalArgumentException("不是刷新令牌");
}
// 检查是否在黑名单中(可选)
if (isTokenRevoked(refreshToken)) {
throw new IllegalArgumentException("令牌已被撤销");
}
// 生成新的访问令牌
Map<String, Object> newClaims = new HashMap<>();
newClaims.put("userId", claims.get("userId"));
newClaims.put("role", claims.get("role"));
return JwtUtil.generateToken(
claims.getSubject(),
claims.get("userId", String.class),
newClaims
);
} catch (Exception e) {
throw new RuntimeException("刷新令牌失败: " + e.getMessage());
}
}
private boolean isTokenRevoked(String token) {
// 实现令牌撤销检查逻辑
return false;
}
}
3.3 安全注意事项
-
令牌存储安全
- 客户端:使用HttpOnly Cookie或安全存储
- 服务端:验证签名前检查令牌格式
-
防止重放攻击
- 使用jti(JWT ID)声明
- 维护已使用令牌的黑名单
- 设置合理的过期时间
-
签名算法选择
- HS256:对称加密,性能好,需要安全保管密钥
- RS256:非对称加密,更安全,公钥可公开
-
令牌撤销机制
- 维护令牌黑名单
- 使用短期令牌+刷新令牌
- 关键操作需要重新认证