JWT簡介:從Session到Token的轉變

ordinary_brony 發表於 2020-11-21

導讀

JWT,全稱Json Web Token。我們平常所說的token實際上就是說JWT技術中的T

SessionToken的對比

如果你對jspServlet非常熟悉,你應該對Session非常熟悉。每當使用者登入的時候都會使用CookieSession聯動起來,服務端使用session.setAttribute()方法完整地儲存使用者的資訊,並使用session.getAttribute()方法完整地獲取使用者的資訊。聽起來非常方便?

可是呢,如果你讀過我之前寫的Session工作原理,你會發現:Session對於伺服器的壓力還是相當大的。如果專案突然迎來了海量資料和大量併發,伺服器當機的概率大大增加,對於已經落地的專案甚至會產生不可逆轉的錯誤。

於是,使用者線上時就每時每刻維護一個Session的方法逐漸被摒棄;進而選擇了使用者每次請求的時候傳送使用者資訊,順便檢查許可權。一舉兩得,豈不美哉?JWT就應運而生了。在分散式場景中,僅在請求時伺服器產生少許壓力的token也就比始終給伺服器施加壓力並無法釋放的Session更為實用。

Token的結構

Token本質上是一串字串,其中包含三個部分,三個部分分別是頭部負載簽名,每個部分由英文句號.來分開。

為什麼這麼做?因為這個字串可以直接作為使用者的標識,一旦被其他人截獲,便能夠直接作為請求使用。所以,我們往往會將這三個部分使用Base64演算法進行編碼,請求傳送到伺服器之後再將資訊使用Base64演算法進行解碼。

頭部

頭部主要是包含兩段資訊:資料型別簽名演算法

資料型別一般是選擇JWT作為資料型別,不同於一般的Json又不同於普通的字串;

簽名演算法一般是選擇HS256,這也是官方推薦的演算法。

負載

第二個部分則是負載,也作有效負載。其中包含了一些有必要的資料宣告。比如,我們可以將使用者id和使用者許可權碼等等非常重要的資訊。

但是呢,如果你傳送的資訊真的被截獲了,還是會被很簡單的解碼獲得了資訊,所以只能在這部分只能儘可能不要放一些敏感資訊,否則會被盜號。

同樣的,這部分也會使用Base64演算法編碼。

簽名

簽名就像是交付的時候有個安全員通過唯一指定的識別器來確認操作的使用者確實是你,而不是其他的什麼人。

所以簽名這部分就是頭部和負載的結合,再使用一個隨機鹽再次編碼。每當使用者傳送請求的時候,伺服器都會首先使用第一部分和第二部分使用只有伺服器知道的隨機鹽加密,再和第三部分比對。加密後的資訊和第三部分是相同的,那麼就是正確的,本次識別器確認了使用者資訊,否則就是非法登入。

這個演算法從邏輯上能夠在一定程度上保證請求資訊不會被其他使用者非法使用。但是呢,這個演算法還是不能夠保證別有用心的人修改你的個人隱私。這就很無奈了。

一個小例子

這個小例子雖然用到了SpringBoot,但是隻使用了框架內的測試功能,單純地使用命令列輸出所有的結果。

由於我們並沒有用到SpringBoot自帶的測試類,所以就只是單純地在{專案根目錄}/src/main/test/{自定義包名}下新建JWTTest.java,並直接使用JWT工具類:

@Test
public void testJWT() {
  // set expire time
  Calendar calendar = Calendar.getInstance();
  calendar.add(Calendar.SECOND, 90);
  // create jwt
  HashMap<String, Object> headers = new HashMap<>();
  headers.put("type", "JSON");
  String token = JWT.create()
                    .withHeader(headers)
                    .withClaim("user", "sakebow")
                    .withClaim("id", "20202104126")
                    .withExpiresAt(calendar.getTime())
                    .sign(Algorithm.HMAC256("[email protected]#sakebow?!~"));
  System.out.println(token);
}

這段程式碼執行之後就會生成一長串程式碼:

eyJ0eXAiOiJKV1QiLCJ0eXBlIjoiSlNPTiIsImFsZyI6IkhTMjU2In0.eyJpZCI6IjIwMjAyMTA0MTI2IiwiZXhwIjoxNjAwNjAwNjM1LCJ1c2VyIjoic2FrZWJvdyJ9.xigradwatkwZYO1QZJ98_sa6qH9L-_8uYyi5_HUSRjE

看起來毫無章法?不過沒有關係,我們還能解碼。

@Test
public void testDecodeJWT() {
  String token =  "eyJ0eXAiOiJKV1QiLCJ0eXBlIjoiSlNPTiIsImFsZyI6IkhTMjU2In0.eyJpZCI6IjIwMjAyMTA0MTI2IiwiZXhwIjoxNjAwNjAwNjM1LCJ1c2VyIjoic2FrZWJvdyJ9.xigradwatkwZYO1QZJ98_sa6qH9L-_8uYyi5_HUSRjE";
  JWTVerifier jwtVerifier = JWT
          .require(Algorithm.HMAC256("[email protected]#sakebow?!~"))
          .build();
  DecodedJWT decodedJWT = jwtVerifier.verify(token);
  System.out.println(decodedJWT.getHeader() + ", " + decodedJWT.getPayload());
}

JWTVerifier除了解碼以外,還會核對前面兩個部分合在一起加密是否與最後一段相等。同時,JWT還會經常變化,這也正為JWT提供了一定的安全性。

隨機鹽

不知道你們注意到沒有,在JWT加密中有一段程式碼:

sign(Algorithm.HMAC256("[email protected]#sakebow?!~"));

實際上這個[email protected]#sakebow?!~就是我們自己規定的隨機鹽,也就是根據這個片段對我們傳送的資料進行加密。因為這個片段基本上只有開發著自己知道,所以安全性還是可以保證的。但是,安全性可不僅限於使用者自己使用。

如果說其他別有用心的使用者拿到了這個資料,偽造了其他人的資訊,傳送了非法請求,到了伺服器結果就會被當成已經授權的人,然後回應請求。這將會讓整個程式會變得非常危險。所以,這段隨機鹽決不可以儲存在客戶端

是不是有點能理解了呢?