我是風箏,公衆號「古時的風箏」,一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農! 文章會收錄在 JavaNewBee 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裏面。html
JWT 全稱是 JSON Web Token,是目前很是流行的跨域認證解決方案,在單點登陸場景中常用到。前端
有些人以爲它很是好用,用了它以後就不用在服務端藉助 redis 實現認證過程了,可是,還有一部分人認爲它生來就有缺陷,根本不能用。java
這是爲何呢?git
傳統的認證方式
從一個登陸場景提及
你平時用過那麼多網站和 APP,其中有不少都是須要登陸的吧,那我們就選一個場景出來講說。程序員
以一個電商系統爲例,若是你想要下單,首先須要註冊一個帳號,擁有了帳號以後,須要輸入用戶名(好比手機號或郵箱)、密碼完成登陸過程。以後你在一段時間內再次進入系統,是不須要輸入用戶名和密碼的,只有在連續長時間不登陸的狀況下(例如一個月沒登陸過)訪問系統,才須要再次輸入用戶名和密碼。github
對於那些使用頻率很高的網站或應用,一般是很長時間都不須要輸入密碼的,以致於你在換了一臺電腦或者一部手機以後,一些常用的網站或 APP 的密碼都不記得了。web
早期的 Cookie-Session 認證方式
早期互聯網以 web 爲主,客戶端是瀏覽器 ,因此 Cookie-Session 方式是早期最經常使用的認證方式,直到如今,一些 web 網站依然用這種方式作認證。redis
認證過程大體以下:算法
- 用戶輸入用戶名、密碼或者用短信驗證碼方式登陸系統;
- 服務端驗證後,建立一個 Session 信息,而且將 SessionID 存到 cookie,發送回瀏覽器;
- 下次客戶端再發起請求,自動帶上 cookie 信息,服務端經過 cookie 獲取 Session 信息進行校驗;
可是爲何說它是傳統的認證方式,由於如今人手一部智能手機,不少人都不用電腦,平時都是使用手機上的各類 APP,好比淘寶、拼多多等。 在這種潮流之下,傳統的 Cookie-Session 就遇到了一些問題: 一、首先,Cookie-Session 只能在 web 場景下使用,若是是 APP 呢,APP 可沒有地方存 cookie。 如今的產品基本上都同時提供 web 端和 APP 兩種使用方式,有點產品甚至只有 APP。數據庫
二、退一萬步說,你作的產品只支持 web,也要考慮跨域問題, 但Cookie 是不能跨域的。 拿天貓商城來講,當你進入天貓商城後,會看到頂部有天貓超市、天貓國際、天貓會員這些菜單。而點擊這些菜單都會進入不一樣的域名,不一樣的域名下的 cookie 都是不同的,你在 A 域名下是沒辦法拿到 B 域名的 cookie 的,即便是子域也不行。
三、若是是分佈式服務,須要考慮 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-Session 認證存在諸多問題,那能夠把上面的方案改造一下。 一、改造 Cookie 既然 Cookie 不能在 APP 等非瀏覽器中使用,那就不用 cookie 作客戶端存儲,改用其餘方式。 改爲什麼呢? web 中可使用 local storage,APP 中使用客戶端數據庫,這樣既能這樣就實現了跨域,而且避免了 CSRF 。
二、服務端也不存 Session 了,把 Session 信息拿出來存到 Redis 等內存數據庫中,這樣即提升了速度,又避免了 Session 同步問題;
通過改造以後變成了以下的認證過程:
- 用戶輸入用戶名、密碼或者用短信驗證碼方式登陸系統;
- 服務端通過驗證,將認證信息構造好的數據結構存儲到 Redis 中,並將 key 值返回給客戶端;
- 客戶端拿到返回的 key,存儲到 local storage 或本地數據庫;
- 下次客戶端再次請求,把 key 值附加到 header 或者 請求體中;
- 服務端根據獲取的 key,到 Redis 中獲取認證信息;
下面兩張圖分別演示了首次登陸和非首次登陸的過程。
通過一頓猛如虎的改造,解決了傳統 Cookie-Session 方式存在的問題。這種改造須要開發者在項目中自行完成。改造起來確定是費時費力的,並且還有可能存在漏洞。
JWT 出場
這時,JWT 就能夠上場了,JWT 就是一種Cookie-Session改造版的具體實現,讓你省去本身造輪子的時間,JWT 還有個好處,那就是你能夠不用在服務端存儲認證信息(好比 token),徹底由客戶端提供,服務端只要根據 JWT 自身提供的解密算法就能夠驗證用戶合法性,並且這個過程是安全的。
若是你是剛接觸 JWT,最有疑問的一點可能就是: JWT 爲何能夠徹底依靠客戶端(好比瀏覽器端)就能實現認證功能,認證信息全都存在客戶端,怎麼保證安全性?
JWT 數據結構
JWT 最後的形式就是個字符串,它由頭部、載荷與簽名這三部分組成,中間以「.」分隔。像下面這樣:
頭部
頭部以 JSON 格式表示,用於指明令牌類型和加密算法。形式以下,表示使用 JWT 格式,加密算法採用 HS256,這是最經常使用的算法,除此以外還有不少其餘的。
{ "alg": "HS256", "typ": "JWT" }
對應上圖的紅色 header 部分,須要 Base64 編碼。
載荷
用來存儲服務器須要的數據,好比用戶信息,例如姓名、性別、年齡等,要注意的是重要的機密信息最好不要放到這裏,好比密碼等。
{ "name": "古時的風箏", "introduce": "英俊瀟灑" }
另外,JWT 還規定了 7 個字段供開發者選用。
- iss (issuer):簽發人
- exp (expiration time):過時時間
- sub (subject):主題
- aud (audience):受衆
- nbf (Not Before):生效時間
- iat (Issued At):簽發時間
- jti (JWT ID):編號
這部分信息也是要用 Base64 編碼的。
簽名
簽名有一個計算公式。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), Secret )
使用HMACSHA256
算法計算得出,這個方法有兩個參數,前一個參數是 (base64 編碼的頭部 + base64 編碼的載荷)用點號相連,後一個參數是自定義的字符串密鑰,密鑰不要暴露在客戶端,近應該服務器知道。
使用方式
瞭解了 JWT 的結構和算法後,那怎麼使用呢?假設我這兒有個網站。
一、在用戶登陸網站的時候,須要輸入用戶名、密碼或者短信驗證的方式登陸,登陸請求到達服務端的時候,服務端對帳號、密碼進行驗證,而後計算出 JWT 字符串,返回給客戶端。
二、客戶端拿到這個 JWT 字符串後,存儲到 cookie 或者 瀏覽器的 LocalStorage 中。
三、再次發送請求,好比請求用戶設置頁面的時候,在 HTTP 請求頭中加入 JWT 字符串,或者直接放到請求主體中。
四、服務端拿到這串 JWT 字符串後,使用 base64的頭部和 base64 的載荷部分,經過HMACSHA256
算法計算簽名部分,比較計算結果和傳來的簽名部分是否一致,若是一致,說明這次請求沒有問題,若是不一致,說明請求過時或者是非法請求。
怎麼保證安全性的
保證安全性的關鍵就是 HMACSHA256
或者與它同類型的加密算法,由於加密過程是不可逆的,因此不能根據傳到前端的 JWT 傳反解到密鑰信息。
另外,不一樣的頭部和載荷加密以後獲得的簽名都是不一樣的,因此,若是有人改了載荷部分的信息,那最後加密出的結果確定就和改以前的不同的,因此,最後驗證的結果就是不合法的請求。
別人拿到完整 JWT 還安全嗎
假設載荷部分存儲了權限級別相關的字段,強盜拿到 JWT 串後想要修改成更高權限的級別,上面剛說了,這種狀況下是確定不會得逞的,由於加密出來的簽名會不同,服務器可能很容易的判別出來。
那若是強盜拿到後不作更改,直接用呢,那就沒有辦法了,爲了更大程度上防止被強盜盜取,應該使用 HTTPS 協議而不是 HTTP 協議,這樣能夠有效的防止一些中間劫持攻擊行爲。
有同窗就要說了,這一點也不安全啊,拿到 JWT 串就能夠輕鬆模擬請求了。確實是這樣,可是前提是你怎麼樣能拿到,除了上面說的中間劫持外,還有什麼辦法嗎?
除非強盜直接拿了你的電腦,那這樣的話,對不起,不光 JWT 不安全了,其餘任何網站,任何認證方式都不安全。
雖然這樣的狀況不多,可是在使用 JWT 的時候仍然要注意合理的設置過時時間,不要太長。
一個問題
JWT 有個問題,致使不少開發團隊放棄使用它,那就是一旦頒發一個 JWT 令牌,服務端就沒辦法廢棄掉它,除非等到它自身過時。有不少應用默認只容許最新登陸的一個客戶端正常使用,不容許多端登陸,JWT 就沒辦法作到,由於頒發了新令牌,可是老的令牌在過時前仍然可用。這種狀況下,就須要服務端增長相應的邏輯。
經常使用的 JWT 庫
JWT 官網列出了各類語言對應的庫,其中 Java 的以下幾個。
以 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
異常,不能經過驗證。
壯士且慢,先給點個贊吧,老是被白嫖,身體吃不消!
公衆號「古時的風箏」,Java 開發者,全棧工程師,bug 殺手,擅長解決問題。 一個兼具深度與廣度的程序員鼓勵師,本打算寫詩卻寫起了代碼的田園碼農!堅持原創乾貨輸出,你可選擇如今就關注我,或者看看歷史文章再關注也不遲。長按二維碼關注,跟我一塊兒變優秀!