<sub id="gqw76"><listing id="gqw76"></listing></sub>
      <sub id="gqw76"><listing id="gqw76"></listing></sub>

    1. <form id="gqw76"><legend id="gqw76"></legend></form>
    2. Loading

      冷飯新炒:理解JWT的實現原理和基本使用

      前提

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

      本文會翻炒一個用以產生訪問令牌的開源標準JWT,介紹JWT的規范、底層實現原理、基本使用和應用場景。

      JWT規范

      很可惜維基百科上沒有搜索到JWT的條目,但是從jwt.io的首頁展示圖中,可以看到描述:

      JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties

      從這段文字中可以提取到JWT的規范文件RFC 7519,里面有詳細地介紹JWT的基本概念,Claims的含義、布局和算法實現等,下面逐個展開擊破。

      JWT基本概念

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

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

      • JWE(規范文件RFC 7516):JSON Web Encryption,表示基于JSON數據結構的加密內容,加密機制對任意八位字節序列進行加密、提供完整性保護和提高破解難度,JWE中的緊湊序列化布局如下
      BASE64URL(UTF8(JWE Protected Header)) || '.' ||
      BASE64URL(JWE Encrypted Key) || '.' ||
      BASE64URL(JWE Initialization Vector) || '.' ||
      BASE64URL(JWE Ciphertext) || '.' ||
      BASE64URL(JWE Authentication Tag)
      
      • JWS(規范文件RFC 7515):JSON Web Signature,表示使用JSON數據結構和BASE64URL編碼表示經過數字簽名或消息認證碼(MAC)認證的內容,數字簽名或者MAC能夠提供完整性保護,JWS中的緊湊序列化布局如下:
      ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || 
      BASE64URL(JWS Payload)) || '.' ||
      BASE64URL(JWS Signature)
      
      • JWA(規范文件RFC 7518):JSON Web AlgorithmJSON Web算法,數字簽名或者MAC算法,應用于JWS的可用算法列表如下:

      總的來說,JWT其實有兩種實現,基于JWE實現的依賴于加解密算法、BASE64URL編碼和身份認證等手段提高傳輸的Claims的被破解難度,而基于JWS的實現使用了BASE64URL編碼和數字簽名的方式對傳輸的Claims提供了完整性保護,也就是僅僅保證傳輸的Claims內容不被篡改,但是會暴露明文。目前主流的JWT框架中大部分都沒有實現JWE,所以下文主要通過JWS的實現方式進行深入探討

      JWT中的Claims

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

      簡稱 全稱 含義
      iss Issuer 發行方
      sub Subject 主體
      aud Audience (接收)目標方
      exp Expiration Time 過期時間
      nbf Not Before 早于該定義的時間的JWT不能被接受處理
      iat Issued At JWT發行時的時間戳
      jti JWT ID JWT的唯一標識

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

      簡稱 全稱 含義
      cid Customer ID 客戶ID
      rid Role ID 角色ID

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

      JWT中的Header

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

      簡稱 全稱 含義
      alg Algorithm 用于保護JWS的加解密算法
      jku JWK Set URL 一組JSON編碼的公共密鑰的URL,其中一個是用于對JWS進行數字簽名的密鑰
      jwk JSON Web Key 用于對JWS進行數字簽名的密鑰相對應的公共密鑰
      kid Key ID 用于保護JWS進的密鑰
      x5u X.509 URL X.509相關
      x5c X.509 Certificate Chain X.509相關
      x5t X.509 Certificate SHA-1 Thumbprin X.509相關
      x5t#S256 X.509 Certificate SHA-256 Thumbprint X.509相關
      typ Type 類型,例如JWTJWS或者JWE等等
      cty Content Type 內容類型,決定payload部分的MediaType

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

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

      JWT的布局

      主要介紹JWS的布局,前面已經提到過,JWS緊湊布局如下:

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

      其實還有非緊湊布局,會通過一個JSON結構完整地展示Header參數、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>"}]
      }
      

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

      {
          "payload":"<payload contents>",
          "protected":"<integrity-protected header contents>",
          "header":<non-integrity-protected header contents>,
          "signature":"<signature contents>"
      }
      

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

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

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

      {  
         ......
         "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的生成、解析和校驗

      前面已經分析過JWT的一些基本概念、布局和簽名算法,這里根據前面的理論進行JWT的生成、解析和校驗操作。先引入common-codec庫簡化一些編碼和加解密操作,引入一個主流的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參數寫死為:

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

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

      {
        "iss": "throwx",
        "jid": 10087,  # <---- 這里有個筆誤,本來打算寫成jti,后來發現寫錯了,不打算改
        "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));
          }
      }
      

      執行輸出日志如下:

      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解碼,然后得到三部分的明文,頭部參數和有效載荷需要做一次JSON反序列化即可還原各個部分的JSON結構:

      public Map<Part, PartContent> parse(String jwt) throws Exception {
          System.out.println("當前解析的JWT:" + jwt);
          Map<Part, PartContent> 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<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;
      }
      

      這里嘗試用之前生產的JWT進行解析:

      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("解析結果如下:\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完成的基礎之上,需要對解析出來的頭部參數和有效載做一次MAC簽名,與解析出來的簽名做校對。另外,可以自定義校驗具體的Claim項,如過期時間和發行者等。一般校驗失敗會針對不同的情況定制不同的運行時異常便于區分場景,這里為了方便統一拋出IllegalStateException

      public void verify(String jwt) throws Exception {
          System.out.println("當前校驗的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("簽名校驗異常");
          }
          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已經過期");
          }
          // 省略其他校驗項
          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實現方式重新實現了JWT的生成、解析和校驗過程,算法也使用了復雜程度和安全性極低的HS256,所以在生產中并不推薦花大量時間去實現JWS,可以選用現成的JWT類庫,如auth0jjwt

      JWT的使用場景和實戰

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

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

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

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

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

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

      <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和對應的實現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<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的非完全實現:

      @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曾經遇到的坑

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

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

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

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

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

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

      小結

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

      參考資料:

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

      posted @ 2021-02-20 08:55  throwable  閱讀(2221)  評論(10編輯  收藏
      最新chease0ldman老人|无码亚洲人妻下载|大香蕉在线看好吊妞视频这里有精品www|亚洲色情综合网

        <sub id="gqw76"><listing id="gqw76"></listing></sub>
        <sub id="gqw76"><listing id="gqw76"></listing></sub>

      1. <form id="gqw76"><legend id="gqw76"></legend></form>