jwt原理及Java中实现过程
作者:༒࿈༙྇洞察༙༙྇྇࿈༒
一、JWT 是什么?解决什么问题?
- 我们先来一张图看一下这个过程:
JWT(JSON Web Token)是一种把“认证信息(Claims)+ 完整性校验”打包成 自包含 的字符串的规范。
它主要用于无状态认证:服务端验证签名即可信任其中的身份与权限,无需每次查库或维护会话(session)。
- 无状态:后端不存会话;减少分布式共享状态的复杂度。
- 可扩展:把自定义字段写入 claims(如角色、租户、权限)。
- 可委托:不同服务/网关只要有验证密钥就能核验并信任。
但请记住:JWT 只保证 完整性(没被篡改),默认不保密(除非用 JWE 加密)。敏感信息不要塞进未加密的 JWT。
二、JWT 的结构与签名流程
JWT 的字符串形如:<header>.<payload>.<signature>
Header(JSON,Base64URL)
alg
: 签名算法(如HS256
/RS256
/ES256
/EdDSA
)typ
: 通常为"JWT"
kid
(可选):密钥标识,用于密钥轮换。
Payload/Claims(JSON,Base64URL)
常见注册声明:
iss
(颁发者)、sub
(主体,通常是用户ID)、aud
(受众)exp
(过期时间,秒级时间戳,必须!)、nbf
(不早于)、iat
(签发时间)jti
(唯一ID,用于一次性/黑名单)
以及你的自定义字段:roles
、tenantId
、scope
等。
Signature
- 计算方式:
signature = Sign( base64url(header) + "." + base64url(payload), key, alg )
- 验证时:使用共享密钥(HMAC)或公钥(RSA/ECDSA/EdDSA)验证。
JWS vs JWE:
- JWS(最常用):签名但不加密;任何人拿到 token 都能看到 payload。
- JWE:加密(可选),适用于含敏感信息的场景。
三、JWT 使用流程(最小闭环)
- 登录:用户名密码校验成功 → 颁发短期 Access Token(JWT)+ 较长期 Refresh Token(不可见给前端或放 HttpOnly Cookie)。
- 访问 API:前端将
Authorization: Bearer <jwt>
送给后端。 - 后端:验证签名、校验
exp/nbf/aud/iss
等 → 放行。 - 刷新:Access Token 过期,用 Refresh Token 换新(做轮换与失效控制)。
- 登出/撤销(可选):把
jti
或 refresh 的标识加入黑名单,或进行密钥轮换。
四、常见安全陷阱(一定要看)
- ❌ 不设置
exp
(永不过期,风险极大)。 - ❌
alg: none
(严格禁用)。 - ❌ 密钥混淆:把对称密钥误当作公钥发布;或同一
kid
指向错密钥。 - ❌ HS256 在多服务扩散:一旦泄露,所有服务都可伪造。跨服务建议
RS256/ES256/EdDSA
(私钥签、公钥验)。 - ❌ 不校验
aud/iss
:导致“错配 token”被误信任。 - ❌ 客户端 localStorage 存储 → 易受 XSS 影响。推荐 HttpOnly + Secure + SameSite Cookie。
- ❌ 忽视 CSRF:若用 Cookie 携带 Access Token,要配合 SameSite + CSRF 令牌 或改为 Bearer 头。
- ❌ 不做 Refresh Token 轮换 与黑名单 → 被盗后长期可用。
- ❌ 把敏感信息(如身份证、银行卡、密码)塞进未加密 JWT。
五、Java 手写(JJWT)创建与验证
1) 依赖(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> <!-- for JSON serialization --> <scope>runtime</scope> </dependency>
若用 RSA/EC/EdDSA:还需对应的 jjwt-xxx
或者用 java.security
生成密钥。
2) 生成密钥(示例:RSA 与 Ed25519)
// RSA 2048(推荐生产至少 2048) KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); KeyPair rsaKeyPair = kpg.generateKeyPair(); // Ed25519(更轻更快) KeyPairGenerator ed = KeyPairGenerator.getInstance("Ed25519"); KeyPair edKeyPair = ed.generateKeyPair();
3) 颁发 JWT(RS256 或 EdDSA)
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.time.Instant; import java.util.Date; import java.util.Map; // 使用 RSA 私钥签名(RS256) String token = Jwts.builder() .setHeaderParam("kid", "key-2025-08") // 便于轮换 .setIssuer("https://auth.example.com") .setSubject("user-123") .setAudience("api.example.com") .setIssuedAt(new Date()) .setExpiration(Date.from(Instant.now().plusSeconds(900))) // 15 分钟 .addClaims(Map.of( "roles", new String[]{"ADMIN","USER"}, "tenantId", "t-1001" )) .signWith(rsaKeyPair.getPrivate()) // 默认按密钥类型选择 RS256/ES256/EdDSA .compact();
signWith(PrivateKey)
:JJWT 会自动选合适 alg
;如想强制算法,可用新版签名 API 指定 SignatureAlgorithm
.
4) 验证 JWT(公钥验签 + 校验声明)
import io.jsonwebtoken.*; Jws<Claims> jws = Jwts.parserBuilder() .requireIssuer("https://auth.example.com") .requireAudience("api.example.com") .setAllowedClockSkewSeconds(60) // 允许 60s 时钟偏差 .setSigningKey(rsaKeyPair.getPublic()) // 或使用 JWKS 拉取的公钥 .build() .parseClaimsJws(token); Claims claims = jws.getBody(); String userId = claims.getSubject(); String[] roles = claims.get("roles", String[].class);
校验失败会抛异常(如 ExpiredJwtException
、SignatureException
)。
在网关/过滤器中统一捕获 → 返回 401/403。
六、Spring Boot(Resource Server)零胶水校验
最省心的是让 Spring Security 资源服务器替你做解析与校验,它支持 JWK 集合(JWKS) 自动远程拉取公钥。
1) 依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency>
2) application.yml(通过 JWKS URL 校验)
server: port: 8080 spring: security: oauth2: resourceserver: jwt: jwk-set-uri: https://auth.example.com/.well-known/jwks.json issuer-uri: https://auth.example.com # 建议同时配置,做 iss 校验
你的授权服务器(自建或第三方,如 Auth0/Keycloak/Spring Authorization Server)对外暴露 JWKS
。资源服自动缓存和轮询按 kid
取公钥。
3) 安全配置
@Bean SecurityFilterChain security(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // 如果前端走 Bearer 头,可关;若走 Cookie,需要保留并配置 CSRF .authorizeHttpRequests(reg -> reg .requestMatchers("/public/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter())) ); return http.build(); } // 可选:把自定义 claims 映射为权限 Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthConverter() { return jwt -> { Collection<GrantedAuthority> authorities = new ArrayList<>(); List<String> roles = jwt.getClaimAsStringList("roles"); if (roles != null) { roles.forEach(r -> authorities.add(new SimpleGrantedAuthority("ROLE_" + r))); } return new JwtAuthenticationToken(jwt, authorities, jwt.getSubject()); }; }
4) 控制器示例
@RestController public class DemoController { @GetMapping("/me") public Map<String, Object> me(@AuthenticationPrincipal Jwt jwt) { return Map.of( "sub", jwt.getSubject(), "roles", jwt.getClaimAsStringList("roles"), "tenantId", jwt.getClaim("tenantId") ); } @GetMapping("/admin/hello") public String admin() { return "hello, admin"; } }
七、发行端:用 Spring Authorization Server 签发 JWT(概念位)
如果你既要颁发又要验证:
- 引入
spring-authorization-server
,配置客户端、用户认证、签名密钥(支持 RSA/ECDSA/EdDSA),开启 /.well-known/jwks.json。 - 认证成功后,框架自动颁发 Access Token(JWT) 与 Refresh Token。
- 资源服务器只需
issuer-uri
/jwk-set-uri
即可对接。
好处:密钥管理、轮换、标准化授权流程(OAuth2/OIDC) 都交给框架;你专注业务。
八、Cookie vs Header、CSRF 与前端存储
推荐:Authorization: Bearer <jwt>
置于请求头,前端保存在内存(刷新丢失)或安全容器;刷新策略依赖 HttpOnly Refresh Cookie。
若必须把 Access Token 放 Cookie:
- 设置:
HttpOnly + Secure + SameSite=Lax/Strict
; - 开启并正确处理 CSRF 防护(基于 Cookie 的双重提交策略或框架自带 CSRF Token)。
不要放 localStorage(XSS 风险大)。
九、刷新与撤销(实战策略)
短期 Access Token(5–15 分钟) + 长期 Refresh Token(7–30 天)。
Refresh Token 轮换:每次刷新都颁发新 refresh,并使旧的失效(存库并维护 revoked
标记或版本号)。
黑名单/撤销:
- 记录 Access Token 的
jti
(可选)用于紧急撤销; - 更常用的是缩短 Access Token寿命 + 轮换 Refresh;
- 密钥轮换:更换私钥(新
kid
),强制旧 token 逐步失效(需兼容一段时间,等旧 token 过期)。
十、微服务与网关
- 首选:网关或每个服务自行校验 JWT(拿到 JWKS 公钥本地验);不要把解析结果当作“可信 JSON”直接传递。
- aud/iss:为不同受众(微服务)使用不同
aud
,防止“错用 token”。 - 性能:缓存 JWKS、公钥对象;JWT 验证成本很低,通常不是瓶颈。
十一、完整示例:无授权服务器时的“轻量颁发 + 校验”
1) 颁发端(登录成功后)
// 假设你用 Spring Security 自己做用户名/密码认证 @PostMapping("/auth/login") public Map<String, String> login(@RequestBody LoginReq req) { // 1. 校验用户名密码(略) // 2. 颁发 Token Instant now = Instant.now(); String access = Jwts.builder() .setHeaderParam("kid", "key-2025-08") .setIssuer("https://auth.example.com") .setSubject("user-" + req.username()) .setAudience("api.example.com") .setIssuedAt(Date.from(now)) .setExpiration(Date.from(now.plusSeconds(900))) .claim("roles", List.of("USER")) .signWith(rsaPrivateKey) // 你的私钥 .compact(); String refreshId = UUID.randomUUID().toString(); // 存入数据库,标记有效 String refresh = Jwts.builder() .setIssuer("https://auth.example.com") .setSubject("user-" + req.username()) .setId(refreshId) .setIssuedAt(Date.from(now)) .setExpiration(Date.from(now.plusSeconds(30 * 24 * 3600))) // 30 天 .signWith(rsaPrivateKey) .compact(); // refresh 建议放 HttpOnly Cookie 返回 return Map.of("access_token", access, "token_type", "Bearer"); }
2) 刷新端点
@PostMapping("/auth/refresh") public Map<String, String> refresh(@CookieValue("refresh_token") String refreshToken) { // 1. 验证 refreshToken 签名与过期 Jws<Claims> jws = Jwts.parserBuilder() .setSigningKey(rsaPublicKey) .build() .parseClaimsJws(refreshToken); String jti = jws.getBody().getId(); // 2. 校验 jti 是否未吊销,且未被使用(轮换) // 3. 颁发新 access(并轮换 refresh:生成新 refresh,旧的置为 revoked) String newAccess = ...; // Set-Cookie: refresh_token=<new>; HttpOnly; Secure; SameSite=Strict return Map.of("access_token", newAccess, "token_type", "Bearer"); }
3) 资源服务(校验端,若不用 Resource Server Starter)
自定义过滤器(不建议重复造轮子,演示用):
@Component public class JwtAuthFilter extends OncePerRequestFilter { private final PublicKey publicKey; public JwtAuthFilter(PublicKey publicKey) { this.publicKey = publicKey; } @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { String auth = req.getHeader("Authorization"); if (auth != null && auth.startsWith("Bearer ")) { String token = auth.substring(7); try { Jws<Claims> jws = Jwts.parserBuilder() .requireIssuer("https://auth.example.com") .requireAudience("api.example.com") .setAllowedClockSkewSeconds(60) .setSigningKey(publicKey) .build() .parseClaimsJws(token); Claims c = jws.getBody(); List<GrantedAuthority> auths = new ArrayList<>(); List<String> roles = c.get("roles", List.class); if (roles != null) roles.forEach(r -> auths.add(new SimpleGrantedAuthority("ROLE_" + r))); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(c.getSubject(), null, auths); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (JwtException e) { res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } chain.doFilter(req, res); } }
十二、测试要点清单(上线前自查)
- ✅
exp/nbf/iat/iss/aud
均有严格校验;允许小量 clock skew。 - ✅ 禁止
alg: none
,不允许客户端指定算法。 - ✅ 使用 非对称算法(RS/ES/EdDSA) 做跨服务验证;对称密钥仅限单体/网关内部。
- ✅ 开启并演练 密钥轮换(
kid
+ JWKS),旧公钥保留到所有 token 过期。 - ✅ 访问控制基于 最小权限(角色/权限来源可在 claims 或 DB)。
- ✅ 选择合适的 存储与传输方式(Bearer 头 或 HttpOnly Cookie + CSRF 防护)。
- ✅ 短期 Access + 轮换 Refresh;可选黑名单(
jti
)应急撤销。 - ✅ 日志中绝不打印完整 token(最多打前后各 6 位用于排错)。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。