『JWT』有人讓你趕快用它,有人勸你放棄它html
古時的風箏第 89 篇原創文章 前端
做者 | 風箏
公衆號:古時的風箏(ID:gushidefengzheng)
轉載請聯繫受權,掃碼文末二維碼加微信java
JWT 全稱是 JSON Web Token,是目前很是流行的跨域認證解決方案,在單點登陸場景中常用到。web
有些人以爲它很是好用,用了它以後就不用在服務端藉助 redis 實現認證過程了,可是,還有一部分人認爲它生來就有缺陷,根本不能用。redis
這是爲何呢?算法
你平時用過那麼多網站和 APP,其中有不少都是須要登陸的吧,那我們就選一個場景出來講說。數據庫
以一個電商系統爲例,若是你想要下單,首先須要註冊一個帳號,擁有了帳號以後,須要輸入用戶名(好比手機號或郵箱)、密碼完成登陸過程。以後你在一段時間內再次進入系統,是不須要輸入用戶名和密碼的,只有在連續長時間不登陸的狀況下(例如一個月沒登陸過)訪問系統,才須要再次輸入用戶名和密碼。跨域
對於那些使用頻率很高的網站或應用,一般是很長時間都不須要輸入密碼的,以致於你在換了一臺電腦或者一部手機以後,一些常用的網站或 APP 的密碼都不記得了。瀏覽器
早期互聯網以 web 爲主,客戶端是瀏覽器 ,因此 Cookie-Session 方式是早期最經常使用的認證方式,直到如今,一些 web 網站依然用這種方式作認證。安全
二、退一萬步說,你作的產品只支持 web,也要考慮跨域問題, 但Cookie 是不能跨域的。拿天貓商城來講,當你進入天貓商城後,會看到頂部有天貓超市、天貓國際、天貓會員這些菜單。而點擊這些菜單都會進入不一樣的域名,不一樣的域名下的 cookie 都是不同的,你在 A 域名下是沒辦法拿到 B 域名的 cookie 的,即便是子域也不行。
image-20200706173939291
三、若是是分佈式服務,須要考慮 Session 同步問題。如今的互聯網網站和 APP 基本上都是分佈式部署,也就是服務端不止一臺機器。當某個用戶在頁面上進行登陸操做後,這個登陸動做一定是請求到了其中某一臺服務器上。你的身份信息得保存下來吧,傳統方式就是存 Session。
接下來,問題來了。你訪問了幾個頁面,這時,有個請求通過負載均衡,路由到了另一臺服務器(不是你登陸的那臺)。當後臺接到請求後,要檢查用戶身份信息和權限,因而接口開始從從 Session 中獲取用戶信息。可是,這臺服務器不是當時登陸的那臺,並沒存你的 Session ,這樣後臺服務就認爲你是一個非登陸的用戶,也就不能給你返回數據了。
因此,爲了不這種狀況的發生,就要作 Session 同步。一臺服務器接收到登陸請求後,在當前服務器保存 Session 後,也要向其餘幾個服務器同步。
四、cookie 存在 CSRF(跨站請求僞造)的風險。跨站請求僞造,是一種挾制用戶在當前已登陸的Web應用程序上執行非本意的操做的***方法。CSRF 利用的是網站對用戶網頁瀏覽器的信任。簡單地說,是***者經過一些技術手段欺騙用戶的瀏覽器去訪問一個本身曾經認證過的網站並運行一些操做(好比購買商品)。因爲瀏覽器曾經認證過,因此被訪問的網站會認爲是真正的用戶發起的操做。好比說我是一個***,我發現你常常訪問的一個技術網站存在 CSRF 漏洞。發佈文章支持 html 格式,進而我在 html 中加入一些危險內容,例如
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
假設 src 指向的地址是一個你平時用的購物網站的付款地址(固然只是舉例,真正的***可沒這麼簡單),若是你以前登陸過而且標識你身份信息的 cookie 已經保存下來了。當你刷到我發佈的這篇文章的時候,img 標籤一加載,這個 CSRF ***就會起做用,在你不知情的狀況下向這個網站付款了。
因爲傳統的 Cookie-Session 認證存在諸多問題,那能夠把上面的方案改造一下。一、改造 Cookie 既然 Cookie 不能在 APP 等非瀏覽器中使用,那就不用 cookie 作客戶端存儲,改用其餘方式。改爲什麼呢?web 中可使用 local storage,APP 中使用客戶端數據庫,這樣既能這樣就實現了跨域,而且避免了 CSRF 。
二、服務端也不存 Session 了,把 Session 信息拿出來存到 Redis 等內存數據庫中,這樣即提升了速度,又避免了 Session 同步問題;
通過改造以後變成了以下的認證過程:
下面兩張圖分別演示了首次登陸和非首次登陸的過程。
通過一頓猛如虎的改造,解決了傳統 Cookie-Session 方式存在的問題。這種改造須要開發者在項目中自行完成。改造起來確定是費時費力的,並且還有可能存在漏洞。
這時,JWT 就能夠上場了,JWT 就是一種Cookie-Session改造版的具體實現,讓你省去本身造輪子的時間,JWT 還有個好處,那就是你能夠不用在服務端存儲認證信息(好比 token),徹底由客戶端提供,服務端只要根據 JWT 自身提供的解密算法就能夠驗證用戶合法性,並且這個過程是安全的。
若是你是剛接觸 JWT,最有疑問的一點可能就是:JWT 爲何能夠徹底依靠客戶端(好比瀏覽器端)就能實現認證功能,認證信息全都存在客戶端,怎麼保證安全性?
JWT 最後的形式就是個字符串,它由頭部、載荷與簽名這三部分組成,中間以「.」分隔。像下面這樣:
997EDE1C-5689-4C3F-98E8-25C25BBEC3FC
頭部以 JSON 格式表示,用於指明令牌類型和加密算法。形式以下,表示使用 JWT 格式,加密算法採用 HS256,這是最經常使用的算法,除此以外還有不少其餘的。
{ "alg": "HS256", "typ": "JWT" }
對應上圖的紅色 header 部分,須要 Base64 編碼。
用來存儲服務器須要的數據,好比用戶信息,例如姓名、性別、年齡等,要注意的是重要的機密信息最好不要放到這裏,好比密碼等。
{ "name": "古時的風箏", "introduce": "英俊瀟灑" }
另外,JWT 還規定了 7 個字段供開發者選用。
簽名有一個計算公式。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), Secret )
使用HMACSHA256算法計算得出,這個方法有兩個參數,前一個參數是 (base64 編碼的頭部 + base64 編碼的載荷)用點號相連,後一個參數是自定義的字符串密鑰,密鑰不要暴露在客戶端,僅應該服務器知道。
瞭解了 JWT 的結構和算法後,那怎麼使用呢?假設我這兒有個網站。
一、在用戶登陸網站的時候,須要輸入用戶名、密碼或者短信驗證的方式登陸,登陸請求到達服務端的時候,服務端對帳號、密碼進行驗證,而後計算出 JWT 字符串,返回給客戶端。
二、客戶端拿到這個 JWT 字符串後,存儲到 cookie 或者 瀏覽器的 LocalStorage 中。
三、再次發送請求,好比請求用戶設置頁面的時候,在 HTTP 請求頭中加入 JWT 字符串,或者直接放到請求主體中。
四、服務端拿到這串 JWT 字符串後,使用 base64的頭部和 base64 的載荷部分,經過HMACSHA256算法計算簽名部分,比較計算結果和傳來的簽名部分是否一致,若是一致,說明這次請求沒有問題,若是不一致,說明請求過時或者是非法請求。
保證安全性的關鍵就是 HMACSHA256 或者與它同類型的加密算法,由於加密過程是不可逆的,因此不能根據傳到前端的 JWT 傳反解到密鑰信息。
另外,不一樣的頭部和載荷加密以後獲得的簽名都是不一樣的,因此,若是有人改了載荷部分的信息,那最後加密出的結果確定就和改以前的不同的,因此,最後驗證的結果就是不合法的請求。
假設載荷部分存儲了權限級別相關的字段,強盜拿到 JWT 串後想要修改成更高權限的級別,上面剛說了,這種狀況下是確定不會得逞的,由於加密出來的簽名會不同,服務器可能很容易的判別出來。
那若是強盜拿到後不作更改,直接用呢,那就沒有辦法了,爲了更大程度上防止被強盜盜取,應該使用 HTTPS 協議而不是 HTTP 協議,這樣能夠有效的防止一些中間劫持***行爲。
有同窗就要說了,這一點也不安全啊,拿到 JWT 串就能夠輕鬆模擬請求了。確實是這樣,可是前提是你怎麼樣能拿到,除了上面說的中間劫持外,還有什麼辦法嗎?
除非強盜直接拿了你的電腦,那這樣的話,對不起,不光 JWT 不安全了,其餘任何網站,任何認證方式都不安全。
雖然這樣的狀況不多,可是在使用 JWT 的時候仍然要注意合理的設置過時時間,不要太長。
JWT 有個問題,致使不少開發團隊放棄使用它,那就是一旦頒發一個 JWT 令牌,服務端就沒辦法廢棄掉它,除非等到它自身過時。有不少應用默認只容許最新登陸的一個客戶端正常使用,不容許多端登陸,JWT 就沒辦法作到,由於頒發了新令牌,可是老的令牌在過時前仍然可用。這種狀況下,就須要服務端增長相應的邏輯。
JWT 官網列出了各類語言對應的庫,其中 Java 的以下幾個。
image-20200817112359199
以 java-jwt爲例。
一、引入對應的 Maven 包。
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
二、在登陸時,調用 create 方法獲得一個令牌,並返回給前端。
public static String create(){ try { Algorithm algorithm = Algorithm.HMAC256("secret"); String token = JWT.create() .withIssuer("auth0") .withSubject("subject") .withClaim("name","古時的風箏") .withClaim("introduce","英俊瀟灑") .sign(algorithm); System.out.println(token); return token; } catch (JWTCreationException exception){ //Invalid Signing configuration / Couldn't convert Claims. throw exception; } }
三、登陸成功後,再次發起請求的時候將 token 放到 header 或者請求體中,服務端對 token 進行驗證。
public static Boolean verify(String token){ try { Algorithm algorithm = Algorithm.HMAC256("secret"); JWTVerifier verifier = JWT.require(algorithm) .withIssuer("auth0") .build(); //Reusable verifier instance DecodedJWT jwt = verifier.verify(token); String payload = jwt.getPayload(); String name = jwt.getClaim("name").asString(); String introduce = jwt.getClaim("introduce").asString(); System.out.println(payload); System.out.println(name); System.out.println(introduce); return true; } catch (JWTVerificationException exception){ //Invalid signature/claims return false; } }
四、用 create 方法生成 token,並用 verify 方法驗證一下。
public static void main(String[] args){
String token = create();
Boolean result = verify(token);
System.out.println(result);
}
獲得下面的結果
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiaW50cm9kdWNlIjoi6Iux5L-K5r2H5rSSIiwiaXNzIjoiYXV0aDAiLCJuYW1lIjoi5Y-k5pe255qE6aOO562dIn0.ooQ1K_XyljjHf34Nv5iJvg1MQgVe6jlphxv4eeFt8pA eyJzdWIiOiJzdWJqZWN0IiwiaW50cm9kdWNlIjoi6Iux5L-K5r2H5rSSIiwiaXNzIjoiYXV0aDAiLCJuYW1lIjoi5Y-k5pe255qE6aOO562dIn0 古時的風箏 英俊瀟灑 true
使用 create 方法建立的 JWT 串能夠經過驗證。
而若是我將 JWT 串中的載荷部分,兩個點號中間的部分修改一下,而後再調用 verify 方法驗證,會出現 JWTVerificationException 異常,不能經過驗證。
「若是你以爲還不錯,歡迎點贊、在看」
還能夠讀:
跟我極速嚐鮮 Spring Boot 2.3
Lambda、函數式接口、Stream 一次性全給你
Java 開發者,全棧工程師。堅持原創乾貨輸出,你可選擇如今就關注我,或者看看歷史文章再關注也不遲。長按二維碼關注,跟我一塊兒變優秀!
技術交流還能夠加羣或者直接加我微信。