淺談JWT

背景知識:

  1. 我們都知道http是無狀態的,因此我們無法通過http來標識用戶,而有些信息或者資源只能給特定的用戶看,例如用戶的信息等等,這時候就出現了會話技術。
  2. 會話:一個會話包含多個http請求,它的作用主要用於這些http請求之間數據的共享。

前面兩篇文章介紹了session技術cookie技術。在上文中我也提到了Session的幾個弊端,例如Session信息存儲在服務器端,當信息量很大的時候會佔據很多服務器端的資源從而影響其他服務的性能,其次Session的擴展性不好,因爲它的信息存儲在服務器上,這就導致了下次請求必須再由該服務器作出響應,降低了分佈式集羣的優勢。本文介紹一種更好的解決方案JWT用戶認證。

JWT全稱是Json Web Token,它由三個部分組成:Header頭部,Payload載荷,Signature簽名。下面分別介紹這三個部分內容。

Header頭部:它一般由兩個部分組成----typ定義類型,值爲JWT;alg定義JWT使用的加密算法。如下:

{
    "typ": "JWT",
    "alg": "HS256"  // 加密算法,默認值,也可以自定義爲其他算法,例如md5等
}

我們需要對其進行Base64編碼,關於Base64編碼和解碼不同的語言API各不相同,java如下:

// 這裏有一個坑:json字符串在寫的時候不要留空格!
String header = "{\"typ\":\"JWT\",\"alg\":\"HS256\"}";
String s = Base64.getEncoder().encodeToString(header.getBytes("UTF-8"));
System.out.println(s); // eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

Payload載荷:它一般由一些官方提供的有具體含義的字段和自定義字段組成。如下:

{
    // 以下是官方提供的一些字段,有特殊的含義但這是非必須的
    "iss": "wxsatellite",  // 一般可以寫JWT簽發者
    "iat": 1652323456,   // 簽發的時間
    "exp": 1652328456,   // JWT過期時間
    "aud": "三斤",  // 接收方
    "sub": "主題"  // JWT的主題


    // 以下是自定義的字段
    "uid": 1
}

同上,java如下:

String payload= "{\"iss\":\"wxsatellite\",\"iat\":1652323456,\"exp\":1652323456,\"aud\":\"三斤\",\"sub\":\"主題\",\"uid\":1}";
String s1 = Base64.getEncoder().encodeToString(payload.getBytes("UTF-8"));

Signature簽名:由Header部分進行Base64編碼得到的字符串拼接上"."再拼接上Payload部分進行Base64編碼得到的字符串,以上三部分拼接得到的字符串再經過Header頭部中定義的alg加密算法得到的字符串就是簽名。需要注意的是:加密的時候需要加上鹽,提高安全性。具體的公式:HMACSHA256( Base64(Header) + "." + Base64(Payload) + "服務端的密碼鹽" )。

得到上面三部分之後,再用點進行拼接,得到的字符串就是JWT了。以下是完整的測試用例:

public class Test {
    public static void main(String[] args) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        // 獲得header
        String header = "{\"typ\":\"JWT\",\"alg\":\"HS256\"}";
        String s = Base64.getEncoder().encodeToString(header.getBytes("UTF-8"));
        
        // 獲得payload
        String payload= "{\"iss\":\"wxsatellite\",\"iat\":1652323456,\"exp\":1652323456,\"aud\":\"三斤\",\"sub\":\"主題\",\"uid\":1}";
        String s1 = Base64.getEncoder().encodeToString(payload.getBytes("UTF-8"));
        
        // 獲得簽名
        MessageDigest instance = MessageDigest.getInstance("SHA-256");
        String slat = "wxsatellite"; // 密碼鹽
        String s3 = s + "." + s1 + slat;
        instance.update(s3.getBytes());
        String signature = Test.byteArrayToHex(instance.digest());
        
        // 獲取jwt
        String jwt = s + "." + s1 + "." + singnature;
    }

    public static String byteArrayToHex(byte[] byteArray) {

        // 首先初始化一個字符數組,用來存放每個16進制字符
        char[] hexDigits = {'0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F' };

        // new一個字符數組,這個就是用來組成結果字符串的(解釋一下:一個byte是八位二進制,也就是2位十六進制字符(2的8次方等於16的2次方))
        char[] resultCharArray =new char[byteArray.length * 2];

        // 遍歷字節數組,通過位運算(位運算效率高),轉換成字符放到字符數組中去
        int index = 0;
        for (byte b : byteArray) {
            resultCharArray[index++] = hexDigits[b>>> 4 & 0xf];
            resultCharArray[index++] = hexDigits[b& 0xf];
        }
        // 字符數組組合成字符串返回
        return new String(resultCharArray);
    }
}

以上大致講述了JWT的三個組成部分和如何使用Java生成JWT。接下來我們來說說幾個注意事項:

1. Base64算法可以輕鬆的加解密,因此嚴格來說它並不是一個加密的過程,而僅僅是一個編碼的過程。

2. 關於Payload這個部分,我們可以存入一些用來維護會話的信息,例如上面的例子中,存儲了uid的值,其實相當於將本來存儲在Session中的信息存儲到了Payload中(主要用於存儲一些不敏感的數據)。

3. 在生成簽名的時候我們增加了密碼鹽,這是因爲JWT字符串是暴露給用戶的,並且生成的格式都是公開的,拿到這個字符串的用戶可以很輕鬆的對頭部和載荷信息進行解碼,然後對信息進行篡改之後再編碼返回給服務器,這就導致了安全性的問題。簽名加鹽的作用就是爲了保證JWT的安全性和完整性,即使惡意用戶修改了Header和PayLoad,並針對修改後的Header和Payload再生成簽名拼接成JWT返回給服務器時,由於惡意用戶不知道鹽,服務器很容易可以判斷出JWT是否合法。(根據用戶傳遞的Header部分和Payload部分,加上密碼鹽生成Signature簽名和用戶傳遞的Signature簽名進行比較,不想等則不合法。)

4. 相比於Session,JWT一個明顯的優勢就是將用戶的登陸狀態分散到了客戶端,而不像Session將用戶的登陸信息存儲在了服務器端,因此它不會導致一個問題:當用戶基數變大時會大量佔用服務器端的資源。

瞭解了什麼JWT以及它的一些注意事項,那麼JWT如何在Web應用程序中使用呢,我簡單畫了一個草圖,供大家參考:

今天的分享就結束啦,感謝閱讀咯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章