1. 程式人生 > 實用技巧 >JWT認證登陸方式

JWT認證登陸方式

JWT認證登陸方式

在學習前後端分離的過程中往往遇到的第一個問題就是登陸驗證問題,以前的我們將Cookie、Session、Token,在我的學習過程中認識了JWT驗證方案,純屬自己理解,有錯還望大佬指出。

JWT 全稱是 JSON Web Token,是目前非常流行的跨域認證解決方案.

前情回顧

早期的Cookie和Session的認證方式,之前沒有進行前後端分離的模式大部分使用的是Cookie-Session的認證方式,現在的未分離的專案基本也還是這種模式。

認證的大致流程就是:

  1. 使用者在前端輸入使用者名稱和密碼進行登陸系統。
  2. 服務端進行使用者名稱和密碼的驗證,建立一個Session物件,存入記憶體中,並將此Session的ID返回給前端
  3. 前端儲存返回的SessionID,每次請求都要帶著SessionID,服務端通過SessionID進行校驗。

JWT

JWT 就是一種Cookie-Session改造版的具體實現,讓你省去自己造輪子的時間,JWT 還有個好處,那就是你可以不用在服務端儲存認證資訊(比如 token),完全由客戶端提供,服務端只要根據 JWT 自身提供的解密演算法就可以驗證使用者合法性,而且這個過程是安全的。

如果你是剛接觸 JWT,最有疑問的一點可能就是: JWT 為什麼可以完全依靠客戶端(比如瀏覽器端)就能實現認證功能,認證資訊全都存在客戶端,怎麼保證安全性?

JWT的構成

JWT 是由三段字串和兩個.組成,每個字串和字串之間沒有換行(類似於這樣:xxxxxx.yyyyyy.zzzzzz),每個字串代表了不同的功能

  • JWT頭部
  • 有效載荷
  • 雜湊簽名

我們將這三個字串的功能按順序列出來並講解:

1. JWT 頭

JWT 頭描述了 JWT 元資料,是一個 JSON 物件,它的格式如下:

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

這裡的 alg 屬性表示簽名所使用的演算法,JWT 簽名預設的演算法為 HMAC SHA256 , alg 屬性值 HS256 就是 HMAC SHA256 演算法。typ 屬性表示令牌型別,這裡就是 JWT。

2. 有效載荷

有效載荷是 JWT 的主體,同樣也是個 JSON 物件。有效載荷包含三個部分:

  • 標準註冊宣告標準註冊宣告不是強制使用是的,但是我建議使用。它一般包括以下內容:

    • iss:jwt的簽發者/發行人;
    • sub:主題;
    • aud:接收方;
    • exp:jwt過期時間;
    • nbf:jwt生效時間;
    • iat:簽發時間
    • jti:jwt唯一身份標識,可以避免重放攻擊
  • 公共宣告:可以在公共宣告新增任何資訊,我們一般會在裡面新增使用者資訊和業務資訊,但是不建議新增敏感資訊,因為公共宣告部分可以在客戶端解密。

  • 私有宣告:私有宣告是伺服器和客戶端共同定義的宣告,同樣這裡不建議新增敏感資訊

下面這個程式碼段就是定義了一個有效載荷:

{"exp":"201909181230","role":"admin","isShow":false}

3. 雜湊簽名

雜湊簽名的演算法主要是確保資料不會被篡改。它主要是對前面所講的兩個部分進行簽名,通過 JWT 頭定義的演算法生成雜湊。雜湊簽名的過程如下:

  1. 指定密碼,密碼儲存在伺服器中,不能向客戶端公開;

  2. 使用 JWT 頭指定的演算法進行簽名,進行簽名前需要對 JWT 頭和有效載荷進行 Base64URL 編碼,JWT 頭和有效載荷編碼後的結果之間需要用 . 來連線。

簡單示例如下:

HMACSHA256(base64UrlEncode(JWT 頭) + "." + base64UrlEncode(有效載荷),密碼)

最終結果如下:

base64UrlEncode(JWT 頭)+"."+base64UrlEncode(有效載荷)+"."+HMACSHA256(base64UrlEncode(JWT 頭) + "." + base64UrlEncode(有效載荷),密碼)

使用方式

  1. 使用者在前端輸入使用者名稱和密碼進行登陸系統。
  2. 伺服器對賬號密碼進行驗證,計算出該使用者的JWT字串,並且返回給客戶端
  3. 客戶端將後端返回的JWT字串儲存到Cookie或者是LocalStorage
  4. 客戶端在進行請求的時候需要帶上JWT
  5. 服務端拿到這個JWT字串後,使用 base64的頭部和 base64 的載荷部分,通過HMACSHA256演算法計算簽名部分,比較計算結果和傳來的簽名部分是否一致,如果一致,說明此次請求沒有問題,如果不一致,說明請求過期或者是非法請求。

Java實現

  • 依賴

    <dependency>
    	<groupId>com.auth0</groupId>
    	<artifactId>java-jwt</artifactId>
    	<version>3.1.0</version>
    </dependency>
    
    <dependency>
    	<groupId>org.apache.commons</groupId>
    	<artifactId>commons-lang3</artifactId>
    	<version>3.4</version>
    </dependency>
    
  • 實現

    package com.example.jwtmdeo.utils;
    
    import java.io.UnsupportedEncodingException;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.UUID;
    
    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTCreator;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.interfaces.Claim;
    import com.auth0.jwt.interfaces.DecodedJWT;
    
    
    /**
     * JWT處理類
     */
    public class JwtHelper {
    
        private static final String SECRET = "9a96349e2345385785e804e0f4254dee";   //加密字串   隨便定義
    
        private static String ISSUER = "sys_user"; //簽發人
    
        /**
         * 生成JWT
         * @param claims  有效載荷
         * @param expireDatePoint  過期時間點
         * @return
         */
        public static String createJWT(Map<String, String> claims, Date expireDatePoint){
    
            try {
                //使用HMAC256進行加密
                Algorithm algorithm = Algorithm.HMAC256(SECRET);
    
                //建立jwt
                JWTCreator.Builder builder = JWT.create().
                        withIssuer(ISSUER). //發行人
                        withExpiresAt(expireDatePoint); //過期時間點
    
                //新增有效載荷
                claims.forEach((key,value)-> {
                    builder.withClaim(key, value);
                });
    
                //簽名加密
                return builder.sign(algorithm);
            } catch (IllegalArgumentException | UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }
    
       /**
         * 解密jwt
         * @param token
         * @return
         * @throws RuntimeException
         */
        public static Map<String,String> verifyToken(String token) throws RuntimeException{
            Algorithm algorithm = null;
            try {
                //使用HMAC256進行加密
                algorithm = Algorithm.HMAC256(SECRET);
            } catch (IllegalArgumentException | UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
    
            //解密
            JWTVerifier verifier = JWT.require(algorithm).withIssuer(ISSUER).build();
            DecodedJWT jwt =  verifier.verify(token);
            Map<String, Claim> map = jwt.getClaims();
            Map<String, String> resultMap = new HashMap<>();
            map.forEach((k,v) -> resultMap.put(k, v.asString()));
            return resultMap;
        }
    }
    

注意事項

在使用 JWT 時需要注意以下事項:

  1. JWT 預設不加密,如果要寫入敏感資訊必須加密,可以用生成的原始令牌再次對內容進行加密;

  2. JWT 無法使伺服器儲存會話狀態,當令牌生成後在有效期內無法取消也不能更改;

  3. JWT 包含認證資訊,如果洩露了,任何人都可以獲得令牌所有的許可權;因此 JWT 有效期不能太長,對於重要操作每次請求都必須進行身份驗證。

  4. 一旦頒發一個 JWT 令牌,服務端就沒辦法廢棄掉它,除非等到它自身過期。有很多應用預設只允許最新登入的一個客戶端正常使用,不允許多端登入,JWT 就沒辦法做到,因為頒發了新令牌,但是老的令牌在過期前仍然可用。這種情況下,就需要服務端增加相應的邏輯。