溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JWT的實(shí)現(xiàn)原理和基本使用方法

發(fā)布時(shí)間:2021-07-19 10:22:43 來源:億速云 閱讀:279 作者:chen 欄目:編程語言

這篇文章主要講解了“JWT的實(shí)現(xiàn)原理和基本使用方法”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“JWT的實(shí)現(xiàn)原理和基本使用方法”吧!


 

JWT規(guī)范

很可惜維基百科上沒有搜索到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基本概念

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í)現(xiàn)原理和基本使用方法  

總的來說,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)行深入探討」。

 

JWT中的Claims

Claim有索賠、聲稱、要求或者權(quán)利要求的含義,但是筆者覺得任一個(gè)翻譯都不怎么合乎語義,這里保留Claim關(guān)鍵字直接作為命名。JWT的核心作用就是保護(hù)Claims的完整性(或者數(shù)據(jù)加密),保證JWT傳輸?shù)倪^程中Claims不被篡改(或者不被破解)。ClaimsJWT原始內(nèi)容中是一個(gè)JSON格式的字符串,其中單個(gè)ClaimK-V結(jié)構(gòu),作為JsonNode中的一個(gè)field-value,這里列出常用的規(guī)范中預(yù)定義好的Claim

簡稱全稱含義
issIssuer發(fā)行方
subSubject主體
audAudience(接收)目標(biāo)方
expExpiration Time過期時(shí)間
nbfNot Before早于該定義的時(shí)間的JWT不能被接受處理
iatIssued AtJWT發(fā)行時(shí)的時(shí)間戳
jtiJWT IDJWT的唯一標(biāo)識(shí)

這些預(yù)定義的Claim并不要求強(qiáng)制使用,何時(shí)選用何種Claim完全由使用者決定,而為了使JWT更加緊湊,這些Claim都使用了簡短的命名方式去定義。在不和內(nèi)建的Claim沖突的前提下,使用者可以自定義新的公共Claim,如:

簡稱全稱含義
cidCustomer ID客戶ID
ridRole ID角色I(xiàn)D

一定要注意,在JWS實(shí)現(xiàn)中,Claims會(huì)作為payload部分進(jìn)行BASE64編碼,明文會(huì)直接暴露,敏感信息一般不應(yīng)該設(shè)計(jì)為一個(gè)自定義Claim。

 

JWT中的Header

JWT規(guī)范文件中稱這些HeaderJOSE Header,JOSE的全稱為Javascript Object Signature Encryption,也就是Javascript對象簽名和加密框架,JOSE Header其實(shí)就是Javascript對象簽名和加密的頭部參數(shù)。「下面列舉一下JWS中常用的Header

簡稱全稱含義
algAlgorithm用于保護(hù)JWS的加解密算法
jkuJWK Set URL一組JSON編碼的公共密鑰的URL,其中一個(gè)是用于對JWS進(jìn)行數(shù)字簽名的密鑰
jwkJSON Web Key用于對JWS進(jìn)行數(shù)字簽名的密鑰相對應(yīng)的公共密鑰
kidKey ID用于保護(hù)JWS進(jìn)的密鑰
x5uX.509 URLX.509相關(guān)
x5cX.509 Certificate ChainX.509相關(guān)
x5tX.509 Certificate SHA-1 ThumbprinX.509相關(guān)
x5t#S256X.509 Certificate SHA-256 ThumbprintX.509相關(guān)
typType類型,例如JWT、JWS或者JWE等等
ctyContent Type內(nèi)容類型,決定payload部分的MediaType

最常見的兩個(gè)Header就是algtyp,例如:

{
  "alg": "HS256",
  "typ": "JWT"
}
   

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è)ClaimsJSON形式是:

{
   "iss": "throwx",
   "jid": 1
}
 

那么扁平化非緊湊格式下的payload節(jié)點(diǎn)就是:

{  
   ......
   "payload": {
      "iss": "throwx",
      "jid": 1
   }
   ......
}
   

JWS簽名算法

JWS簽名生成依賴于散列或者加解密算法,可以使用的算法見前面貼出的圖,例如HS256,具體是HMAC SHA-256,也就是通過散列算法SHA-256對于編碼后的HeaderClaims字符串進(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。

 

JWT的生成、解析和校驗(yàn)

前面已經(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的實(shí)現(xià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類庫,如auth0jjwt。

 

JWT的使用場景和實(shí)戰(zhàn)

JWT本質(zhì)是一個(gè)令牌,更多場景下是作為會(huì)話IDsession_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盡可能不要寫入敏感信息
  • 高風(fēng)險(xiǎn)場景如支付操作等不能僅僅依賴     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è)場景:

  • 對于控制器入口可以提供一個(gè)自定義注解標(biāo)識(shí)特定接口需要進(jìn)行     JWT認(rèn)證,這個(gè)場景在     Spring Cloud Gateway中需要自定義實(shí)現(xiàn)一個(gè)     JWT認(rèn)證的     WebFilter
  • 對于單純的路由和轉(zhuǎn)發(fā)可以提供一個(gè)     URI白名單集合,命中白名單則不需要進(jìn)行     JWT認(rèn)證,這個(gè)場景在     Spring Cloud Gateway中需要自定義實(shí)現(xiàn)一個(gè)     JWT認(rèn)證的     GlobalFilter

下面就Spring Cloud Gatewayjjwt,貼一些骨干代碼,限于篇幅不進(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;
    }
}
 

然后是JwtGlobalFilterJwtWebFilter的非完全實(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/*
   

使用JWT曾經(jīng)遇到的坑

筆者負(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ā)流量場景
 

小結(jié)

主流的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)衡利弊。

參考資料:

  • RFC 7519
  • jjwt部分源碼

感謝各位的閱讀,以上就是“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)注!

向AI問一下細(xì)節(jié)

免責(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)容。

jwt
AI