JSON Web Token(缩写 JWT) 目前最流行、最常见的跨域认证解决方案,前端后端都需要会使用的东西
JSON Web Token(縮寫 JWT)是目前最流行,也是最常見的跨域認證解決方案。無論是咱們后端小伙伴,還是前端小伙伴對都是需要了解。
本文介紹它的原理、使用場景、用法。
關于封面:這個冬天你過得開心嗎
一、跨域認證的問題
1.1、常見的前后端認證方式
- Session-Cookie
- Token 驗證(包括JWT,SSO)
- OAuth2.0(開放授權)
1.2、Session-Cookie實現方式
流程大致如下:
1、用戶向服務器發送用戶名和密碼。
2、服務器驗證通過后,在當前對話(session)里面保存相關數據,比如用戶角色、登錄時間等等。
3、服務器向用戶返回一個 session_id,寫入用戶的 Cookie。
4、用戶隨后的每一次請求,都會通過 Cookie,將 session_id傳回服務器。
5、服務器收到 session_id,找到前期保存的數據,由此得知用戶的身份。
這種模式在單機時不存在什么問題,但是一旦服務器變為集群模式時,或者是跨域的服務器時,這個時候Session就必須實現數據共享。
這個時候就要考慮每臺服務器如何實現對 Session 的數據共享呢??
二、什么是 JWT ?
根據官網介紹:
JSON Web Token (JWT) 是一個開放標準,它定義了一種緊湊且自包含的方式,用于在各方之間作為 JSON 對象安全地傳輸信息。該信息可以被驗證和信任,因為它是經過數字簽名的。JWT 可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名。
簡單來理解就是 JWT 就是一個JSON對象經過加密和簽名的,可以在網絡中安全的傳輸信息,并且可以被驗證和信任。
2.1、什么時候應該使用 JWT ?
我目前用的最多的地方就是在授權方面,這也是 JWT 最常見的場景,其次還可以用來交換信息。
授權例子:
用戶登錄后,服務器端返回一個JWT,用戶保存在本地,之后的每次請求都將包含JWT,服務器驗證用戶攜帶的JWT,來判斷是否允許訪問服務和資源。
另外,單點登錄(SSO) 也是當今廣泛使用JWT的一項功能,就是在A網站登錄后,在B網站也能夠實現自動登錄,而不需要重復登錄,如你在淘寶登錄了,在身份沒有過期前,你去看天貓網站,也會發現你已經登錄了。
簡而言之:用戶只需要登錄一次就可以訪問所有相互信任的應用系統。并且能夠輕松跨不同域使用,服務器也不需要存儲session相關信息,減輕了負擔。
2.2、JWT 原理
其實 JWT 的原理就是,服務器認證以后,將一個 JSON 對象加密成一個緊湊的字符串(Token),發回給用戶,就像下面這樣。
// JSON 對象 {"姓名": "王五","角色": "管理員","到期時間": "2021年9月21日0點0分" } //加密后 eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VybmFtZSIsIm5iZiI6MTYzMjI3NzU1NCwiaXNzIjoiY3J1c2giLCJleHAiOjE2MzIyNzc2NTQsImRlbW8iOiLlj6_lrZjlgqjkv6Hmga8iLCJpYXQiOjE2MzIyNzc1NTQsImRlbW8yIjoi5Y-v5a2Y5YKo5L-h5oGvMiJ9.OuqG5Ha_Ofmh5R9Et1vqLYSAlIO85oW9D9Jq9cKKYODO643ZLiDTyQs8dl3PLsZ-_5t0xv6kfKhCzCkCYznBNA在認證之后,用戶和服務器通信時,每次都會攜帶上這個Token。服務器端不再存儲session信息,完全依靠用戶攜帶的Token來判斷用戶身份。為了安全,服務器在生成Token的時候,都會加上一個數字簽名。
這樣做的優勢:服務器不需要再保存 session數據,減輕了服務器負擔,并且基于 JWT 認證機制的應用不需要去考慮用戶在哪一臺服務器登錄,為應用的擴展提供了便利。
2.3、JWT 數據結構
JSON Web Tokens 由用點 ( .)分隔的三個部分組成,它們是:
- Header(頭部)
- Payload(負載)
- Signature(簽名)
因此,JWT 通常如下所示。注意:實際上是未分行的,這里是為了便于展示。
xxxxx.yyyyy.zzzzz 如: eyJhbGciOiJIUzUxMiJ9. eyJzdWIiOiJ1c2VybmFtZSIsIm5iZiI6MTYzMjI3NzU1NCwiaXNzIjoiY3J1c2giLCJleHAiOjE2MzIyNzc2NTQsImRlbW8iOiLlj6_lrZjlgqjkv6Hmga8iLCJpYXQiOjE2MzIyNzc1NTQsImRlbW8yIjoi5Y-v5a2Y5YKo5L-h5oGvMiJ9. OuqG5Ha_Ofmh5R9Et1vqLYSAlIO85oW9D9Jq9cKKYODO643ZLiDTyQs8dl3PLsZ-_5t0xv6kfKhCzCkCYznBNA2.3.1、Header (標題)
jwt的頭部承載兩部分信息:
- 聲明類型,這里是jwt
- 聲明加密的算法 通常直接使用 HMAC SHA256
Header 部分是一個 JSON 對象,描述 JWT 的元數據,通常是下面的樣子。
{ "alg": "HS256", "typ": "JWT" }上面代碼中,alg屬性表示簽名的算法(algorithm),默認是 HMAC SHA256(寫成 HS256);typ屬性表示這個令牌(token)的類型(type),JWT 令牌統一寫為JWT。
2.3.2、Payload(有效載荷)
Payload 部分也是一個 JSON 對象,用來存放實際需要傳遞的數據。JWT 規定了7個官方字段,供選用。
- iss (issuer):簽發人
- exp (expiration time):過期時間
- sub (subject):主題 jwt所面向的用戶
- aud (audience):受眾 接收jwt的一方
- nbf (Not Before):生效時間
- iat (Issued At):簽發時間
- jti (JWT ID):編號,jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
除了官方字段,你還可以在這個部分定義私有字段,下面就是一個例子。
{"sub": "1234567890","name": "John Doe","admin": true }注意,JWT 默認是不加密的,任何人都可以讀到,所以不要把秘密信息放在這個部分。
2.3.3、Signature(簽名)
Signature 部分是對前兩部分的簽名,防止數據篡改。
首先,需要指定一個密鑰(secret)。這個密鑰只有服務器才知道,不能泄露給用戶。然后,使用 Header 里面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產生簽名。
HMACSHA256 (base64UrlEncode(header) + "." +base64UrlEncode(payload),secret )算出簽名以后,把 Header、Payload、Signature 三個部分拼成一個字符串,每個部分之間用"點"(.)分隔,就可以返回給用戶。
注意:簽名用于驗證消息在此過程中沒有更改,并且在使用私鑰簽名的令牌的情況下,它還可以驗證 JWT 的發送者是它所說的人。secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證的關鍵,所以,它就是我們服務端的私鑰,在任何場景都不應該泄露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了,那么安全將不復存在。
2.3.4、 Base64URL
前面提到,Header 和 Payload 串型化的算法是 Base64URL。這個算法跟 Base64 算法基本類似,但有一些小的不同。
JWT 作為一個令牌(token),有些場合可能會 放到 URL(比如 api.example.com/?token=xxx)。Base64 有三個字符+、/和=,在 URL 里面有特殊含義,所以要被替換掉:=被省略、+替換成-,/替換成_ 。這就是 Base64URL 算法。
2.4、JWT工具類
相關依賴:
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version> </dependency>如果是Jdk11使用的話,可能會報這樣的一個錯誤:
Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverterat io.jsonwebtoken.impl.Base64Codec.decode(Base64Codec.java:26)at io.jsonwebtoken.impl.DefaultJwtBuilder.signWith(DefaultJwtBuilder.java:99)at com.crush.jwt.utils.JwtUtils.createJwt(JwtUtils.java:47)at com.crush.jwt.utils.JwtUtils.main(JwtUtils.java:127) Caused by: java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverterat java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)... 4 more好像是因為Jdk11中沒有這個類了,得加上下面這樣的一個依賴:
<dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.0</version> </dependency>工具類
import io.jsonwebtoken.*;import java.util.Date; import java.util.HashMap;/*** @Author: crush* @Date: 2021-09-21 22:18* version 1.0*/ public class JwtUtils {/*** 服務器端密鑰*/private static final String SECRET = "jwtsecretdemo";/*** 頒發者*/private static final String ISS = "crush";/*** 這里創建用到的時間、用戶名、應該是傳入進來的,* 登錄時選擇是否記住我,過期時間應當是不一致的。* @return*/public static String createJwt() {HashMap<String, Object> map = new HashMap<>();map.put("demo", "可存儲信息");map.put("demo2","可存儲信息2");String jwt = Jwts.builder().setClaims(map)// jwt所面向的用戶.setSubject("username")//設置頒發者.setIssuer(ISS)// 定義在什么時間之前,該jwt都是不可用的..setNotBefore(new Date())//簽發時間.setIssuedAt(new Date())//設置 JWT 聲明exp (到期)值.setExpiration(new Date(System.currentTimeMillis() + 100000)).signWith(SignatureAlgorithm.HS512, SECRET)//實際構建 JWT 并根據JWT 緊湊序列化 規則將其序列化為緊湊的、URL 安全的字符串。.compact();return jwt;}/*** 獲取 Claims 實例* Claims :一個 JWT聲明集 。* 這最終是一個 JSON 映射,可以向其中添加任何值,但為了方便起見,JWT 標準名稱作為類型安全的 getter 和 setter 提供。* 因為這個接口擴展了Map<String, Object> , 如果您想添加自己的屬性,只需使用 map 方法,* 例如:* claims.put("someKey", "someValue");** @param jwt* @return*/public static Claims getBody(String jwt) {return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(jwt).getBody();}/*** 判斷 JWT 是否已過期** @param jwt* @return*/public static boolean isExpiration(String jwt) {return getBody(jwt)//返回 JWT exp (到期)時間戳,如果不存在則返回null 。.getExpiration()//測試此日期是否在指定日期之前。.before(new Date());}/*** Subject:獲取 jwt 所面向的用戶** @param jwt* @return*/public static String getSubject(String jwt) {return getBody(jwt).getSubject();}/*** Issuer:獲取頒發者** @param jwt* @return*/public static String getIssuer(String jwt) {return getBody(jwt).getIssuer();}/*** getClaimsValue** @param jwt* @return*/public static String getClaimsValue(String jwt) {return (String) getBody(jwt).get("demo");}/*** getClaimsValue** @param jwt* @return*/public static String getClaimsValue2(String jwt) {return (String) getBody(jwt).get("demo2");}public static void main(String[] args) {String jwt = createJwt();System.out.println(jwt);System.out.println("jwt 是否已經過期:"+isExpiration(jwt));System.out.println("Claims 中所存儲信息:"+getBody(jwt).toString());System.out.println("jwt 所面向的用戶:"+getSubject(jwt));System.out.println("jwt 頒發者:"+getIssuer(jwt));System.out.println("通過鍵值,取出我們自己放進 Jwt 中的信息:"+getClaimsValue(jwt));System.out.println("通過鍵值,取出我們自己放進 Jwt 中的信息2:"+getClaimsValue2(jwt));} }三、如何應用
此后,客戶端每次與服務器通信,都要帶上這個 JWT。你可以把它放在 Cookie 里面自動發送,但是這樣不能跨域,所以更好的做法是放在 HTTP 請求的頭信息Authorization字段里面。
Authorization: Bearer <token>一般是在請求頭里加入Authorization,并加上Bearer標注:
fetch('api/user/1', {headers: {'Authorization': 'Bearer ' + token} })服務端會驗證token,如果驗證通過就會返回相應的資源。整個流程就是這樣的:
實際使用過程中,我們通常是結合著Security安全框架一起使用的,大家感興趣的話,可以來一起看看我寫的這篇文章。
SpringBoot整合Security安全框架、控制權限
也可以直接看源碼:Security-Gitee
四、總結
4.1、優點:
- 因為json的通用性,JWT支持多語言,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
- 因為有了payload部分,所以JWT可以在自身存儲一些其他業務邏輯所必要的非敏感信息。
- 可以用于交換信息。有效使用 JWT,可以降低服務器查詢數據庫的次數。
- 便于傳輸,jwt的構成非常簡單,字節占用很小,所以它是非常便于傳輸的。
- 它不需要在服務端保存會話信息, 所以它易于應用的擴展
4.2、安全相關:
- 保護好secret私鑰,該私鑰非常重要。如果密鑰泄露,用戶自己即可頒布JWT令牌,安全將不復存在。
- 如果條件允許,JWT 不應該使用 HTTP 協議明碼傳輸,而是要使用 HTTPS 協議傳輸。Https協議更安全。
- JWT 的有效期應該設置得比較短。對于一些比較重要的權限,使用時應該再次對用戶進行認證。
4.3、缺點:
- JWT 的最大優點是不需要在服務端保存會話信息,最大的缺點也是如此,由于服務器不保存 session 狀態,因此無法在使用過程中廢止某個 token,或者更改 token 的權限。也就是說,一旦 JWT 簽發了,在到期之前就會始終有效。
五、自言自語
本文就是簡單介紹了,具體使用具體情況具體分析啦。
你好,我是博主寧在春:主頁
希望本篇文章能讓你感到有所收獲!!!
祝 我們:待別日相見時,都已有所成。
參考:
jwt
JSON Web Token 入門教程
總結
以上是生活随笔為你收集整理的JSON Web Token(缩写 JWT) 目前最流行、最常见的跨域认证解决方案,前端后端都需要会使用的东西的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我们:待别日相见时,我们都已有所成。挥手
- 下一篇: Java设计模式-观察者模式(订阅发布模