PAT(乙級)2019年秋季考試 7-4 天長地久 (20分)
阿新 • • 發佈:2020-12-02
- 什麼是jwt
- jsonwebtoken (JWT)是一個開放標準(rfc7519).它定義了一種緊湊的,自包含式的方式,用於在各方之間以JSON物件安全地傳輸資訊,此資訊可以驗證和信任,因為他是數字簽名的,jwt可以使用祕密(使用HMAC演算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名
- 通俗解釋
- JWT簡稱JSON Web Token ,也就是通過json形式作為web應用的令牌,用在各方之間安全的將資訊作為json物件傳輸,在資料傳輸過程中還可以完成資料加密,簽名相關處理
- JWT能做什麼?
- 授權
- 這是使用jwt的最常見方案,一旦使用者登入,每個後續請求將包括JWT,從而允許使用者訪問該領牌允許的路由,服務和資源,單點登入是的當今廣泛使用JWT的一項功能,因為他的開銷很小,並且可以在不同的域中使用
- 這是使用jwt的最常見方案,一旦使用者登入,每個後續請求將包括JWT,從而允許使用者訪問該領牌允許的路由,服務和資源,單點登入是的當今廣泛使用JWT的一項功能,因為他的開銷很小,並且可以在不同的域中使用
- 資訊交換
- JSON Web Toekn 是在各方面之間安全地傳輸資訊的好方法,因為可以對JWT進行簽名(例如,使用公鑰/私鑰對),所以您可以確保發件人是他們所說的人,此外由於簽名是使用標頭和有效負載計算的,因此您還可以驗證內容是否遭到篡改
- 授權
- 為什麼是JWT?
- 基於傳統的session認證
- 認證方式
- 我們知道,http協議本身是一種無狀態協議,而這就意味著使用者向我們的應用提供了使用者名稱和密碼來進行使用者驗證,那麼下一次請求時,使用者還需要再一次進行使用者認證才行,因為根據http協議,我們並不知道是哪個使用者發出的請求,所以為了讓我們的使用者知道是哪一個使用者傳送的請求,我們只能在伺服器儲存一份使用者登入的資訊,這份登入資訊會在響應資訊時傳遞給瀏覽器,告訴其儲存為cookie以便下次請求是傳送給我們的應用,這樣我們的應用就能識別請求來自於那個使用者了,這就是傳統的基於session認證
- 我們知道,http協議本身是一種無狀態協議,而這就意味著使用者向我們的應用提供了使用者名稱和密碼來進行使用者驗證,那麼下一次請求時,使用者還需要再一次進行使用者認證才行,因為根據http協議,我們並不知道是哪個使用者發出的請求,所以為了讓我們的使用者知道是哪一個使用者傳送的請求,我們只能在伺服器儲存一份使用者登入的資訊,這份登入資訊會在響應資訊時傳遞給瀏覽器,告訴其儲存為cookie以便下次請求是傳送給我們的應用,這樣我們的應用就能識別請求來自於那個使用者了,這就是傳統的基於session認證
- 認證流程
- 暴露問題
- 每個使用者經過我們的應用認證之後,我們的應用都要在伺服器端做一次記錄,以便使用者下次請求的鑑別,通常而言session都是儲存在記憶體中,而隨著認證使用者的增多,服務端的開銷就會明顯增大
- 使用者認證之後,服務端就做認證記錄,如果認證的記錄被儲存到記憶體中的話,這意味著使用者下次請求還必須要請求在這臺伺服器上面,這樣才可以拿到授權的資源,這樣在分散式的應用上,相應的限制了負載均衡器的能力,這也意味著限制了應用的擴充套件能力
- 因為是基於cookie來進行使用者識別的,cookie如果被獲取,使用者就很容易受到跨站請求偽造的攻擊
- 在前後端分離系統中就更加痛苦了,如下圖所示:
- 也就是說前後端分離在應用解耦後增加了部署的複雜性,通常使用者,一次請求要轉發多次,如果使用session 每次攜帶sessionid 到伺服器,伺服器還要查詢使用者資訊,同事如果使用者很多,這些資訊儲存在伺服器記憶體中,給伺服器增加負擔,還有就是CSRF(跨站偽造請求攻擊),session是基於cookie進行使用者識別的,cookie如果被截獲使用者容易受到跨站請求偽造的攻擊,還有就是sessionid就是一個特徵值,表達的資訊不夠豐富,不容易擴充套件,而且如果你後端應用是多個節點部署,那麼就需要實現session共享機制,不方便叢集
- 認證方式
- 基於JWT認證
- 認證流程
- 首先,前端通過web表單將自己的使用者名稱和密碼傳送到後端介面,這一過程一般是一個HTTP,POST請求,建議的方式是通過SSL加密的傳輸(https協議),從而避免敏感資訊被嗅探
- 後端核對使用者名稱和密碼 成功後,將使用者的id等其他資訊最為JWT Payload(負載),將其與頭部分別進行Base64編碼拼接後簽名,形成一個JWT(token),形成的JWT就是一個形同111.zzz.xxx的字串
- 後端將JWT字串作為登入成功的返回結果給前端。前端可以將返回的結果儲存在localStorage或sessionStorage上,退出登入是前端刪除儲存的JWT即可
- 前端將每次請求的JWT都放入放入HTTP Header 中的Authorization為(解決XSS和XSRF問題)
- 後端檢查是否存在,如存在檢查JWT的有效性。例如,檢查簽名是否正確;檢查Token是否過期,檢查Token的接收方收否是自己(可選)
- 驗證通過後後端使用JWT中包含的使用者資訊進行其他的邏輯操作,返回相應的結果
- jwt優勢
- 簡介(Compact):可以通過URL,POST引數或者在HTTP header中傳送,因為資料量小,所以傳輸的速度也快
- 自包含(Self-contained): 負載中包含了所有使用者所需的資訊,避免了多次查詢資料庫
- 因為Token是以JSON加密的形式儲存在客戶端的,所以JWT是跨語言的,原則上任何Web形式都支援
- 不要再服務端儲存資訊,特別適用於分散式微服務
- 認證流程
- 基於傳統的session認證
- JWT的結構是什麼?
- 令牌的組成
- 有效的標頭(Header)
- 有效負載(Payload)
- 簽名(Signature)
- 因此JWT通常如下所示 xxx.yyy.zzz, Header.Payload.Signature
- Header
- 通常由兩部分組成:令牌的型別(即JWT)和所使用的簽名演算法,例如HMAC,SHA265或RSA,它會使用Base64編碼組成JWT結構的一部分
- 注意: Base64是一種編碼,也就是說,他是可以被翻譯回原來的樣子的,他並不是一種加密的過程
- { "alg":"HS365", "typ":"JWT"}
- PayLoad
- 令牌的第二部分是有效負載,其中包含宣告,宣告有關實體(通常是指使用者)和其他資料的宣告。同樣的他會使用Base64編碼組成JWT結構的第二部分
- {"sub":"12334798", "name":"John Doe", "admin":true}
- Signature
- 前面兩部分都是使用Base64進行編碼的,即前端可以解開知道里面的資訊,Signature需要使用編碼後的Header和Payload以及我們提供的一個金鑰,然後使用Header中指定的簽名演算法(HS256)進行簽名,沒有被篡改過:
- 如:
- HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payLoad).secret)
- # 簽名的目的
- 最後一步簽名的過程,實際上是對頭部以及負載內容進行簽名,防止內容被篡改,如果有人對頭部以及負載的內容解碼之後進行修改,再進行編碼,最後加上之前的簽名組合成新的JWT的話,那麼伺服器會判斷出新的頭部和負載形成的簽名和JWT附帶上的簽名是不一樣的,如果要對新的頭部和負載進行簽名,在不知道伺服器加密時用的金鑰的話,得出來的簽名也是不一樣的。
- 最後一步簽名的過程,實際上是對頭部以及負載內容進行簽名,防止內容被篡改,如果有人對頭部以及負載的內容解碼之後進行修改,再進行編碼,最後加上之前的簽名組合成新的JWT的話,那麼伺服器會判斷出新的頭部和負載形成的簽名和JWT附帶上的簽名是不一樣的,如果要對新的頭部和負載進行簽名,在不知道伺服器加密時用的金鑰的話,得出來的簽名也是不一樣的。
- # 資訊保安的問題
- 在這裡大家一定會問一個問題,:Base64是一種編碼,是可逆的,那麼我的資訊不就暴露了嘛?
- 是的,所以,在JWT中不應該在負載裡面加入任何敏感的資料,在上面的例子中,我們傳輸的是使用者的User ID,這個值實際上不是什麼敏感內容,一般情況下被知道也是安全的,到那時像密碼這樣的內容就不可以放在JWT中了,如果將使用者的密碼放在了JWT中,那麼懷有惡意的第三方通過Base64解碼就能很快地知道你的密碼了,因此JWT適合用於向Web應用傳遞一些非敏感資訊,JWT還經常用於設計使用者認證和授權系統,甚至實現Web應用的單點登入
- 在這裡大家一定會問一個問題,:Base64是一種編碼,是可逆的,那麼我的資訊不就暴露了嘛?
- 令牌的組成
- 使用JWT
- #先引入依賴
1 <!--引入jwt--> 2 <dependency> 3 <groupId>com.auth0</groupId> 4 <artifactId>java-jwt</artifactId> 5 <version>3.4.0</version> 6 </dependency>
- 生成token
1 Calendar instance = Calendar.getInstance(); 2 Calendar instance = Calendar.getInstance(); 3 instance.add(Calendar.SECOND, 60); 4 String token = JWT.create() 5 .withHeader(map) 6 .withClaim("userId", 60) 7 .withClaim("username", "xiao") 8 .withExpiresAt(instance.getTime()) 9 .sign(Algorithm.HMAC256("!@Qw#$R"));// 簽名System.out.println(token); 10 生成結果 11 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDY4OTY1MzIsInVzZXJJZCI6NjAsInVzZXJuYW1lIjoieGlhbyJ9.eblcFVkLQuE-TQTuWgqYpI-GOLOZthZ4_fZBB6XR2nA
- 根據令牌和簽名解析資料
1 JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!@Qw#$R")).build(); 2 DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDY4OTY1MzIsInVzZXJJZCI6NjAsInVzZXJuYW1lIjoieGlhbyJ9.eblcFVkLQuE-TQTuWgqYpI-GOLOZthZ4_fZBB6XR2nA");
3 System.out.println(verify.getClaim("userId").asInt());
4 System.out.println(verify.getClaim("username").asString()); 5 System.out.println(verify.getExpiresAt()); - 封裝成工具類
1 package com.example.utils; 2 import com.auth0.jwt.JWT; 3 import com.auth0.jwt.JWTCreator; 4 import com.auth0.jwt.algorithms.Algorithm; 5 import com.auth0.jwt.interfaces.DecodedJWT; 6 import java.util.Calendar; 7 import java.util.Map; 8 /** * @Author: LiuLi * @Description: * @Date: Create in 16:21 2020/12/2 */ 9 public class JWTUtils { 10 private static final String SING = "!Q@W3142%^TY"; 11 /** * 生成token header.payload.sing */ 12 public static String getToken(Map<String, String> map) { 13 Calendar instance = Calendar.getInstance(); 14 instance.add(Calendar.DATE, 7); // 預設是7天的時間 // 建立 JWT builder JWTCreator.Builder builder = JWT.create(); 15 map.forEach((k, v) -> { 16 builder.withClaim(k, v); 17 }); 18 String token = builder 19 .withExpiresAt(instance.getTime()) // 指定令牌的過期時間 .sign(Algorithm.HMAC256(SING));// 簽名 return token; 20 } 21 /** * 驗證token的合法性 */ 22 public static void verify(String token) { 23 JWT.require(Algorithm.HMAC256(SING)).build().verify(token); 24 } 25 /** * 獲取token的方法 */ 26 public static DecodedJWT getTokenInfo(String token) { 27 DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token); 28 return verify; 29 } 30 }
- #先引入依賴