目錄
1.什麼是JWT
JSON Web令牌(JWT)是一個開放標準(RFC 7519),它定義了一種緊湊且自包含的方式,用於在各方之間作爲JSON對象安全地傳輸信息。由於此信息是經過數字簽名的,因此可以被驗證和信任。可以使用祕密(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰對對JWT進行簽名。
儘管可以對JWT進行加密以在雙方之間提供保密性,但我們將重點關注已簽名的令牌。簽名的令牌可以驗證其中包含的聲明的完整性,而加密的令牌則將這些聲明隱藏在其他方的面前。當使用公鑰/私鑰對對令牌進行簽名時,簽名還證明只有持有私鑰的一方纔是對其進行簽名的一方。
2.JWT什麼時候去使用,有什麼好處
授權: 這是使用JWT的最常見方案。一旦用戶登錄,每個後續請求將包括JWT,從而允許用戶訪問該令牌允許的路由,服務和資源。單一登錄是當今廣泛使用JWT的一項功能,因爲它的開銷很小並且可以在不同的域中輕鬆使用
信息交換: JSONWeb令牌是在各方之間安全地傳輸信息的好方法。因爲可以對JWT進行簽名(例如,使用公鑰/私鑰對),所以您可以確定發件人是他們所說的人。另外,由於簽名是使用標頭和有效負載計算的,因此您還可以驗證內容是否未被篡改。
支持跨域訪問:Cookie是不允許垮域訪問的,這一點對Token機制是不存在的,前提是傳輸的用戶認證信息通過HTTP頭傳輸.
無狀態(也稱:服務端可擴展行):Token機制在服務端不需要存儲session信息,因爲Token 自身包含了所有登錄用戶的信息,只需要在客戶端的cookie或本地介質存儲狀態信息.
去耦:不需要綁定到一個特定的身份驗證方案。Token可以在任何地方生成,只要在你的API被調用的時候,你可以進行Token生成調用即可.(這個似乎也在繼續說前面第一點和第二點的好處。。。)
更適用於移動應用:當你的客戶端是一個原生平臺(iOS, Android,Windows 8等)時,Cookie是不被支持的(你需要通過Cookie容器進行處理),這時採用Token認證機制就會簡單得多。
CSRF:因爲不再依賴於Cookie,所以你就不需要考慮對CSRF(跨站請求僞造)的防範。(如果token是用cookie保存,CSRF還是需要考慮,一般建議使用1、在HTTP請求中以參數的形式加入一個服務器端產生的token。或者2.放入http請求頭中也就是一次性給所有該類請求加上csrftoken這個HTTP頭屬性,並把token值放入其中)
基於標準化:你的API可以採用標準化的 JSON Web Token (JWT). 這個標準已經存在多個後端庫(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)
性能:JWT不僅可用於認證,還可用於信息交換。善用JWT有助於減少服務器請求數據庫的次數,請求數據庫的一次網絡往返時間(通過數據庫查詢session信息),相對比HMAC SHA256計算Token驗證和解析要費時得多。
3 JWT問題和趨勢
令牌問題:JWT默認不加密,但可以加密。生成原始令牌後,可以使用該令牌再次對其進行加密。
會話狀態:JWT的最大缺點是服務器不保存會話狀態,所以在使用期間不可能取消令牌或更改令牌的權限。也就是說,一旦JWT簽發,在有效期內將會一直有效。
安全問題:JWT本身包含認證信息,因此一旦信息泄露,任何人都可以獲得令牌的所有權限。爲了減少盜用,JWT的有效期不宜設置太長。對於某些重要操作,用戶在使用時應該每次都進行進行身份驗證。而且JWT不建議使用HTTP協議來傳輸代碼,爲了減少盜用和竊取,而是使用加密的HTTPS協議進行傳輸。
4 JWT的組成
JSON Web令牌以緊湊的形式由三部分組成,這些部分由點(.
)分隔,分別是:
- 頭部
- 有效載荷
- 簽名
因此,JWT通常如下結構所示。
xxxxx.yyyyy.zzzzz
頭部
JWT頭部分是一個描述JWT元數據的JSON對象,通常如下所示。
{
"alg": "HS256",
"typ": "JWT"
}
alg屬性表示簽名使用的算法,默認爲HMAC SHA256(寫爲HS256);typ屬性表示令牌的類型,JWT令牌統一寫爲JWT。
最後,使用Base64 URL算法將上述JSON對象轉換爲字符串保存,。
然後,此JSON被Base64 Url編碼以形成JWT的第一部分。
有效載荷
有效載荷部分,是JWT的主體內容部分,也是一個JSON對象,包含需要傳遞的數據。 JWT指定七個默認字段供選擇
iss: jwt簽發者
sub: jwt所面向,使用jwt的用戶
aud: 接收jwt的一方
exp: jwt的過期時間,這個過期時間必須大於簽發時間
nbf: 定義在指定時間之前,該jwt都是不可用的.
iat: jwt的簽發時間
jti: jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊
除以上默認字段外,我們還可以自定義私有字段,如下例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然後,對有效負載進行Base64Url編碼,以形成JSON Web令牌的第二部分。
注意:
JWT頭和有效載荷序列化的算法都用到了Base64 URL,該算法和常見Base64算法類似,稍有差別。
對於已簽名的令牌,此信息儘管可以防止篡改,但任何人都可以讀取。除非將其加密,否則請勿將重要信息放入JWT的有效負載或報頭元素中。
簽名
簽名部分是對上面兩部分數據簽名,通過指定的算法生成哈希,以確保數據不會被篡改。
我們需要指定一個密鑰(secret)。該密碼僅在服務器中使用,不能公開。然後,使用標頭中指定的簽名算法(默認情況下爲HMAC SHA256)根據以下公式生成簽名
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
注意:
簽名主要通過密鑰進行加密解密,用於驗證JWT在整個過程中有沒有被更改,保證JWT的完整性。
作爲令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三個字符是"+","/"和"=",由於在URL中有特殊含義,因此Base64URL中對他們做了替換:"="去掉,"+"用"-"替換,"/"用"_"替換,在使用的時候需要注意。
將這三部分用.連接成一個完整的字符串,構成了最終的jwt:
JWT解析地址:https://jwt.io/#libraries
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
通過解析上面JWT,可以得到以下圖中標頭,載荷信息:
4.java中使用
在項目中建議使用JJWT,因爲其API相對友好
引入Maven依賴
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
在項目中新建JwtDemo類
在jwtDemo類中添加以下jwt常量
public class JwtDemo {
//加密算法
private final static SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
//私鑰 / 生成簽名的時候使用的祕鑰secret,一般可以從本地配置文件中讀取,切記這個祕鑰不能外露,只在服務端使用,在任何場景都不應該流露出去。
// 一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
private final static String secret = "secretKey";
// 過期時間(單位秒)/ 2小時
private final static Long access_token_expiration = 7200L;
//jwt簽發者
private final static String jwt_iss = "spzhang";
//jwt所有人
private final static String subject = "zhangsp";
}
JWT生成方法
在jwtDemo類下添加以下jwt生成方法
/**
* 創建jwt
* @return
* 返回生成的jwt token
*/
public static String generateJwtToken(){
// 頭部 map / Jwt的頭部承載,第一部分
// 可不設置 默認格式是{"alg":"HS256"}
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
//載荷 map / Jwt的載荷,第二部分
Map<String,Object> claims = new HashMap<String,Object>();
//私有聲明 / 自定義數據,根據業務需要添加
claims.put("id","123456");
claims.put("userName", "admin");
//標準中註冊的聲明 (建議但不強制使用)
//一旦寫標準聲明賦值之後,就會覆蓋了那些標準的聲明
claims.put("iss", jwt_iss);
/* iss: jwt簽發者
sub: jwt所面向的用戶
aud: 接收jwt的一方
exp: jwt的過期時間,這個過期時間必須要大於簽發時間
nbf: 定義在什麼時間之前,該jwt都是不可用的.
iat: jwt的簽發時間
jti: jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊
*/
//下面就是在爲payload添加各種標準聲明和私有聲明瞭
return Jwts.builder() // 這裏其實就是new一個JwtBuilder,設置jwt的body
.setHeader(map) // 頭部信息
.setClaims(claims) // 載荷信息
.setId(UUID.randomUUID().toString()) // 設置jti(JWT ID):是JWT的唯一標識,從而回避重放攻擊。
.setIssuedAt(new Date()) // 設置iat: jwt的簽發時間
.setExpiration(new Date(System.currentTimeMillis() + access_token_expiration * 1000)) // 設置exp:jwt過期時間
.setSubject(subject) //設置sub:代表這個jwt所面向的用戶,所有人
.signWith(SIGNATURE_ALGORITHM, secret)//設置簽名:通過簽名算法和祕鑰生成簽名
.compact(); // 開始壓縮爲xxxxx.yyyyy.zzzzz 格式的jwt token
}
JWT解析方法
在jwtDemo下添加以下解析方法
/**
* 從jwt中獲取 載荷 信息
* @param jwt
* @return
*/
private static Claims getClaimsFromJwt(String jwt) {
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(jwt).getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
測試方法
通過Jwt生成Jwt token,並通過解析jwt獲取 載荷 信息
public static void main(String[] args) {
String jwtToken = generateJwtToken();
System.out.println("JWT Token "+ jwtToken);
System.out.println("=======================================================");
Claims claims = getClaimsFromJwt(jwtToken);
System.out.println(claims);
System.out.println(claims.get("userName"));
}
通過執行得到了以下信息,可以看到生成時間,和失效時間,自定義數據等
JWT Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ6aGFuZ3NwIiwiaXNzIjoic3B6aGFuZyIsImlkIjoiMTIzNDU2IiwidXNlck5hbWUiOiJhZG1pbiIsImV4cCI6MTU3NjEyODQzMSwiaWF0IjoxNTc2MTIxMjMxLCJqdGkiOiI1YWM1NTYyMy03MGMyLTRkYzgtYThkMC1lMGFlY2I5OTNmMTQifQ.51y0YnmPvDJBzzMe9aNwkZG6Pgcvqq6vJHtaTuY1CEI
=======================================================
{sub=zhangsp, iss=spzhang, id=123456, userName=admin, exp=1576128431, iat=1576121231, jti=5ac55623-70c2-4dc8-a8d0-e0aecb993f14}
admin