Spring Security OAuth2.0(19):JWT令牌

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的应用场景

  1. 身份认证:用户登录后,服务器生成JWT返回给客户端
  2. 授权:访问受保护资源时,客户端携带JWT
  3. 信息交换:安全地在各方之间传递信息

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 生成令牌的最佳实践

  1. 密钥管理

    • 使用强密钥(至少256位)
    • 定期轮换密钥
    • 将密钥存储在安全的地方(如密钥管理服务)
  2. 声明设计

    • 避免存储敏感信息(如密码)
    • 控制令牌大小,避免过大
    • 使用标准声明提高互操作性
  3. 过期时间设置

    • 访问令牌:较短(如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 安全注意事项

  1. 令牌存储安全

    • 客户端:使用HttpOnly Cookie或安全存储
    • 服务端:验证签名前检查令牌格式
  2. 防止重放攻击

    • 使用jti(JWT ID)声明
    • 维护已使用令牌的黑名单
    • 设置合理的过期时间
  3. 签名算法选择

    • HS256:对称加密,性能好,需要安全保管密钥
    • RS256:非对称加密,更安全,公钥可公开
  4. 令牌撤销机制

    • 维护令牌黑名单
    • 使用短期令牌+刷新令牌
    • 关键操作需要重新认证
相关推荐
玛卡巴卡ldf1 小时前
【LeetCode 手撕算法】(细节知识点总结)
java·数据结构·算法·leetcode·力扣
一叶飘零_sweeeet2 小时前
IDEA 插件 Trae AI 最新全攻略(基于 TRAE AI: Coding Assistant 1.7.0.0)
java·intellij-idea·trae
KobeSacre2 小时前
CyclicBarrier 源码
java·jvm·算法
达达尼昂2 小时前
编码智能体中的模型选择与执行深度
人工智能·后端·程序员
用户6757049885022 小时前
写了 6 年 Go ,我终于领悟到了泛型的真正威力!
后端·go
我登哥MVP2 小时前
Hadoop成长史-从Nutch子项目到大数据生态王者
java·大数据·hadoop·分布式·云原生·云计算
进击切图仔2 小时前
SAM3 微调标注流水线和 Label Studio
java·服务器·前端
名字还没想好☜2 小时前
Go error 处理:errors.Is/As 与错误包装
开发语言·后端·golang·go·错误处理
java1234_小锋2 小时前
Maven 4 要来了:15 年后,Java 构建工具迎来“彻底重构“
java·重构·maven