<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          冷飯新炒:理解JWT的實現(xiàn)原理和基本使用

          共 15395字,需瀏覽 31分鐘

           ·

          2021-02-20 10:56

          前提

          這是《冷飯新炒》系列的第五篇文章。

          本文會翻炒一個用以產(chǎn)生訪問令牌的開源標準JWT,介紹JWT的規(guī)范、底層實現(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,里面有詳細地介紹JWT的基本概念,Claims的含義、布局和算法實現(xiàn)等,下面逐個展開擊破。

          JWT基本概念

          JWT全稱是JSON Web Token,如果從字面上理解感覺是基于JSON格式用于網(wǎng)絡傳輸?shù)牧钆?。實際上,JWT是一種緊湊的Claims聲明格式,旨在用于空間受限的環(huán)境進行傳輸,常見的場景如HTTP授權請求頭參數(shù)和URI查詢參數(shù)。JWT會把Claims轉(zhuǎn)換成JSON格式,而這個JSON內(nèi)容將會應用為JWS結構的有效載荷或者應用為JWE結構的(加密處理后的)原始字符串,通過消息認證碼(Message Authentication Code或者簡稱MAC)和/或者加密操作對Claims進行數(shù)字簽名或者完整性保護。

          這里有三個概念在其他規(guī)范文件中,簡單提一下:

          • JWE(規(guī)范文件RFC 7516):JSON Web Encryption,表示基于JSON數(shù)據(jù)結構的加密內(nèi)容,加密機制對任意八位字節(jié)序列進行加密、提供完整性保護和提高破解難度,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ù)結構和BASE64URL編碼表示經(jīng)過數(shù)字簽名或消息認證碼(MAC)認證的內(nèi)容,數(shù)字簽名或者MAC能夠提供完整性保護,JWS中的緊湊序列化布局如下:
          ASCII(BASE64URL(UTF8(JWS?Protected?Header))?||?'.'?||?
          BASE64URL(JWS?Payload))?||?'.'?||
          BASE64URL(JWS?Signature)
          • JWA(規(guī)范文件RFC 7518):JSON Web Algorithm,JSON Web算法,數(shù)字簽名或者MAC算法,應用于JWS的可用算法列表如下:

          總的來說,JWT其實有兩種實現(xiàn),基于JWE實現(xiàn)的依賴于加解密算法、BASE64URL編碼和身份認證等手段提高傳輸?shù)?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">Claims的被破解難度,而基于JWS的實現(xiàn)使用了BASE64URL編碼和數(shù)字簽名的方式對傳輸?shù)?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">Claims提供了完整性保護,也就是僅僅保證傳輸?shù)?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">Claims內(nèi)容不被篡改,但是會暴露明文。「目前主流的JWT框架中大部分都沒有實現(xiàn)JWE,所以下文主要通過JWS的實現(xiàn)方式進行深入探討」。

          JWT中的Claims

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

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

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

          簡稱全稱含義
          cidCustomer ID客戶ID
          ridRole ID角色ID

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

          JWT中的Header

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

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

          最常見的兩個Header就是algtyp,例如:

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

          JWT的布局

          主要介紹JWS的布局,前面已經(jīng)提到過,JWS「緊湊布局」如下:

          ASCII(BASE64URL(UTF8(JWS?Protected?Header))?||?'.'?||?
          BASE64URL(JWS?Payload))?||?'.'?||
          BASE64URL(JWS?Signature)

          其實還有「非緊湊布局」,會通過一個JSON結構完整地展示Header參數(shù)、Claims和分組簽名:

          {
          ????"payload":"",
          ????"signatures":[
          ????{"protected":"",
          ????"header":1?contents>,
          ????"signature":""},
          ????...
          ????{"protected":"",
          ????"header":,
          ????"signature":""}]
          }

          非緊湊布局還有一個扁平化的表示形式:

          {
          ????"payload":"",
          ????"protected":"",
          ????"header":,
          ????"signature":""
          }

          其中Header參數(shù)部分可以參看上一小節(jié),而簽名部分可以參看下一小節(jié),剩下簡單提一下payload部分,payload(有效載荷)其實就是完整的Claims,假設ClaimsJSON形式是:

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

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

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

          JWS簽名算法

          JWS簽名生成依賴于散列或者加解密算法,可以使用的算法見前面貼出的圖,例如HS256,具體是HMAC SHA-256,也就是通過散列算法SHA-256對于編碼后的HeaderClaims字符串進行一次散列計算,簽名生成的偽代碼如下:

          ##?不進行編碼
          HMACSHA256(
          ??base64UrlEncode(header)?+?"."?+
          ??base64UrlEncode(payload),
          ??256?bit?secret?key
          )

          #
          #?進行編碼
          base64UrlEncode(
          ????HMACSHA256(
          ???????base64UrlEncode(header)?+?"."?+
          ???????base64UrlEncode(payload)
          ???????[256?bit?secret?key])
          )

          其他算法的操作基本相似,生成好的簽名直接加上一個前置的.拼接在base64UrlEncode(header).base64UrlEncode(payload)之后就生成完整的JWS

          JWT的生成、解析和校驗

          前面已經(jīng)分析過JWT的一些基本概念、布局和簽名算法,這里根據(jù)前面的理論進行JWT的生成、解析和校驗操作。先引入common-codec庫簡化一些編碼和加解密操作,引入一個主流的JSON框架做序列化和反序列化:

          <dependency>
          ????<groupId>commons-codecgroupId>
          ????<artifactId>commons-codecartifactId>
          ????<version>1.15version>
          dependency>
          <dependency>
          ????<groupId>com.fasterxml.jackson.coregroupId>
          ????<artifactId>jackson-databindartifactId>
          ????<version>2.11.0version>
          dependency>

          為了簡單起見,Header參數(shù)寫死為:

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

          使用的簽名算法是HMAC SHA-256,輸入的加密密鑰長度必須為256 bit(如果單純用英文和數(shù)字組成的字符,要32個字符),這里為了簡單起見,用00000000111111112222222233333333作為KEY。定義Claims部分如下:

          {
          ??"iss":?"throwx",
          ??"jid":?10087,??#?<----?這里有個筆誤,本來打算寫成jti,后來發(fā)現(xiàn)寫錯了,不打算改
          ??"exp":?1613227468168?????#?20210213????
          }

          生成JWT的代碼如下:

          @Slf4j
          public?class?JsonWebToken?{

          ????private?static?final?String?KEY?=?"00000000111111112222222233333333";

          ????private?static?final?String?DOT?=?".";

          ????private?static?final?Map?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?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?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?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上驗證一下:

          解析JWT的過程是構造JWT的逆向過程,首先基于點號.分三段,然后分別進行BASE64解碼,然后得到三部分的明文,頭部參數(shù)和有效載荷需要做一次JSON反序列化即可還原各個部分的JSON結構:

          public?Map?parse(String?jwt)?throws?Exception?{
          ????System.out.println("當前解析的JWT:"?+?jwt);
          ????Map?result?=?new?HashMap<>(8);
          ????//?這里暫且認為所有的輸入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>()?{
          ????}));
          ????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>()?{
          ????}));
          ????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?pairs;
          }

          這里嘗試用之前生產(chǎn)的JWT進行解析:

          public?static?void?main(String[]?args)?throws?Exception?{
          ????JsonWebToken?jsonWebToken?=?new?JsonWebToken();
          ????String?jwt?=?"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs";
          ????Map?parseResult?=?jsonWebToken.parse(jwt);
          ????System.out.printf("解析結果如下:\nHEADER:%s\nPAYLOAD:%s\nSIGNATURE:%s%n",
          ????????????parseResult.get(Part.HEADER),
          ????????????parseResult.get(Part.PAYLOAD),
          ????????????parseResult.get(Part.SIGNATURE)
          ????);
          }

          解析結果如下:

          當前解析的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
          解析結果如下:
          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)

          驗證JWT建立在解析JWT完成的基礎之上,需要對解析出來的頭部參數(shù)和有效載做一次MAC簽名,與解析出來的簽名做校對。另外,可以自定義校驗具體的Claim項,如過期時間和發(fā)行者等。一般校驗失敗會針對不同的情況定制不同的運行時異常便于區(qū)分場景,這里為了方便統(tǒng)一拋出IllegalStateException

          public?void?verify(String?jwt)?throws?Exception?{
          ????System.out.println("當前校驗的JWT:"?+?jwt);
          ????Map?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("簽名校驗異常");
          ????}
          ????String?iss?=?payloadContent.getPairs().get("iss").toString();
          ????//?iss校驗
          ????if?(!Objects.equals(iss,?"throwx"))?{
          ????????throw?new?IllegalStateException("ISS校驗異常");
          ????}
          ????long?exp?=?Long.parseLong(payloadContent.getPairs().get("exp").toString());
          ????//?exp校驗,有效期14天
          ????if?(System.currentTimeMillis()?-?exp?>?24?*?3600?*?1000?*?14)?{
          ????????throw?new?IllegalStateException("exp校驗異常,JWT已經(jīng)過期");
          ????}
          ????//?省略其他校驗項
          ????System.out.println("JWT校驗通過");
          }

          類似地,用上面生成過的JWT進行驗證,結果如下:

          當前校驗的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
          當前解析的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
          23:33:00.174?[main]?INFO?club.throwable.jwt.JsonWebToken?-?生成的Signature部分為:7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
          JWT校驗通過

          「上面的代碼存在硬編碼問題,只是為了用最簡單的JWS實現(xiàn)方式重新實現(xiàn)了JWT的生成、解析和校驗過程」,算法也使用了復雜程度和安全性極低的HS256,所以在生產(chǎn)中并不推薦花大量時間去實現(xiàn)JWS,可以選用現(xiàn)成的JWT類庫,如auth0jjwt。

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

          JWT本質(zhì)是一個令牌,更多場景下是作為會話IDsession_id)使用,作用是'維持會話的粘性'和攜帶認證信息(如果用JWT術語,應該是安全地傳遞Claims)。筆者記得很久以前使用的一種Session ID解決方案是由服務端生成和持久化Session ID,返回的Session ID需要寫入用戶的Cookie,然后用戶每次請求必須攜帶Cookie,Session ID會映射用戶的一些認證信息,這一切都是由服務端管理,一個很常見的例子就是Tomcat容器中出現(xiàn)的J(ava)SESSIONID。與之前的方案不同,JWT是一種無狀態(tài)的令牌,它并不需要由服務端保存,攜帶的數(shù)據(jù)或者會話的數(shù)據(jù)都不需要持久化,使用JWT只需要關注Claims的完整性和合法性即可,生成JWT時候所有有效數(shù)據(jù)已經(jīng)通過編碼存儲在JWT字符串中。正因JWT是無狀態(tài)的,一旦頒發(fā)后得到JWT的客戶端都可以通過它與服務端交互,JWT一旦泄露有可能造成嚴重安全問題,因此實踐的時候一般需要做幾點:

          • JWT需要設置有效期,也就是exp這個Claim必須啟用和校驗
          • JWT需要建立黑名單,一般使用jti這個Claim即可,技術上可以使用布隆過濾器加數(shù)據(jù)庫的組合(數(shù)量少的情況下簡單操作甚至可以用RedisSET數(shù)據(jù)類型)
          • JWS的簽名算法盡可能使用安全性高的算法,如RSXXX
          • Claims盡可能不要寫入敏感信息
          • 高風險場景如支付操作等不能僅僅依賴JWT認證,需要進行短信、指紋等二次認證
          ?

          PS:身邊有不少同事所在的項目會把JWT持久化,其實這違背了JWT的設計理念,把JWT當成傳統(tǒng)的會話ID使用了

          ?

          JWT一般用于認證場景,搭配API網(wǎng)關使用效果甚佳。多數(shù)情況下,API網(wǎng)關會存在一些通用不需要認證的接口,其他則是需要認證JWT合法性并且提取JWT中的消息載荷內(nèi)容進行調(diào)用,針對這個場景:

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

          下面就Spring Cloud Gatewayjjwt,貼一些骨干代碼,限于篇幅不進行細節(jié)展開。引入依賴:

          <dependencyManagement>
          ????<dependencies>
          ????????<dependency>
          ????????????<groupId>org.springframework.cloudgroupId>
          ????????????<artifactId>spring-cloud-dependenciesartifactId>
          ????????????<version>Hoxton.SR10version>
          ????????????<type>pomtype>
          ????????????<scope>importscope>
          ????????dependency>
          ????dependencies>
          dependencyManagement>
          <dependencies>
          ????<dependency>
          ????????<groupId>io.jsonwebtokengroupId>
          ????????<artifactId>jjwt-apiartifactId>
          ????????<version>0.11.2version>
          ????dependency>
          ????<dependency>
          ????????<groupId>io.jsonwebtokengroupId>
          ????????<artifactId>jjwt-implartifactId>
          ????????<version>0.11.2version>
          ????????<scope>runtimescope>
          ????dependency>
          ????<dependency>
          ????????<groupId>io.jsonwebtokengroupId>
          ????????<artifactId>jjwt-jacksonartifactId>
          ????????<version>0.11.2version>
          ????????<scope>runtimescope>
          ????dependency>
          ????<dependency>
          ????????<groupId>org.projectlombokgroupId>
          ????????<artifactId>lombokartifactId>
          ????????<version>1.18.18version>
          ????????<scope>providedscope>
          ????dependency>
          ????<dependency>
          ????????<groupId>org.springframework.cloudgroupId>
          ????????<artifactId>spring-cloud-starter-gatewayartifactId>
          ????dependency>
          dependencies>

          然后編寫JwtSpi和對應的實現(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);

          ????/**
          ?????*?校驗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?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?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的非完全實現(xiàn):

          @Component
          public?class?JwtGlobalFilter?implements?GlobalFilter,?Ordered,?EnvironmentAware?{

          ????private?final?AntPathMatcher?pathMatcher?=?new?AntPathMatcher();

          ????private?List?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?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?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)遇到的坑

          筆者負責的API網(wǎng)關使用了JWT應用于認證場景,算法上使用了安全性稍高的RS256,使用RSA算法進行簽名生成。項目上線初期,JWT的過期時間都固定設置為7天,生產(chǎn)日志發(fā)現(xiàn)該API網(wǎng)關周期性發(fā)生"假死"現(xiàn)象,具體表現(xiàn)為:

          • Nginx自檢周期性出現(xiàn)自檢接口調(diào)用超時,提示部分或者全部API網(wǎng)關節(jié)點宕機
          • API網(wǎng)關所在機器的CPU周期性飆高,在用戶訪問量低的時候表現(xiàn)平穩(wěn)
          • 通過ELK進行日志排查,發(fā)現(xiàn)故障出現(xiàn)時段有JWT集中性過期和重新生成的日志痕跡

          排查結果表明JWT集中過期和重新生成時候使用RSA算法進行簽名是CPU密集型操作,同時重新生成大量JWT會導致服務所在機器的CPU超負載工作。「初步的解決方案是」

          • JWT生成的時候,過期時間添加一個隨機數(shù),例如360000(1小時的毫秒數(shù)) ~ 8640000(24小時的毫秒數(shù))之間取一個隨機值添加到當前時間戳加7天得到exp

          這個方法,對于一些老用戶營銷場景(老用戶長時間沒有登錄,他們客戶端緩存的JWT一般都已經(jīng)過期)沒有效果。有時候運營會通過營銷活動喚醒老用戶,大量老用戶重新登錄有可能出現(xiàn)爆發(fā)性大批量重新生成JWT的情況,對于這個場景提出兩個解決思路:

          • 首次生成JWT時候,考慮延長過期時間,但是時間越長,風險越大
          • 提升API網(wǎng)關所在機器的硬件配置,特別是CPU配置,現(xiàn)在很多云廠商都有彈性擴容方案,可以很好應對這類突發(fā)流量場景

          小結

          主流的JWT方案是JWS,此方案是只編碼和簽名,不加密,務必注意這一點,JWS方案是無狀態(tài)并且不安全的,關鍵操作應該做多重認證,也要做好黑名單機制防止JWT泄漏后造成安全性問題。JWT不存儲在服務端,這既是它的優(yōu)勢,同時也是它的劣勢。很多軟件架構都無法做到盡善盡美,這個時候只能權衡利弊。

          參考資料:

          • RFC 7519
          • jjwt部分源碼

          (本文完 c-3-w e-a-20210219)

          瀏覽 58
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  最新欧美操逼 | 久久日批 | 激情五月色情在线播放 | 日韩专区中文字幕 | 无码免费性爱视频 |