JWT令牌
單點登入
一、使用者身份認證
1、單一伺服器模式
一般過程如下:
- 使用者向伺服器傳送使用者名稱和密碼。
- 驗證伺服器後,相關資料(如使用者名稱,使用者角色等)將儲存在當前會話(session)中。
- 伺服器向用戶返回session_id,session資訊都會寫入到使用者的Cookie。
- 使用者的每個後續請求都將通過在Cookie中取出session_id傳給伺服器。
- 伺服器收到session_id並對比之前儲存的資料,確認使用者的身份。
缺點:
- 單點效能壓力,無法擴充套件。
- 分散式架構中,需要session共享方案,session共享方案存在效能瓶頸。
session共享方案:
session廣播:效能瓶頸,不推薦
redis代替session:推薦,效能高
2、SSO(Single Sign On)模式
CAS單點登入、OAuth2
分散式,SSO(single sign on)模式:單點登入英文全稱Single Sign On,簡稱就是SSO。它的解釋是:在多個應用系統中,只需要登入一次,就可以訪問其他相互信任的應用系統。
- 如圖所示,圖中有3個系統,分別是業務A、業務B、和SSO。
- 業務A、業務B沒有登入模組。
- 而SSO只有登入模組,沒有其他的業務模組。
一般過程如下:
- 當業務A、業務B需要登入時,將跳到SSO系統。
- SSO從使用者資訊資料庫中獲取使用者資訊並校驗使用者資訊,SSO系統完成登入。
- 然後將使用者資訊存入快取(例如redis)。
- 當用戶訪問業務A或業務B,需要判斷使用者是否登入時,將跳轉到SSO系統中進行使用者身份驗證,SSO判斷快取中是否存在使用者身份資訊。
- 這樣,只要其中一個系統完成登入,其他的應用系統也就隨之登入了。這就是單點登入(SSO)的定義。
優點 :
使用者身份資訊獨立管理,更好的分散式管理。可以自己擴充套件安全策略
缺點:
認證伺服器訪問壓力較大。
3、Token模式
優點:
- 無狀態: token是無狀態,session是有狀態的
- 基於標準化:你的API可以採用標準化的 JSON Web Token (JWT)
缺點:
- 佔用頻寬
- 無法在伺服器端銷燬
二、訪問令牌的型別
三、JWT令牌
1、什麼是JWT令牌
JWT是JSON Web Token的縮寫,即JSON Web令牌,是一種自包含令牌。
JWT的使用場景:
一種情況是webapi,類似之前的阿里雲播放憑證的功能另一種情況是多web伺服器下實現無狀態分散式身份驗證JWT官網有一張圖描述了JWT的認證過程JWT的作用:JWT 最重要的作用就是對 token資訊的防偽作用JWT的原理:一個JWT由三個部分組成:JWT頭、有效載荷、簽名雜湊最後由這三者組合進行base64編碼得到JWT
2、JWT令牌的組成
典型的,一個JWT看起來如下圖:
該物件為一個很長的字串,字元之間通過"."分隔符分為三個子串。
每一個子串表示了一個功能塊,總共有以下三個部分:JWT頭、有效載荷和簽名
JWT頭
JWT頭部分是一個描述JWT元資料的JSON物件,通常如下所示。
{
"alg": "HS256",
"typ": "JWT"
}
在上面的程式碼中,alg屬性表示簽名使用的演算法,預設為HMAC SHA256(寫為HS256);typ屬性表示令牌的型別,JWT令牌統一寫為JWT。最後,使用Base64 URL演算法將上述JSON物件轉換為字串儲存。
有效載荷
有效載荷部分,是JWT的主體內容部分,也是一個JSON物件,包含需要傳遞的資料。 JWT指定七個預設欄位供選擇。
sub: 主題
iss: jwt簽發者
aud: 接收jwt的一方
iat: jwt的簽發時間
exp: jwt的過期時間,這個過期時間必須要大於簽發時間
nbf: 定義在什麼時間之前,該jwt都是不可用的.
jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
除以上預設欄位外,我們還可以自定義私有欄位,如下例:
{
"name": "Helen",
"admin": true,
"avatar": "helen.jpg"
}
請注意,預設情況下JWT是未加密的,任何人都可以解讀其內容,因此不要構建隱私資訊欄位,存放保密資訊,以防止資訊洩露。
JSON物件也使用Base64 URL演算法轉換為字串儲存。
簽名雜湊
簽名雜湊部分是對上面兩部分資料簽名,通過指定的演算法生成雜湊,以確保資料不會被篡改。
首先,需要指定一個密碼(secret)。該密碼僅僅為儲存在伺服器中,並且不能向用戶公開。然後,使用標頭中指定的簽名演算法(預設情況下為HMAC SHA256)根據以下公式生成簽名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret) ==> 簽名hash
在計算出簽名雜湊後,JWT頭,有效載荷和簽名雜湊的三個部分組合成一個字串,每個部分用"."分隔,就構成整個JWT物件。
Base64URL演算法
如前所述,JWT頭和有效載荷序列化的演算法都用到了Base64URL。該演算法和常見Base64演算法類似,稍有差別。
作為令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三個字元是"+","/"和"=",由於在URL中有特殊含義,因此Base64URL中對他們做了替換:"="去掉,"+"用"-"替換,"/"用"_"替換,這就是Base64URL演算法。
注意:base64編碼,並不是加密,只是把明文資訊變成了不可見的字串。但是其實只要用一些工具就可以把base64編碼解成明文,所以不要在JWT中放入涉及私密的資訊。
3、JWT的用法
客戶端接收伺服器返回的JWT,將其儲存在Cookie或LocalStorage中。
此後,客戶端將在與伺服器互動中都會攜帶JWT。如果將它儲存在Cookie中,就可以自動傳送,但是不會跨域,因此一般是將它放入HTTP請求的Header Authorization欄位中。
當跨域時,也可以將JWT放置於POST請求的資料主體中。
四、JWT問題和趨勢
1、JWT預設不加密,但可以加密。生成原始令牌後,可以使用該令牌再次對其進行加密。
2、當JWT未加密時,一些私密資料無法通過JWT傳輸。
3、JWT不僅可用於認證,還可用於資訊交換。善用JWT有助於減少伺服器請求資料庫的次數。
4、JWT的最大缺點是伺服器不儲存會話狀態,所以在使用期間不可能取消令牌或更改令牌的許可權。也就是說,一旦JWT簽發,在有效期內將會一直有效。
5、JWT本身包含認證資訊,因此一旦資訊洩露,任何人都可以獲得令牌的所有許可權。為了減少盜用,JWT的有效期不宜設定太長。對於某些重要操作,使用者在使用時應該每次都進行身份驗證。
6、為了減少盜用和竊取,JWT不建議使用HTTP協議來傳輸程式碼,而是使用加密的HTTPS協議進行傳輸。
五、測試JWT
JWT依賴
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
1、生成token
package com.atguigu.jwt;
public class JwtTests {
//過期時間,毫秒,24小時
private static long tokenExpiration = 24*60*60*1000;
//祕鑰
private static String tokenSignKey = "hguo123";
@Test
public void testCreateToken(){
String token = Jwts.builder()
// 頭
.setHeaderParam("typ", "JWT") //令牌型別
.setHeaderParam("alg", "HS256") //簽名演算法
// 載荷:預設資訊
.setSubject("guli-user") //令牌主題
.setIssuer("atguigu") //簽發者
.setAudience("atguigu") //接收者
.setIssuedAt(new Date()) //簽發時間
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) //過期時間
.setNotBefore(new Date(System.currentTimeMillis() + 20*1000)) //20秒後可用
.setId(UUID.randomUUID().toString())
// 載荷:自定義資訊
.claim("nickname", "Helen")
.claim("avatar", "1.jpg")
// 簽名雜湊
.signWith(SignatureAlgorithm.HS256, tokenSignKey)
// 組裝JWT字串
.compact();
System.out.println(token);
}
}
2、解析token
@Test
public void testGetUserInfo(){
//祕鑰
private static String tokenSignKey = "hguo123";
String token = "jwt字串";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
// 取出有效載荷
Claims claims = claimsJws.getBody();
String subject = claims.getSubject();
String issuer = claims.getIssuer();
String audience = claims.getAudience();
Date issuedAt = claims.getIssuedAt();
Date expiration = claims.getExpiration();
Date notBefore = claims.getNotBefore();
String id = claims.getId();
// 取出自定義有效載荷
String nickname = (String)claims.get("nickname");
String avatar = (String)claims.get("avatar");
System.out.println(subject);
System.out.println(issuer);
System.out.println(audience);
System.out.println(issuedAt);
System.out.println(expiration);
System.out.println(notBefore);
System.out.println(id);
System.out.println(nickname);
System.out.println(avatar);
}