jwt原理及Java中實(shí)現(xiàn)過(guò)程
一、JWT 是什么?解決什么問(wèn)題?
- 我們先來(lái)一張圖看一下這個(gè)過(guò)程:

JWT(JSON Web Token)是一種把“認(rèn)證信息(Claims)+ 完整性校驗(yàn)”打包成 自包含 的字符串的規(guī)范。
它主要用于無(wú)狀態(tài)認(rèn)證:服務(wù)端驗(yàn)證簽名即可信任其中的身份與權(quán)限,無(wú)需每次查庫(kù)或維護(hù)會(huì)話(huà)(session)。
- 無(wú)狀態(tài):后端不存會(huì)話(huà);減少分布式共享狀態(tài)的復(fù)雜度。
- 可擴(kuò)展:把自定義字段寫(xiě)入 claims(如角色、租戶(hù)、權(quán)限)。
- 可委托:不同服務(wù)/網(wǎng)關(guān)只要有驗(yàn)證密鑰就能核驗(yàn)并信任。
但請(qǐng)記?。篔WT 只保證 完整性(沒(méi)被篡改),默認(rèn)不保密(除非用 JWE 加密)。敏感信息不要塞進(jìn)未加密的 JWT。
二、JWT 的結(jié)構(gòu)與簽名流程
JWT 的字符串形如:<header>.<payload>.<signature>
Header(JSON,Base64URL)
alg: 簽名算法(如HS256/RS256/ES256/EdDSA)typ: 通常為"JWT"kid(可選):密鑰標(biāo)識(shí),用于密鑰輪換。
Payload/Claims(JSON,Base64URL)
常見(jiàn)注冊(cè)聲明:
iss(頒發(fā)者)、sub(主體,通常是用戶(hù)ID)、aud(受眾)exp(過(guò)期時(shí)間,秒級(jí)時(shí)間戳,必須?。?code>nbf(不早于)、iat(簽發(fā)時(shí)間)jti(唯一ID,用于一次性/黑名單)
以及你的自定義字段:roles、tenantId、scope等。
Signature
- 計(jì)算方式:
signature = Sign( base64url(header) + "." + base64url(payload), key, alg ) - 驗(yàn)證時(shí):使用共享密鑰(HMAC)或公鑰(RSA/ECDSA/EdDSA)驗(yàn)證。
JWS vs JWE:
- JWS(最常用):簽名但不加密;任何人拿到 token 都能看到 payload。
- JWE:加密(可選),適用于含敏感信息的場(chǎng)景。
三、JWT 使用流程(最小閉環(huán))
- 登錄:用戶(hù)名密碼校驗(yàn)成功 → 頒發(fā)短期 Access Token(JWT)+ 較長(zhǎng)期 Refresh Token(不可見(jiàn)給前端或放 HttpOnly Cookie)。
- 訪(fǎng)問(wèn) API:前端將
Authorization: Bearer <jwt>送給后端。 - 后端:驗(yàn)證簽名、校驗(yàn)
exp/nbf/aud/iss等 → 放行。 - 刷新:Access Token 過(guò)期,用 Refresh Token 換新(做輪換與失效控制)。
- 登出/撤銷(xiāo)(可選):把
jti或 refresh 的標(biāo)識(shí)加入黑名單,或進(jìn)行密鑰輪換。
四、常見(jiàn)安全陷阱(一定要看)
- ? 不設(shè)置
exp(永不過(guò)期,風(fēng)險(xiǎn)極大)。 - ?
alg: none(嚴(yán)格禁用)。 - ? 密鑰混淆:把對(duì)稱(chēng)密鑰誤當(dāng)作公鑰發(fā)布;或同一
kid指向錯(cuò)密鑰。 - ? HS256 在多服務(wù)擴(kuò)散:一旦泄露,所有服務(wù)都可偽造。跨服務(wù)建議
RS256/ES256/EdDSA(私鑰簽、公鑰驗(yàn))。 - ? 不校驗(yàn)
aud/iss:導(dǎo)致“錯(cuò)配 token”被誤信任。 - ? 客戶(hù)端 localStorage 存儲(chǔ) → 易受 XSS 影響。推薦 HttpOnly + Secure + SameSite Cookie。
- ? 忽視 CSRF:若用 Cookie 攜帶 Access Token,要配合 SameSite + CSRF 令牌 或改為 Bearer 頭。
- ? 不做 Refresh Token 輪換 與黑名單 → 被盜后長(zhǎng)期可用。
- ? 把敏感信息(如身份證、銀行卡、密碼)塞進(jìn)未加密 JWT。
五、Java 手寫(xiě)(JJWT)創(chuàng)建與驗(yàn)證
1) 依賴(lài)(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:還需對(duì)應(yīng)的 jjwt-xxx 或者用 java.security 生成密鑰。
2) 生成密鑰(示例:RSA 與 Ed25519)
// RSA 2048(推薦生產(chǎn)至少 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()) // 默認(rèn)按密鑰類(lèi)型選擇 RS256/ES256/EdDSA
.compact();
signWith(PrivateKey):JJWT 會(huì)自動(dòng)選合適 alg;如想強(qiáng)制算法,可用新版簽名 API 指定 SignatureAlgorithm.
4) 驗(yàn)證 JWT(公鑰驗(yàn)簽 + 校驗(yàn)聲明)
import io.jsonwebtoken.*;
Jws<Claims> jws = Jwts.parserBuilder()
.requireIssuer("https://auth.example.com")
.requireAudience("api.example.com")
.setAllowedClockSkewSeconds(60) // 允許 60s 時(shí)鐘偏差
.setSigningKey(rsaKeyPair.getPublic()) // 或使用 JWKS 拉取的公鑰
.build()
.parseClaimsJws(token);
Claims claims = jws.getBody();
String userId = claims.getSubject();
String[] roles = claims.get("roles", String[].class);
校驗(yàn)失敗會(huì)拋異常(如 ExpiredJwtException、SignatureException)。
在網(wǎng)關(guān)/過(guò)濾器中統(tǒng)一捕獲 → 返回 401/403。
六、Spring Boot(Resource Server)零膠水校驗(yàn)
最省心的是讓 Spring Security 資源服務(wù)器替你做解析與校驗(yàn),它支持 JWK 集合(JWKS) 自動(dòng)遠(yuǎn)程拉取公鑰。
1) 依賴(lài)
<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(通過(guò) JWKS URL 校驗(yàn))
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 # 建議同時(shí)配置,做 iss 校驗(yàn)
你的授權(quán)服務(wù)器(自建或第三方,如 Auth0/Keycloak/Spring Authorization Server)對(duì)外暴露 JWKS。資源服自動(dòng)緩存和輪詢(xún)按 kid 取公鑰。
3) 安全配置
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 如果前端走 Bearer 頭,可關(guān);若走 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 映射為權(quán)限
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ā)又要驗(yàn)證:
- 引入
spring-authorization-server,配置客戶(hù)端、用戶(hù)認(rèn)證、簽名密鑰(支持 RSA/ECDSA/EdDSA),開(kāi)啟 /.well-known/jwks.json。 - 認(rèn)證成功后,框架自動(dòng)頒發(fā) Access Token(JWT) 與 Refresh Token。
- 資源服務(wù)器只需
issuer-uri/jwk-set-uri即可對(duì)接。
好處:密鑰管理、輪換、標(biāo)準(zhǔn)化授權(quán)流程(OAuth2/OIDC) 都交給框架;你專(zhuān)注業(yè)務(wù)。
八、Cookie vs Header、CSRF 與前端存儲(chǔ)
推薦:Authorization: Bearer <jwt> 置于請(qǐng)求頭,前端保存在內(nèi)存(刷新丟失)或安全容器;刷新策略依賴(lài) HttpOnly Refresh Cookie。
若必須把 Access Token 放 Cookie:
- 設(shè)置:
HttpOnly + Secure + SameSite=Lax/Strict; - 開(kāi)啟并正確處理 CSRF 防護(hù)(基于 Cookie 的雙重提交策略或框架自帶 CSRF Token)。
不要放 localStorage(XSS 風(fēng)險(xiǎn)大)。
九、刷新與撤銷(xiāo)(實(shí)戰(zhàn)策略)
短期 Access Token(5–15 分鐘) + 長(zhǎng)期 Refresh Token(7–30 天)。
Refresh Token 輪換:每次刷新都頒發(fā)新 refresh,并使舊的失效(存庫(kù)并維護(hù) revoked 標(biāo)記或版本號(hào))。
黑名單/撤銷(xiāo):
- 記錄 Access Token 的
jti(可選)用于緊急撤銷(xiāo); - 更常用的是縮短 Access Token壽命 + 輪換 Refresh;
- 密鑰輪換:更換私鑰(新
kid),強(qiáng)制舊 token 逐步失效(需兼容一段時(shí)間,等舊 token 過(guò)期)。
十、微服務(wù)與網(wǎng)關(guān)
- 首選:網(wǎng)關(guān)或每個(gè)服務(wù)自行校驗(yàn) JWT(拿到 JWKS 公鑰本地驗(yàn));不要把解析結(jié)果當(dāng)作“可信 JSON”直接傳遞。
- aud/iss:為不同受眾(微服務(wù))使用不同
aud,防止“錯(cuò)用 token”。 - 性能:緩存 JWKS、公鑰對(duì)象;JWT 驗(yàn)證成本很低,通常不是瓶頸。
十一、完整示例:無(wú)授權(quán)服務(wù)器時(shí)的“輕量頒發(fā) + 校驗(yàn)”
1) 頒發(fā)端(登錄成功后)
// 假設(shè)你用 Spring Security 自己做用戶(hù)名/密碼認(rèn)證
@PostMapping("/auth/login")
public Map<String, String> login(@RequestBody LoginReq req) {
// 1. 校驗(yàn)用戶(hù)名密碼(略)
// 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ù)庫(kù),標(biāo)記有效
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) 刷新端點(diǎn)
@PostMapping("/auth/refresh")
public Map<String, String> refresh(@CookieValue("refresh_token") String refreshToken) {
// 1. 驗(yàn)證 refreshToken 簽名與過(guò)期
Jws<Claims> jws = Jwts.parserBuilder()
.setSigningKey(rsaPublicKey)
.build()
.parseClaimsJws(refreshToken);
String jti = jws.getBody().getId();
// 2. 校驗(yàn) jti 是否未吊銷(xiāo),且未被使用(輪換)
// 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) 資源服務(wù)(校驗(yàn)端,若不用 Resource Server Starter)
自定義過(guò)濾器(不建議重復(fù)造輪子,演示用):
@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);
}
}
十二、測(cè)試要點(diǎn)清單(上線(xiàn)前自查)
- ?
exp/nbf/iat/iss/aud均有嚴(yán)格校驗(yàn);允許小量 clock skew。 - ? 禁止
alg: none,不允許客戶(hù)端指定算法。 - ? 使用 非對(duì)稱(chēng)算法(RS/ES/EdDSA) 做跨服務(wù)驗(yàn)證;對(duì)稱(chēng)密鑰僅限單體/網(wǎng)關(guān)內(nèi)部。
- ? 開(kāi)啟并演練 密鑰輪換(
kid+ JWKS),舊公鑰保留到所有 token 過(guò)期。 - ? 訪(fǎng)問(wèn)控制基于 最小權(quán)限(角色/權(quán)限來(lái)源可在 claims 或 DB)。
- ? 選擇合適的 存儲(chǔ)與傳輸方式(Bearer 頭 或 HttpOnly Cookie + CSRF 防護(hù))。
- ? 短期 Access + 輪換 Refresh;可選黑名單(
jti)應(yīng)急撤銷(xiāo)。 - ? 日志中絕不打印完整 token(最多打前后各 6 位用于排錯(cuò))。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
springBoot解決static和@Component遇到的bug
這篇文章主要介紹了springBoot解決static和@Component遇到的bug,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
SpringBoot統(tǒng)一數(shù)據(jù)返回的幾種方式
在Web應(yīng)用程序開(kāi)發(fā)中,統(tǒng)一數(shù)據(jù)返回格式對(duì)于前后端分離項(xiàng)目尤為重要,本文就來(lái)介紹一下SpringBoot統(tǒng)一數(shù)據(jù)返回的幾種方式,具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07
springboot后端接收前端傳數(shù)組參數(shù)三種方法
這篇文章主要給大家介紹了關(guān)于springboot后端接收前端傳數(shù)組參數(shù)三種方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-07-07
logback整合rabbitmq實(shí)現(xiàn)消息記錄日志的配置
這篇文章主要介紹了logback整合rabbitmq實(shí)現(xiàn)消息記錄日志的配置,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-12-12
Java基礎(chǔ)教程之類(lèi)型轉(zhuǎn)換與多態(tài)
這篇文章主要介紹了Java基礎(chǔ)教程之類(lèi)型轉(zhuǎn)換與多態(tài),本文講解了 基本類(lèi)型轉(zhuǎn)換、 upcast與多態(tài)、 Object類(lèi)等內(nèi)容,需要的朋友可以參考下2014-09-09
SpringBoot淺析Redis訪(fǎng)問(wèn)操作使用
Redis是一個(gè)速度非??斓姆顷P(guān)系數(shù)據(jù)庫(kù)(Non-Relational?Database),它可以存儲(chǔ)鍵(Key)與多種不同類(lèi)型的值(Value)之間的映射(Mapping),可以將存儲(chǔ)在內(nèi)存的鍵值對(duì)數(shù)據(jù)持久化到硬盤(pán),可以使用復(fù)制特性來(lái)擴(kuò)展讀性能,還可以使用客戶(hù)端分片來(lái)擴(kuò)展寫(xiě)性能2022-11-11
SpringBoot對(duì)接Spark過(guò)程詳解
這篇文章主要介紹SpringBoot接入Spark的方法的相關(guān)知識(shí),小編通過(guò)實(shí)際案例向大家展示操作過(guò)程,操作方法簡(jiǎn)單快捷,實(shí)用性強(qiáng),希望能幫助大家解決問(wèn)題2023-02-02

