ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

详解 Spring Boot 中的 JWT 工具类:从原理到实践

详解 Spring Boot 中的 JWT 工具类:从原理到实践 前言在微服务和前后端分离架构盛行的今天JWTJSON Web Token已成为实现无状态认证的主流方案。本文将深入剖析一个标准的 Spring Boot JWT 工具类的实现细节帮助你理解每一行代码背后的原理并学会如何在实际项目中正确使用它。一、什么是 JWTJWT 是一种开放标准RFC 7519它定义了一种紧凑且自包含的方式用于在各方之间安全地传输信息。一个 JWT 由三部分组成用点号.分隔textHeader.Payload.SignatureHeader声明令牌类型和签名算法Payload存放声明Claims即实际要传递的数据Signature对前两部分的签名防止数据被篡改二、项目依赖在 Spring Boot 项目中使用 JWT需要引入 JJWT 库xmldependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency配置文件application.ymlyamljwt: secret: your-very-long-and-secure-secret-key-at-least-64-characters expiration: 86400000 # 24小时单位毫秒三、完整代码javaComponent public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.expiration}) private Long expiration; public String generateToken(Long userId, String username) { MapString, Object claims new HashMap(); claims.put(userId, userId); claims.put(username, username); return createToken(claims, username); } private String createToken(MapString, Object claims, String subject) { Date now new Date(); Date expirationDate new Date(now.getTime() expiration); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(now) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } public String getUsernameFromToken(String token) { return parseToken(token).getSubject(); } public Long getUserIdFromToken(String token) { return parseToken(token).get(userId, Long.class); } public boolean isTokenExpired(String token) { Date expiration parseToken(token).getExpiration(); return expiration.before(new Date()); } public boolean validateToken(String token) { try { parseToken(token); return !isTokenExpired(token); } catch (Exception e) { return false; } } }四、逐段深度解析4.1 类级别设计javaComponent public class JwtUtils {使用Component注解将这个类交给 Spring 容器管理。这样其他组件可以通过Autowired直接注入使用Spring 默认以单例模式管理保证整个应用只有一个实例线程安全工具类本身无状态所有状态都在方法参数中传递4.2 配置注入javaValue(${jwt.secret}) private String secret; Value(${jwt.expiration}) private Long expiration;1.Value的工作原理从 Spring 环境变量中读取配置${jwt.secret}对应配置文件中的键支持从多种来源获取配置文件、环境变量、命令行参数等这两个配置的重要性配置项作用安全要求secret签名密钥验证令牌真伪的唯一凭证必须足够长HMAC-SHA512 建议 64 字节以上生产环境禁止硬编码expiration令牌有效期毫秒决定用户登录状态的持久性根据业务安全需求设定太短用户体验差太长安全风险高2.secret字段作用JWT 的签名密钥。使用 HMAC-SHA512 算法时签名是对“头部 载荷”进行哈希然后与密钥混合生成签名。只有持有相同密钥的人才能验证签名或者签发新令牌。密钥泄露 任何人都可以伪造令牌因此密钥长度要足够HMAC-SHA512 建议密钥长度至少为 512 bits即 64 字节以上。不能硬编码在代码中生产环境应使用环境变量或密钥管理服务。配置文件中可写成${JWT_SECRET}从外部注入。3.expiration字段作用令牌的有效时长单位通常是毫秒。例如86400000表示 24 小时。在生成令牌时会将“当前时间 expiration”作为过期时间exp声明写入令牌。这个值决定了登录状态保持多久需要根据业务安全要求设定如半小时、2小时、一天。4.3 令牌生成入口generateTokenjavapublic String generateToken(Long userId, String username) { MapString, Object claims new HashMap(); claims.put(userId, userId); claims.put(username, username); return createToken(claims, username); }设计思路对外暴露的公共接口参数清晰只放入必要的用户标识userId和username调用私有的createToken完成实际构建⚠️安全提醒JWT 的 Payload 只是 Base64 编码不是加密。切勿放入密码、身份证号等敏感信息。1. 方法签名入参userId用户唯一IDLong类型、username用户名String类型。返回值生成的 JWT 字符串类似于eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJ0b20ifQ....。2. 构建自定义声明 MapMapString, Object claims new HashMap();创建一个空 Map。向其中放入两个键值对userId - userIdusername - username这些数据将成为 JWT 的私有声明Private Claims它们会被序列化到 JSON 格式的载荷中。重要JWT 的载荷部分是 Base64 编码不是加密所以不要放入密码、身份证号等敏感信息除非你对整个令牌再加密。3. 调用createToken(claims, username)将自定义声明 Map 和username作为第二个参数subject传入。这里的username被用作令牌的主题sub即令牌代表谁。方法名createToken是内部私有的只在本类内部调用对外暴露的是generateToken。4.4 令牌构建核心createTokenjavaprivate String createToken(MapString, Object claims, String subject) { Date now new Date(); Date expirationDate new Date(now.getTime() expiration); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(now) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); }JWT 标准声明方法对应声明含义setClaims()自定义声明放入我们自定义的 Map 数据setSubject()sub令牌主体代表谁持有这个令牌setIssuedAt()iat签发时间令牌何时生成setExpiration()exp过期时间令牌何时失效signWith()签名算法使用 HMAC-SHA512 密钥签名关于 Subject 的讨论当前代码使用username作为sub优点方便调试日志中可以直观看到用户名缺点如果用户名可以修改所有已签发的令牌中的sub都会变成旧值建议用不可变的userId作为sub用户名仅放在自定义 claims 中签名算法的选择算法类型适用场景HS256/HS512对称密钥单体应用或内部服务间通信RS256/RS512非对称密钥微服务架构需多方验证令牌1. 方法私有性private封装细节外部只能通过generateToken来调用便于统一控制放入哪些声明。2. 时间处理Date now new Date();获取当前系统时间作为签发时间iat。Date expirationDate new Date(now.getTime() expiration);expiration是上面注入的有效时长毫秒。now.getTime()返回从 UTC 1970.1.1 至今的毫秒数。相加得到过期时刻转换为Date对象。注意系统时间必须准确否则会导致令牌立即过期或有效期异常长。3.Jwts.builder()JJWT 库提供的建造者模式用于一步步构造 JWT。最终.compact()将一切序列化为一个字符串。4. 设置声明Claims.setClaims(claims)将传入的 Map 里的所有键值对放入 JWT 载荷。这会把userId和username两个字段写进去。如果 claims Map 里包含 JWT 保留声明如sub,exp,iat等后续用 setter 单独设置时会覆盖它们。这里 claims 只有自定义字段所以没问题。5. 设置主题Subject.setSubject(subject)设置sub声明值为传入的subject即username。在 JWT 规范中sub用于标识令牌的主体比如用户标识。这里选择用username作为sub意味着用“用户名”唯一标识令牌的所有者。优缺点方便调试和记录但如果用户名允许修改所有已签发的令牌中的sub会变成旧用户名。从纯粹标识角度看推荐用不可变的用户 ID如userId.toString()作为sub把用户名仅保留在私有声明中。6. 设置签发时间Issued At.setIssuedAt(now)设置iat声明表示令牌的签发时间可用于判断令牌是否是后来签发的比如配合nbf使用。7. 设置过期时间Expiration.setExpiration(expirationDate)设置exp声明表示令牌的过期时间戳。验证时必须检查此时间超过则令牌无效。8. 签名.signWith(SignatureAlgorithm.HS512, secret)SignatureAlgorithm.HS512表示使用 HMAC-SHA512 算法。这是对称密钥签名算法性能高适用于单服务或可信服务间。secret就是注入的密钥字符串。JJWT 库会根据算法要求对 secret 进行适当处理例如若长度不足会自动补全或报错版本不同策略不同。签名结果会生成第三部分Signature防止载荷被篡改。9. 生成最终字符串.compact()完成构建返回一个String格式为header.payload.signature每一部分用点号.分隔。HeaderBase64 编码的 JSON包含算法类型HS512和令牌类型JWT。PayloadBase64 编码的 JSON包含我们设置的所有声明。Signature对前两部分用密钥计算出的签名。4.5 令牌解析基础parseTokenjavapublic Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); }1. 方法作用对传入的 JWT 字符串进行解析验签并返回载荷Claims对象。如果签名不匹配、令牌格式错误、过期等会抛出异常具体异常类型来自 JJWT如SignatureException,ExpiredJwtException等。2.Jwts.parser()创建解析器建造者。3..setSigningKey(secret)设置验签密钥必须与签发时的密钥完全一致。如果是 HMAC验签时对头部载荷用同一密钥计算签名与令牌中的签名比对一致则通过。4..parseClaimsJws(token)解析签名的 JWTJWS。返回一个JwsClaims对象从中可以获取头部、载荷、签名等。此方法内部会进行签名校验不合法则抛出异常。5..getBody()从JwsClaims中提取载荷部分返回Claims对象。Claims接口扩展了MapString, Object可以方便地用get方法按名称取值。可能抛出的异常SignatureException签名不匹配篡改或密钥错误ExpiredJwtException令牌已过期MalformedJwtException令牌格式错误UnsupportedJwtException不支持的 JWT 格式4.6 辅助查询方法javapublic String getUsernameFromToken(String token) { return parseToken(token).getSubject(); } public Long getUserIdFromToken(String token) { return parseToken(token).get(userId, Long.class); } public boolean isTokenExpired(String token) { Date expiration parseToken(token).getExpiration(); return expiration.before(new Date()); }1.getUsernameFromTokenjavapublic String getUsernameFromToken(String token) { return parseToken(token).getSubject(); }调用parseToken获取Claims然后返回sub声明。因为设计时把username存入了sub所以这里返回的就是用户名。2.getUserIdFromTokenjavapublic Long getUserIdFromToken(String token) { return parseToken(token).get(userId, Long.class); }从Claims中获取键userId的值并指定类型为Long.class自动转换。如果该声明不存在返回null如果类型转换失败抛出异常。3.isTokenExpiredjavapublic boolean isTokenExpired(String token) { Date expiration parseToken(token).getExpiration(); return expiration.before(new Date()); }从Claims中获取exp声明过期时间。用before(new Date())判断如果过期时间早于“当前时间”说明已经过期返回true否则false。注意这个方法内部仍调用parseToken会进行一次验签所以验签和过期判断是分开的但通常会封装在统一验证里。4.7 综合验证validateTokenjavapublic boolean validateToken(String token) { try { parseToken(token); return !isTokenExpired(token); } catch (Exception e) { return false; } }两层校验签名校验parseToken(token)成功说明签名正确令牌未被篡改时效校验!isTokenExpired(token)确保令牌未过期异常处理策略捕获所有异常统一返回false生产环境建议记录 debug 日志便于排查安全事件1. 功能对令牌进行全面校验签名正确 未过期。通常在过滤器、拦截器中使用判断一个请求的令牌是否合法。2. 执行逻辑第一步parseToken(token)—— 如果签名无效、格式错误、密钥不匹配等都会抛出异常被 catch 捕获直接返回false。第二步如果解析成功调用!isTokenExpired(token)检查是否过期。如果未过期返回true如果已过期返回false。3. 异常捕获catch (Exception e)范围很宽会捕获所有可能的异常ExpiredJwtException已过期UnsupportedJwtException不支持的格式MalformedJwtException结构错误SignatureException签名错误IllegalArgumentException参数为空等这样设计能保证只要令牌不合法就统一返回 false业务侧不用关心具体异常。五、完整使用流程5.1 登录时生成令牌javaRestController public class AuthController { Autowired private JwtUtils jwtUtils; PostMapping(/login) public Result login(RequestBody LoginRequest request) { // 验证用户名密码省略 User user userService.login(request.getUsername(), request.getPassword()); // 生成 JWT String token jwtUtils.generateToken(user.getId(), user.getUsername()); return Result.success(token); } }5.2 请求拦截验证javaComponent public class JwtInterceptor extends HandlerInterceptorAdapter { Autowired private JwtUtils jwtUtils; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); if (jwtUtils.validateToken(token)) { Long userId jwtUtils.getUserIdFromToken(token); request.setAttribute(userId, userId); return true; } } response.setStatus(401); return false; } }六、常见问题与最佳实践6.1 密钥管理❌错误做法propertiesjwt.secret123456✅正确做法bash# 生成强密钥 openssl rand -base64 64 # 通过环境变量注入 export JWT_SECRET生成的强密钥不要在application.properties中明文写jwt.secretmySecretKey应该用环境变量或配置中心并确保复杂度和长度足够。HMAC-SHA512 要求密钥至少 512 bits64 字节来保证算法强度。如果配置的密钥太短JJWT 可能会报错取决于版本最好主动生成一个长密钥例如 Base64 编码的随机字符串。6.2 Subject 选择建议使用不可变的用户标识作为subjectjava// 修改 generateToken return createToken(claims, userId.toString()); // 用 userId 作为 sub6.3 如何实现强制下线JWT 是无状态的签发后无法主动失效。如果需要实现强制下线有以下方案黑名单机制维护一个已失效令牌的列表Redis版本号机制在用户表中存tokenVersion令牌中携带此值缩短有效期将令牌有效期设短如 15 分钟配合 Refresh Token6.4 Refresh Token 设计java// 简化的双令牌设计 public TokenPair generateTokenPair(Long userId, String username) { String accessToken generateToken(userId, username); // 短期 String refreshToken UUID.randomUUID().toString(); // 长期存 Redis return new TokenPair(accessToken, refreshToken); }6.5. 过期时间解析异常确保jwt.expiration的值是毫秒数。比如 3600000 表示 1 小时。如果意外配置成秒会导致令牌有效期极短用户频繁掉线。6.6. 签发时的时间同步如果服务器时间差异较大需要启用 NTP 时间同步否则令牌可能一签发就“即将过期”或“早已过期”。6.7. Subject 的设计当前subject使用username若用户名可改建议改成userId.toString()保持令牌主体的不变性。6.8. 缺少notBefore和jti没有设置nbfNot Before令牌签发后立即生效这通常是预期的。没有设置jtiJWT ID无法实现令牌的唯一性追踪和主动失效需配合黑名单。如果有单点登出或强制下线需求可以考虑加入jti。6.9. 异常日志validateToken吞掉了所有异常生产环境可能难以排查伪造令牌的情况。建议至少记录 debug 级别的异常信息以利于安全审计。6.10. 令牌刷新机制该类不涉及 refresh token需结合业务设计自动续期或单独刷新接口。6.11. 依赖的 JJWT 版本不同版本的 API 存在差异例如新版使用了Jwts.parserBuilder()而非Jwts.parser()。如果升级需要修改解析部分的代码。七、总结本文深入解析了一个完整的 Spring Boot JWT 工具类的实现涵盖了JWT 的基本结构和原理令牌的生成、解析、验证全流程密钥安全和配置管理实际项目中的使用方式常见问题和优化建议掌握这些知识你就能在实际项目中自信地使用 JWT 实现安全的无状态认证了。
返回列表