您好,登錄后才能下訂單哦!
這篇文章主要講解了“JWT的實(shí)現(xiàn)原理和基本使用方法”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“JWT的實(shí)現(xiàn)原理和基本使用方法”吧!
很可惜維基百科上沒有搜索到JWT
的條目,但是從jwt.io
的首頁展示圖中,可以看到描述:
?JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties
?
從這段文字中可以提取到JWT
的規(guī)范文件RFC 7519
,里面有詳細(xì)地介紹JWT
的基本概念,Claims
的含義、布局和算法實(shí)現(xiàn)等,下面逐個(gè)展開擊破。
JWT
全稱是JSON Web Token
,如果從字面上理解感覺是基于JSON
格式用于網(wǎng)絡(luò)傳輸?shù)牧钆啤?shí)際上,JWT
是一種緊湊的Claims
聲明格式,旨在用于空間受限的環(huán)境進(jìn)行傳輸,常見的場景如HTTP
授權(quán)請求頭參數(shù)和URI
查詢參數(shù)。JWT
會(huì)把Claims
轉(zhuǎn)換成JSON
格式,而這個(gè)JSON
內(nèi)容將會(huì)應(yīng)用為JWS
結(jié)構(gòu)的有效載荷或者應(yīng)用為JWE
結(jié)構(gòu)的(加密處理后的)原始字符串,通過消息認(rèn)證碼(Message Authentication Code
或者簡稱MAC
)和/或者加密操作對Claims
進(jìn)行數(shù)字簽名或者完整性保護(hù)。
這里有三個(gè)概念在其他規(guī)范文件中,簡單提一下:
JWE
(規(guī)范文件
RFC 7516
):
JSON Web Encryption
,表示基于
JSON
數(shù)據(jù)結(jié)構(gòu)的加密內(nèi)容,加密機(jī)制對任意八位字節(jié)序列進(jìn)行加密、提供完整性保護(hù)和提高破解難度,
JWE
中的緊湊序列化布局如下BASE64URL(UTF8(JWE Protected Header)) || '.' ||
BASE64URL(JWE Encrypted Key) || '.' ||
BASE64URL(JWE Initialization Vector) || '.' ||
BASE64URL(JWE Ciphertext) || '.' ||
BASE64URL(JWE Authentication Tag)
JWS
(規(guī)范文件
RFC 7515
):
JSON Web Signature
,表示使用
JSON
數(shù)據(jù)結(jié)構(gòu)和
BASE64URL
編碼表示經(jīng)過數(shù)字簽名或消息認(rèn)證碼(
MAC
)認(rèn)證的內(nèi)容,數(shù)字簽名或者
MAC
能夠提供完整性保護(hù),
JWS
中的緊湊序列化布局如下:ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' ||
BASE64URL(JWS Payload)) || '.' ||
BASE64URL(JWS Signature)
JWA
(規(guī)范文件
RFC 7518
):
JSON Web Algorithm
,
JSON Web
算法,數(shù)字簽名或者
MAC
算法,應(yīng)用于
JWS
的可用算法列表如下:總的來說,JWT
其實(shí)有兩種實(shí)現(xiàn),基于JWE
實(shí)現(xiàn)的依賴于加解密算法、BASE64URL
編碼和身份認(rèn)證等手段提高傳輸?shù)?code>Claims的被破解難度,而基于JWS
的實(shí)現(xiàn)使用了BASE64URL
編碼和數(shù)字簽名的方式對傳輸?shù)?code>Claims提供了完整性保護(hù),也就是僅僅保證傳輸?shù)?code>Claims內(nèi)容不被篡改,但是會(huì)暴露明文。「目前主流的JWT
框架中大部分都沒有實(shí)現(xiàn)JWE
,所以下文主要通過JWS
的實(shí)現(xiàn)方式進(jìn)行深入探討」。
Claim
有索賠、聲稱、要求或者權(quán)利要求的含義,但是筆者覺得任一個(gè)翻譯都不怎么合乎語義,這里保留Claim
關(guān)鍵字直接作為命名。JWT
的核心作用就是保護(hù)Claims
的完整性(或者數(shù)據(jù)加密),保證JWT
傳輸?shù)倪^程中Claims
不被篡改(或者不被破解)。Claims
在JWT
原始內(nèi)容中是一個(gè)JSON
格式的字符串,其中單個(gè)Claim
是K-V
結(jié)構(gòu),作為JsonNode
中的一個(gè)field-value
,這里列出常用的規(guī)范中預(yù)定義好的Claim
:
簡稱 | 全稱 | 含義 |
---|---|---|
iss | Issuer | 發(fā)行方 |
sub | Subject | 主體 |
aud | Audience | (接收)目標(biāo)方 |
exp | Expiration Time | 過期時(shí)間 |
nbf | Not Before | 早于該定義的時(shí)間的JWT 不能被接受處理 |
iat | Issued At | JWT 發(fā)行時(shí)的時(shí)間戳 |
jti | JWT ID | JWT 的唯一標(biāo)識(shí) |
這些預(yù)定義的Claim
并不要求強(qiáng)制使用,何時(shí)選用何種Claim
完全由使用者決定,而為了使JWT
更加緊湊,這些Claim
都使用了簡短的命名方式去定義。在不和內(nèi)建的Claim
沖突的前提下,使用者可以自定義新的公共Claim
,如:
簡稱 | 全稱 | 含義 |
---|---|---|
cid | Customer ID | 客戶ID |
rid | Role ID | 角色I(xiàn)D |
一定要注意,在JWS
實(shí)現(xiàn)中,Claims
會(huì)作為payload
部分進(jìn)行BASE64
編碼,明文會(huì)直接暴露,敏感信息一般不應(yīng)該設(shè)計(jì)為一個(gè)自定義Claim
。
在JWT
規(guī)范文件中稱這些Header
為JOSE Header
,JOSE
的全稱為Javascript Object Signature Encryption
,也就是Javascript
對象簽名和加密框架,JOSE Header
其實(shí)就是Javascript
對象簽名和加密的頭部參數(shù)。「下面列舉一下JWS
中常用的Header
」:
簡稱 | 全稱 | 含義 |
---|---|---|
alg | Algorithm | 用于保護(hù)JWS 的加解密算法 |
jku | JWK Set URL | 一組JSON 編碼的公共密鑰的URL ,其中一個(gè)是用于對JWS 進(jìn)行數(shù)字簽名的密鑰 |
jwk | JSON Web Key | 用于對JWS 進(jìn)行數(shù)字簽名的密鑰相對應(yīng)的公共密鑰 |
kid | Key ID | 用于保護(hù)JWS 進(jìn)的密鑰 |
x5u | X.509 URL | X.509 相關(guān) |
x5c | X.509 Certificate Chain | X.509 相關(guān) |
x5t | X.509 Certificate SHA-1 Thumbprin | X.509 相關(guān) |
x5t#S256 | X.509 Certificate SHA-256 Thumbprint | X.509 相關(guān) |
typ | Type | 類型,例如JWT 、JWS 或者JWE 等等 |
cty | Content Type | 內(nèi)容類型,決定payload 部分的MediaType |
最常見的兩個(gè)Header
就是alg
和typ
,例如:
{
"alg": "HS256",
"typ": "JWT"
}
主要介紹JWS
的布局,前面已經(jīng)提到過,JWS
的「緊湊布局」如下:
ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' ||
BASE64URL(JWS Payload)) || '.' ||
BASE64URL(JWS Signature)
其實(shí)還有「非緊湊布局」,會(huì)通過一個(gè)JSON
結(jié)構(gòu)完整地展示Header
參數(shù)、Claims
和分組簽名:
{
"payload":"<payload contents>",
"signatures":[
{"protected":"<integrity-protected header 1 contents>",
"header":<non-integrity-protected header 1 contents>,
"signature":"<signature 1 contents>"},
...
{"protected":"<integrity-protected header N contents>",
"header":<non-integrity-protected header N contents>,
"signature":"<signature N contents>"}]
}
非緊湊布局還有一個(gè)扁平化的表示形式:
{
"payload":"<payload contents>",
"protected":"<integrity-protected header contents>",
"header":<non-integrity-protected header contents>,
"signature":"<signature contents>"
}
其中Header
參數(shù)部分可以參看上一小節(jié),而簽名部分可以參看下一小節(jié),剩下簡單提一下payload
部分,payload
(有效載荷)其實(shí)就是完整的Claims
,假設(shè)Claims
的JSON
形式是:
{
"iss": "throwx",
"jid": 1
}
那么扁平化非緊湊格式下的payload
節(jié)點(diǎn)就是:
{
......
"payload": {
"iss": "throwx",
"jid": 1
}
......
}
JWS
簽名生成依賴于散列或者加解密算法,可以使用的算法見前面貼出的圖,例如HS256
,具體是HMAC SHA-256
,也就是通過散列算法SHA-256
對于編碼后的Header
和Claims
字符串進(jìn)行一次散列計(jì)算,簽名生成的偽代碼如下:
## 不進(jìn)行編碼
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
256 bit secret key
)
## 進(jìn)行編碼
base64UrlEncode(
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload)
[256 bit secret key])
)
其他算法的操作基本相似,生成好的簽名直接加上一個(gè)前置的.
拼接在base64UrlEncode(header).base64UrlEncode(payload)
之后就生成完整的JWS
。
前面已經(jīng)分析過JWT
的一些基本概念、布局和簽名算法,這里根據(jù)前面的理論進(jìn)行JWT
的生成、解析和校驗(yàn)操作。先引入common-codec
庫簡化一些編碼和加解密操作,引入一個(gè)主流的JSON
框架做序列化和反序列化:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.0</version>
</dependency>
為了簡單起見,Header
參數(shù)寫死為:
{
"alg": "HS256",
"typ": "JWT"
}
使用的簽名算法是HMAC SHA-256
,輸入的加密密鑰長度必須為256 bit
(如果單純用英文和數(shù)字組成的字符,要32
個(gè)字符),這里為了簡單起見,用00000000111111112222222233333333
作為KEY
。定義Claims
部分如下:
{
"iss": "throwx",
"jid": 10087, # <---- 這里有個(gè)筆誤,本來打算寫成jti,后來發(fā)現(xiàn)寫錯(cuò)了,不打算改
"exp": 1613227468168 # 20210213
}
生成JWT
的代碼如下:
@Slf4j
public class JsonWebToken {
private static final String KEY = "00000000111111112222222233333333";
private static final String DOT = ".";
private static final Map<String, String> HEADERS = new HashMap<>(8);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
HEADERS.put("alg", "HS256");
HEADERS.put("typ", "JWT");
}
String generateHeaderPart() throws JsonProcessingException {
byte[] headerBytes = OBJECT_MAPPER.writeValueAsBytes(HEADERS);
String headerPart = new String(Base64.encodeBase64(headerBytes,false ,true), StandardCharsets.US_ASCII);
log.info("生成的Header部分為:{}", headerPart);
return headerPart;
}
String generatePayloadPart(Map<String, Object> claims) throws JsonProcessingException {
byte[] payloadBytes = OBJECT_MAPPER.writeValueAsBytes(claims);
String payloadPart = new String(Base64.encodeBase64(payloadBytes,false ,true), StandardCharsets.UTF_8);
log.info("生成的Payload部分為:{}", payloadPart);
return payloadPart;
}
String generateSignaturePart(String headerPart, String payloadPart) {
String content = headerPart + DOT + payloadPart;
Mac mac = HmacUtils.getInitializedMac(HmacAlgorithms.HMAC_SHA_256, KEY.getBytes(StandardCharsets.UTF_8));
byte[] output = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
String signaturePart = new String(Base64.encodeBase64(output, false ,true), StandardCharsets.UTF_8);
log.info("生成的Signature部分為:{}", signaturePart);
return signaturePart;
}
public String generate(Map<String, Object> claims) throws Exception {
String headerPart = generateHeaderPart();
String payloadPart = generatePayloadPart(claims);
String signaturePart = generateSignaturePart(headerPart, payloadPart);
String jws = headerPart + DOT + payloadPart + DOT + signaturePart;
log.info("生成的JWT為:{}", jws);
return jws;
}
public static void main(String[] args) throws Exception {
Map<String, Object> claims = new HashMap<>(8);
claims.put("iss", "throwx");
claims.put("jid", 10087L);
claims.put("exp", 1613227468168L);
JsonWebToken jsonWebToken = new JsonWebToken();
System.out.println("自行生成的JWT:" + jsonWebToken.generate(claims));
}
}
執(zhí)行輸出日志如下:
23:37:48.743 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Header部分為:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
23:37:48.747 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Payload部分為:eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9
23:37:48.748 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Signature部分為:7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
23:37:48.749 [main] INFO club.throwable.jwt.JsonWebToken - 生成的JWT為:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
自行生成的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
可以在jwt.io
上驗(yàn)證一下:
解析JWT
的過程是構(gòu)造JWT
的逆向過程,首先基于點(diǎn)號.
分三段,然后分別進(jìn)行BASE64
解碼,然后得到三部分的明文,頭部參數(shù)和有效載荷需要做一次JSON
反序列化即可還原各個(gè)部分的JSON
結(jié)構(gòu):
public Map<Part, PartContent> parse(String jwt) throws Exception {
System.out.println("當(dāng)前解析的JWT:" + jwt);
Map<Part, PartContent> result = new HashMap<>(8);
// 這里暫且認(rèn)為所有的輸入JWT的格式都是合法的
StringTokenizer tokenizer = new StringTokenizer(jwt, DOT);
String[] jwtParts = new String[3];
int idx = 0;
while (tokenizer.hasMoreElements()) {
jwtParts[idx] = tokenizer.nextToken();
idx++;
}
String headerPart = jwtParts[0];
PartContent headerContent = new PartContent();
headerContent.setRawContent(headerPart);
headerContent.setPart(Part.HEADER);
headerPart = new String(Base64.decodeBase64(headerPart), StandardCharsets.UTF_8);
headerContent.setPairs(OBJECT_MAPPER.readValue(headerPart, new TypeReference<Map<String, Object>>() {
}));
result.put(Part.HEADER, headerContent);
String payloadPart = jwtParts[1];
PartContent payloadContent = new PartContent();
payloadContent.setRawContent(payloadPart);
payloadContent.setPart(Part.PAYLOAD);
payloadPart = new String(Base64.decodeBase64(payloadPart), StandardCharsets.UTF_8);
payloadContent.setPairs(OBJECT_MAPPER.readValue(payloadPart, new TypeReference<Map<String, Object>>() {
}));
result.put(Part.PAYLOAD, payloadContent);
String signaturePart = jwtParts[2];
PartContent signatureContent = new PartContent();
signatureContent.setRawContent(signaturePart);
signatureContent.setPart(Part.SIGNATURE);
result.put(Part.SIGNATURE, signatureContent);
return result;
}
enum Part {
HEADER,
PAYLOAD,
SIGNATURE
}
@Data
public static class PartContent {
private Part part;
private String rawContent;
private Map<String, Object> pairs;
}
這里嘗試用之前生產(chǎn)的JWT
進(jìn)行解析:
public static void main(String[] args) throws Exception {
JsonWebToken jsonWebToken = new JsonWebToken();
String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs";
Map<Part, PartContent> parseResult = jsonWebToken.parse(jwt);
System.out.printf("解析結(jié)果如下:\nHEADER:%s\nPAYLOAD:%s\nSIGNATURE:%s%n",
parseResult.get(Part.HEADER),
parseResult.get(Part.PAYLOAD),
parseResult.get(Part.SIGNATURE)
);
}
解析結(jié)果如下:
當(dāng)前解析的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
解析結(jié)果如下:
HEADER:PartContent(part=HEADER, rawContent=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9, pairs={typ=JWT, alg=HS256})
PAYLOAD:PartContent(part=PAYLOAD, rawContent=eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9, pairs={iss=throwx, jid=10087, exp=1613227468168})
SIGNATURE:PartContent(part=SIGNATURE, rawContent=7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs, pairs=null)
驗(yàn)證JWT
建立在解析JWT
完成的基礎(chǔ)之上,需要對解析出來的頭部參數(shù)和有效載做一次MAC
簽名,與解析出來的簽名做校對。另外,可以自定義校驗(yàn)具體的Claim
項(xiàng),如過期時(shí)間和發(fā)行者等。一般校驗(yàn)失敗會(huì)針對不同的情況定制不同的運(yùn)行時(shí)異常便于區(qū)分場景,這里為了方便統(tǒng)一拋出IllegalStateException
:
public void verify(String jwt) throws Exception {
System.out.println("當(dāng)前校驗(yàn)的JWT:" + jwt);
Map<Part, PartContent> parseResult = parse(jwt);
PartContent headerContent = parseResult.get(Part.HEADER);
PartContent payloadContent = parseResult.get(Part.PAYLOAD);
PartContent signatureContent = parseResult.get(Part.SIGNATURE);
String signature = generateSignaturePart(headerContent.getRawContent(), payloadContent.getRawContent());
if (!Objects.equals(signature, signatureContent.getRawContent())) {
throw new IllegalStateException("簽名校驗(yàn)異常");
}
String iss = payloadContent.getPairs().get("iss").toString();
// iss校驗(yàn)
if (!Objects.equals(iss, "throwx")) {
throw new IllegalStateException("ISS校驗(yàn)異常");
}
long exp = Long.parseLong(payloadContent.getPairs().get("exp").toString());
// exp校驗(yàn),有效期14天
if (System.currentTimeMillis() - exp > 24 * 3600 * 1000 * 14) {
throw new IllegalStateException("exp校驗(yàn)異常,JWT已經(jīng)過期");
}
// 省略其他校驗(yàn)項(xiàng)
System.out.println("JWT校驗(yàn)通過");
}
類似地,用上面生成過的JWT
進(jìn)行驗(yàn)證,結(jié)果如下:
當(dāng)前校驗(yàn)的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
當(dāng)前解析的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
23:33:00.174 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Signature部分為:7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
JWT校驗(yàn)通過
「上面的代碼存在硬編碼問題,只是為了用最簡單的JWS
實(shí)現(xiàn)方式重新實(shí)現(xiàn)了JWT
的生成、解析和校驗(yàn)過程」,算法也使用了復(fù)雜程度和安全性極低的HS256
,所以在生產(chǎn)中并不推薦花大量時(shí)間去實(shí)現(xiàn)JWS
,可以選用現(xiàn)成的JWT
類庫,如auth0
和jjwt
。
JWT
本質(zhì)是一個(gè)令牌,更多場景下是作為會(huì)話ID
(session_id
)使用,作用是'維持會(huì)話的粘性'
和攜帶認(rèn)證信息(如果用JWT
術(shù)語,應(yīng)該是安全地傳遞Claims
)。筆者記得很久以前使用的一種Session ID
解決方案是由服務(wù)端生成和持久化Session ID
,返回的Session ID
需要寫入用戶的Cookie
,然后用戶每次請求必須攜帶Cookie
,Session ID
會(huì)映射用戶的一些認(rèn)證信息,這一切都是由服務(wù)端管理,一個(gè)很常見的例子就是Tomcat
容器中出現(xiàn)的J(ava)SESSIONID
。與之前的方案不同,JWT
是一種無狀態(tài)的令牌,它并不需要由服務(wù)端保存,攜帶的數(shù)據(jù)或者會(huì)話的數(shù)據(jù)都不需要持久化,使用JWT
只需要關(guān)注Claims
的完整性和合法性即可,生成JWT
時(shí)候所有有效數(shù)據(jù)已經(jīng)通過編碼存儲(chǔ)在JWT
字符串中。正因JWT
是無狀態(tài)的,一旦頒發(fā)后得到JWT
的客戶端都可以通過它與服務(wù)端交互,JWT
一旦泄露有可能造成嚴(yán)重安全問題,因此實(shí)踐的時(shí)候一般需要做幾點(diǎn):
JWT
需要設(shè)置有效期,也就是
exp
這個(gè)
Claim
必須啟用和校驗(yàn)JWT
需要建立黑名單,一般使用
jti
這個(gè)
Claim
即可,技術(shù)上可以使用布隆過濾器加數(shù)據(jù)庫的組合(數(shù)量少的情況下簡單操作甚至可以用
Redis
的
SET
數(shù)據(jù)類型)JWS
的簽名算法盡可能使用安全性高的算法,如
RSXXX
Claims
盡可能不要寫入敏感信息JWT
認(rèn)證,需要進(jìn)行短信、指紋等二次認(rèn)證?PS:身邊有不少同事所在的項(xiàng)目會(huì)把JWT持久化,其實(shí)這違背了JWT的設(shè)計(jì)理念,把JWT當(dāng)成傳統(tǒng)的會(huì)話ID使用了
?
JWT
一般用于認(rèn)證場景,搭配API
網(wǎng)關(guān)使用效果甚佳。多數(shù)情況下,API
網(wǎng)關(guān)會(huì)存在一些通用不需要認(rèn)證的接口,其他則是需要認(rèn)證JWT
合法性并且提取JWT
中的消息載荷內(nèi)容進(jìn)行調(diào)用,針對這個(gè)場景:
JWT
認(rèn)證,這個(gè)場景在
Spring Cloud Gateway
中需要自定義實(shí)現(xiàn)一個(gè)
JWT
認(rèn)證的
WebFilter
URI
白名單集合,命中白名單則不需要進(jìn)行
JWT
認(rèn)證,這個(gè)場景在
Spring Cloud Gateway
中需要自定義實(shí)現(xiàn)一個(gè)
JWT
認(rèn)證的
GlobalFilter
下面就Spring Cloud Gateway
和jjwt
,貼一些骨干代碼,限于篇幅不進(jìn)行細(xì)節(jié)展開。引入依賴:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR10</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
然后編寫JwtSpi
和對應(yīng)的實(shí)現(xiàn)HMAC256JwtSpiImpl
:
@Data
public class CreateJwtDto {
private Long customerId;
private String customerName;
private String customerPhone;
}
@Data
public class JwtCacheContent {
private Long customerId;
private String customerName;
private String customerPhone;
}
@Data
public class VerifyJwtResultDto {
private Boolean valid;
private Throwable throwable;
private long jwtId;
private JwtCacheContent content;
}
public interface JwtSpi {
/**
* 生成JWT
*
* @param dto dto
* @return String
*/
String generate(CreateJwtDto dto);
/**
* 校驗(yàn)JWT
*
* @param jwt jwt
* @return VerifyJwtResultDto
*/
VerifyJwtResultDto verify(String jwt);
/**
* 把JWT添加到封禁名單中
*
* @param jwtId jwtId
*/
void blockJwt(long jwtId);
/**
* 判斷JWT是否在封禁名單中
*
* @param jwtId jwtId
* @return boolean
*/
boolean isInBlockList(long jwtId);
}
@Component
public class HMAC256JwtSpiImpl implements JwtSpi, InitializingBean, EnvironmentAware {
private SecretKey secretKey;
private Environment environment;
private int minSeed;
private String issuer;
private int seed;
private Random random;
@Override
public void afterPropertiesSet() throws Exception {
String secretKey = Objects.requireNonNull(environment.getProperty("jwt.hmac.secretKey"));
this.minSeed = Objects.requireNonNull(environment.getProperty("jwt.exp.seed.min", Integer.class));
int maxSeed = Objects.requireNonNull(environment.getProperty("jwt.exp.seed.max", Integer.class));
this.issuer = Objects.requireNonNull(environment.getProperty("jwt.issuer"));
this.random = new Random();
this.seed = (maxSeed - minSeed);
this.secretKey = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public String generate(CreateJwtDto dto) {
long duration = this.random.nextInt(this.seed) + minSeed;
Map<String, Object> claims = new HashMap<>(8);
claims.put("iss", issuer);
// 這里的jti最好用類似雪花算法之類的序列算法生成,確保唯一性
claims.put("jti", dto.getCustomerId());
claims.put("uid", dto.getCustomerId());
claims.put("exp", TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + duration);
String jwt = Jwts.builder()
.setHeaderParam("typ", "JWT")
.signWith(this.secretKey, SignatureAlgorithm.HS256)
.addClaims(claims)
.compact();
// 這里需要緩存uid->JwtCacheContent的信息
JwtCacheContent content = new JwtCacheContent();
// redis.set(KEY[uid],toJson(content),expSeconds);
return jwt;
}
@Override
public VerifyJwtResultDto verify(String jwt) {
JwtParser parser = Jwts.parserBuilder()
.requireIssuer(this.issuer)
.setSigningKey(this.secretKey)
.build();
VerifyJwtResultDto resultDto = new VerifyJwtResultDto();
try {
Jws<Claims> parseResult = parser.parseClaimsJws(jwt);
Claims claims = parseResult.getBody();
long jti = Long.parseLong(claims.getId());
if (isInBlockList(jti)) {
throw new IllegalArgumentException(String.format("jti is in block list,[i:%d]", jti));
}
long uid = claims.get("uid", Long.class);
// JwtCacheContent content = JSON.parse(redis.get(KEY[uid]),JwtCacheContent.class);
// resultDto.setContent(content);
resultDto.setValid(Boolean.TRUE);
} catch (Exception e) {
resultDto.setValid(Boolean.FALSE);
resultDto.setThrowable(e);
}
return resultDto;
}
@Override
public void blockJwt(long jwtId) {
}
@Override
public boolean isInBlockList(long jwtId) {
return false;
}
}
然后是JwtGlobalFilter
和JwtWebFilter
的非完全實(shí)現(xiàn):
@Component
public class JwtGlobalFilter implements GlobalFilter, Ordered, EnvironmentAware {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private List<String> accessUriList;
@Autowired
private JwtSpi jwtSpi;
private static final String JSON_WEB_TOKEN_KEY = "X-TOKEN";
private static final String UID_KEY = "X-UID";
private static final String JWT_ID_KEY = "X-JTI";
@Override
public void setEnvironment(Environment environment) {
accessUriList = Arrays.asList(Objects.requireNonNull(environment.getProperty("jwt.access.uris"))
.split(","));
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// OPTIONS 請求直接放行
HttpMethod method = request.getMethod();
if (Objects.nonNull(method) && Objects.equals(method, HttpMethod.OPTIONS)) {
return chain.filter(exchange);
}
// 獲取請求路徑
String requestPath = request.getPath().value();
// 命中請求路徑白名單
boolean matchWhiteRequestPathList = Optional.ofNullable(accessUriList)
.map(paths -> paths.stream().anyMatch(path -> pathMatcher.match(path, requestPath)))
.orElse(false);
if (matchWhiteRequestPathList) {
return chain.filter(exchange);
}
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(JSON_WEB_TOKEN_KEY);
if (!StringUtils.hasLength(token)) {
throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), "token is null");
}
VerifyJwtResultDto resultDto = jwtSpi.verify(token);
if (Objects.equals(resultDto.getValid(), Boolean.FALSE)) {
throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), resultDto.getThrowable());
}
headers.set(JWT_ID_KEY, String.valueOf(resultDto.getJwtId()));
headers.set(UID_KEY, String.valueOf(resultDto.getContent().getCustomerId()));
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
@Component
public class JwtWebFilter implements WebFilter {
@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
@Autowired
private JwtSpi jwtSpi;
private static final String JSON_WEB_TOKEN_KEY = "X-TOKEN";
private static final String UID_KEY = "X-UID";
private static final String JWT_ID_KEY = "X-JTI";
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// OPTIONS 請求直接放行
HttpMethod method = exchange.getRequest().getMethod();
if (Objects.nonNull(method) && Objects.equals(method, HttpMethod.OPTIONS)) {
return chain.filter(exchange);
}
HandlerMethod handlerMethod = requestMappingHandlerMapping.getHandlerInternal(exchange).block();
if (Objects.isNull(handlerMethod)) {
return chain.filter(exchange);
}
RequireJWT typeAnnotation = handlerMethod.getBeanType().getAnnotation(RequireJWT.class);
RequireJWT methodAnnotation = handlerMethod.getMethod().getAnnotation(RequireJWT.class);
if (Objects.isNull(typeAnnotation) && Objects.isNull(methodAnnotation)) {
return chain.filter(exchange);
}
HttpHeaders headers = exchange.getRequest().getHeaders();
String token = headers.getFirst(JSON_WEB_TOKEN_KEY);
if (!StringUtils.hasLength(token)) {
throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), "token is null");
}
VerifyJwtResultDto resultDto = jwtSpi.verify(token);
if (Objects.equals(resultDto.getValid(), Boolean.FALSE)) {
throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), resultDto.getThrowable());
}
headers.set(JWT_ID_KEY, String.valueOf(resultDto.getJwtId()));
headers.set(UID_KEY, String.valueOf(resultDto.getContent().getCustomerId()));
return chain.filter(exchange);
}
}
最后是一些配置屬性:
jwt.hmac.secretKey='00000000111111112222222233333333'
jwt.exp.seed.min=360000
jwt.exp.seed.max=8640000
jwt.issuer='throwx'
jwt.access.uris=/index,/actuator/*
筆者負(fù)責(zé)的API
網(wǎng)關(guān)使用了JWT
應(yīng)用于認(rèn)證場景,算法上使用了安全性稍高的RS256
,使用RSA
算法進(jìn)行簽名生成。項(xiàng)目上線初期,JWT
的過期時(shí)間都固定設(shè)置為7
天,生產(chǎn)日志發(fā)現(xiàn)該API
網(wǎng)關(guān)周期性發(fā)生"假死"現(xiàn)象,具體表現(xiàn)為:
Nginx
自檢周期性出現(xiàn)自檢接口調(diào)用超時(shí),提示部分或者全部
API
網(wǎng)關(guān)節(jié)點(diǎn)宕機(jī)API
網(wǎng)關(guān)所在機(jī)器的
CPU
周期性飆高,在用戶訪問量低的時(shí)候表現(xiàn)平穩(wěn)ELK
進(jìn)行日志排查,發(fā)現(xiàn)故障出現(xiàn)時(shí)段有
JWT
集中性過期和重新生成的日志痕跡排查結(jié)果表明JWT
集中過期和重新生成時(shí)候使用RSA
算法進(jìn)行簽名是CPU
密集型操作,同時(shí)重新生成大量JWT
會(huì)導(dǎo)致服務(wù)所在機(jī)器的CPU
超負(fù)載工作。「初步的解決方案是」:
JWT
生成的時(shí)候,過期時(shí)間添加一個(gè)隨機(jī)數(shù),例如
360000(1小時(shí)的毫秒數(shù)) ~ 8640000(24小時(shí)的毫秒數(shù))
之間取一個(gè)隨機(jī)值添加到當(dāng)前時(shí)間戳加
7
天得到
exp
值這個(gè)方法,對于一些老用戶營銷場景(老用戶長時(shí)間沒有登錄,他們客戶端緩存的JWT
一般都已經(jīng)過期)沒有效果。有時(shí)候運(yùn)營會(huì)通過營銷活動(dòng)喚醒老用戶,大量老用戶重新登錄有可能出現(xiàn)爆發(fā)性大批量重新生成JWT
的情況,對于這個(gè)場景提出兩個(gè)解決思路:
JWT
時(shí)候,考慮延長過期時(shí)間,但是時(shí)間越長,風(fēng)險(xiǎn)越大API
網(wǎng)關(guān)所在機(jī)器的硬件配置,特別是
CPU
配置,現(xiàn)在很多云廠商都有彈性擴(kuò)容方案,可以很好應(yīng)對這類突發(fā)流量場景主流的JWT
方案是JWS
,此方案是只編碼和簽名,不加密,務(wù)必注意這一點(diǎn),JWS
方案是無狀態(tài)并且不安全的,關(guān)鍵操作應(yīng)該做多重認(rèn)證,也要做好黑名單機(jī)制防止JWT
泄漏后造成安全性問題。JWT
不存儲(chǔ)在服務(wù)端,這既是它的優(yōu)勢,同時(shí)也是它的劣勢。很多軟件架構(gòu)都無法做到盡善盡美,這個(gè)時(shí)候只能權(quán)衡利弊。
參考資料:
感謝各位的閱讀,以上就是“JWT的實(shí)現(xiàn)原理和基本使用方法”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對JWT的實(shí)現(xiàn)原理和基本使用方法這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。