jwt原理及Java中實現(xiàn)過程
一、JWT 是什么?解決什么問題?
- 我們先來一張圖看一下這個過程:
JWT(JSON Web Token)是一種把“認證信息(Claims)+ 完整性校驗”打包成 自包含 的字符串的規(guī)范。
它主要用于無狀態(tài)認證:服務端驗證簽名即可信任其中的身份與權限,無需每次查庫或維護會話(session)。
- 無狀態(tài):后端不存會話;減少分布式共享狀態(tài)的復雜度。
- 可擴展:把自定義字段寫入 claims(如角色、租戶、權限)。
- 可委托:不同服務/網(wǎng)關只要有驗證密鑰就能核驗并信任。
但請記?。篔WT 只保證 完整性(沒被篡改),默認不保密(除非用 JWE 加密)。敏感信息不要塞進未加密的 JWT。
二、JWT 的結構與簽名流程
JWT 的字符串形如:<header>.<payload>.<signature>
Header(JSON,Base64URL)
alg
: 簽名算法(如HS256
/RS256
/ES256
/EdDSA
)typ
: 通常為"JWT"
kid
(可選):密鑰標識,用于密鑰輪換。
Payload/Claims(JSON,Base64URL)
常見注冊聲明:
iss
(頒發(fā)者)、sub
(主體,通常是用戶ID)、aud
(受眾)exp
(過期時間,秒級時間戳,必須?。?、nbf
(不早于)、iat
(簽發(fā)時間)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 使用流程(最小閉環(huán))
- 登錄:用戶名密碼校驗成功 → 頒發(fā)短期 Access Token(JWT)+ 較長期 Refresh Token(不可見給前端或放 HttpOnly Cookie)。
- 訪問 API:前端將
Authorization: Bearer <jwt>
送給后端。 - 后端:驗證簽名、校驗
exp/nbf/aud/iss
等 → 放行。 - 刷新:Access Token 過期,用 Refresh Token 換新(做輪換與失效控制)。
- 登出/撤銷(可選):把
jti
或 refresh 的標識加入黑名單,或進行密鑰輪換。
四、常見安全陷阱(一定要看)
- ? 不設置
exp
(永不過期,風險極大)。 - ?
alg: none
(嚴格禁用)。 - ? 密鑰混淆:把對稱密鑰誤當作公鑰發(fā)布;或同一
kid
指向錯密鑰。 - ? HS256 在多服務擴散:一旦泄露,所有服務都可偽造??绶战ㄗh
RS256/ES256/EdDSA
(私鑰簽、公鑰驗)。 - ? 不校驗
aud/iss
:導致“錯配 token”被誤信任。 - ? 客戶端 localStorage 存儲 → 易受 XSS 影響。推薦 HttpOnly + Secure + SameSite Cookie。
- ? 忽視 CSRF:若用 Cookie 攜帶 Access Token,要配合 SameSite + CSRF 令牌 或改為 Bearer 頭。
- ? 不做 Refresh Token 輪換 與黑名單 → 被盜后長期可用。
- ? 把敏感信息(如身份證、銀行卡、密碼)塞進未加密 JWT。
五、Java 手寫(JJWT)創(chuàng)建與驗證
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) 頒發(fā) 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
)。
在網(wǎng)關/過濾器中統(tǒng)一捕獲 → 返回 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"; } }
七、發(fā)行端:用 Spring Authorization Server 簽發(fā) JWT(概念位)
如果你既要頒發(fā)又要驗證:
- 引入
spring-authorization-server
,配置客戶端、用戶認證、簽名密鑰(支持 RSA/ECDSA/EdDSA),開啟 /.well-known/jwks.json。 - 認證成功后,框架自動頒發(fā) Access Token(JWT) 與 Refresh Token。
- 資源服務器只需
issuer-uri
/jwk-set-uri
即可對接。
好處:密鑰管理、輪換、標準化授權流程(OAuth2/OIDC) 都交給框架;你專注業(yè)務。
八、Cookie vs Header、CSRF 與前端存儲
推薦:Authorization: Bearer <jwt>
置于請求頭,前端保存在內存(刷新丟失)或安全容器;刷新策略依賴 HttpOnly Refresh Cookie。
若必須把 Access Token 放 Cookie:
- 設置:
HttpOnly + Secure + SameSite=Lax/Strict
; - 開啟并正確處理 CSRF 防護(基于 Cookie 的雙重提交策略或框架自帶 CSRF Token)。
不要放 localStorage(XSS 風險大)。
九、刷新與撤銷(實戰(zhàn)策略)
短期 Access Token(5–15 分鐘) + 長期 Refresh Token(7–30 天)。
Refresh Token 輪換:每次刷新都頒發(fā)新 refresh,并使舊的失效(存庫并維護 revoked
標記或版本號)。
黑名單/撤銷:
- 記錄 Access Token 的
jti
(可選)用于緊急撤銷; - 更常用的是縮短 Access Token壽命 + 輪換 Refresh;
- 密鑰輪換:更換私鑰(新
kid
),強制舊 token 逐步失效(需兼容一段時間,等舊 token 過期)。
十、微服務與網(wǎng)關
- 首選:網(wǎng)關或每個服務自行校驗 JWT(拿到 JWKS 公鑰本地驗);不要把解析結果當作“可信 JSON”直接傳遞。
- aud/iss:為不同受眾(微服務)使用不同
aud
,防止“錯用 token”。 - 性能:緩存 JWKS、公鑰對象;JWT 驗證成本很低,通常不是瓶頸。
十一、完整示例:無授權服務器時的“輕量頒發(fā) + 校驗”
1) 頒發(fā)端(登錄成功后)
// 假設你用 Spring Security 自己做用戶名/密碼認證 @PostMapping("/auth/login") public Map<String, String> login(@RequestBody LoginReq req) { // 1. 校驗用戶名密碼(略) // 2. 頒發(fā) 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(); // 存入數(shù)據(jù)庫,標記有效 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. 頒發(fā)新 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) 做跨服務驗證;對稱密鑰僅限單體/網(wǎng)關內部。
- ? 開啟并演練 密鑰輪換(
kid
+ JWKS),舊公鑰保留到所有 token 過期。 - ? 訪問控制基于 最小權限(角色/權限來源可在 claims 或 DB)。
- ? 選擇合適的 存儲與傳輸方式(Bearer 頭 或 HttpOnly Cookie + CSRF 防護)。
- ? 短期 Access + 輪換 Refresh;可選黑名單(
jti
)應急撤銷。 - ? 日志中絕不打印完整 token(最多打前后各 6 位用于排錯)。
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
一次由Lombok的@AllArgsConstructor注解引發(fā)的錯誤及解決
這篇文章主要介紹了一次由Lombok的@AllArgsConstructor注解引發(fā)的錯誤及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09淺談Java8 的foreach跳出循環(huán)break/return
這篇文章主要介紹了Java8 的foreach跳出循環(huán)break/return,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07springboot jackson自定義序列化和反序列化實例
這篇文章主要介紹了spring boot jackson自定義序列化和反序列化實例,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10