JWT(JSON Web Token)是一個非常輕巧的規範,這個規範允許我們使用JWT在使用者和伺服器之間傳遞安全可靠的資訊,通常使用在HTTP通信過程中進行身份認證。
我們知道,HTTP通信是無狀态的,用戶端的請求到了伺服器處理完之後是無法傳回給原來的用戶端的,是以需要對通路的用戶端進行識别,常用的做法是通過Session機制:用戶端在服務端登入成功後,服務端會生成一個sessionId傳回給用戶端,然後用戶端會把這個sessionId存到本地浏覽器的Cookie中,當用戶端再向服務端發起請求就會從Cookie中取出這個sessionId并附加到請求頭中。這樣服務端就能夠知道是哪個使用者發起的請求。
Session存在的問題
1.Session是儲存在服務端的,當客戶通路量增加的時候,服務端就需要存儲大量的Session會話,對響應性能造成影響。
2.當伺服器為叢集的時候,使用者登入其中的一台伺服器,會将Session儲存到該伺服器的記憶體中,但是當使用者通路到其他伺服器的時候,因為在該伺服器的記憶體中找不到該Session,就會重新生成一個Session傳回給用戶端,造成分布式Session共享問題。通常會采用緩存一緻性的技術來解決分布式Session的共享問題,或者是使用第三方緩存(比如Redis)來儲存Session,其實也可以使用JWT來解決這個問題,也就是使用JWT替代Session。
JWT的産生與使用步驟
1.用戶端通過使用者名和密碼登入伺服器。
2.服務端對用戶端進行身份驗證。
3.服務端對該使用者生成Token,傳回給用戶端。
4.用戶端發起請求,需要攜帶該Token。
5.服務端收到請求後,首先驗證Token,然後再傳回資料。
6.用戶端将Token儲存到本地浏覽器,一般也是儲存到Cookie中。
這樣,服務端就不需要儲存Token,隻需要對Token中攜帶的資訊進行驗證即可。
無論用戶端通路背景的哪台伺服器,隻要可以通過使用者資訊的驗證即可。
JWT的原理
JWT的原理是,伺服器認證之後,生成一個JSON對象,傳回給使用者,就像下面這樣。
{
"姓名": "張飛",
"角色": "管理者",
"到期時間": "2019年05月20日0時0分"
}
以後使用者和伺服器進行通信的時候都要發送這個JSON對象。伺服器完全隻靠這個對象認證使用者資訊。為了防止使用者篡改資料,伺服器在生成這個對象的時候,都會加上簽名。這樣,伺服器就不需要儲存任何Session會話資訊了,也就是說,伺服器變成無狀态的了,進而比較容易實作擴充。
JWT的資料結構
實際的JWT大概就是像下面這個樣子:

這是一個很長的字元串,中間用點分隔符【.】分隔成三個部分(要注意,JWT内部是沒有換行的)。
JWT由三個部分組成:頭部(Header)、負載(Payload)和簽名(Signature)。
Header
Heade部分是一個JSON對象,是描述JWT的中繼資料。通常是下面這種格式的:
{
"alg": "HS256",
"typ": "JWT"
}
在上面的代碼中,alg屬性表示簽名的算法(Algorithm),預設是HMAC SHA256(寫成HS256);typ屬性表示這個令牌(token)的類型(type),JWT令牌固定寫為JWT。最後,将上面的JSON對象使用Base64URL算法轉成字元串。
Payload
Payload部分也是一個JSON對象,用來存放實際需要傳遞的資料。JWT規定了7個官方字段供選用。
iss(issuer) | 簽發人 |
exp(expiration time) | 過期時間 |
sub(subject) | 主題 |
aud(audience) | 閱聽人 |
nbf(not before) | 生效時間 |
iat(issued at) | 簽發時間 |
jti(jwt id) | 編号 |
除了這些官方字段,也可以在這個部分定義私有字段:
{
"sub": "buybuybuy",
"name": "yanggb",
"admin": true
}
注意這個JWT部分預設是不加密的,任何人都可以讀到,是以不要把秘密資訊放在這個部分。
這個JSON對象也要使用Base64URL算法轉成字元串。
Signature
Signature部分是對前兩部分的簽名,防止資料篡改。
首先,需要指定一個密鑰(secret)。這個密鑰隻有伺服器才知道,不能洩露給使用者。
然後,使用Header裡面指定的簽名算法(預設是HMAC SHA256),按照下面的公式産生簽名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
最後,在算出簽名之後,把Header、Payload、Signature三個部分拼成一個字元串,每個部分之間用點分隔符【.】分割,就可以傳回給使用者了。
Base64URL算法
前面提到,Header和Payload串型化的算法是Base64URL。這個算法跟Base64算法基本類似,但有一些小的不同。
JWT作為一個令牌(Token),有些場合可能會放到URL(比如api.yanggb.com/?token=xxx)。Base64有三個字元【+】、【/】和【=】,在URL裡面是有特殊含義的,是以需要被替換掉:【=】号被省略、【+】号被替換成【-】、【/】被替換成【_】。這就是Base64URL算法和Base64算法的不同。
JWT的使用方式
用戶端收到服務端傳回的JWT,可以存儲在Cookie裡面,也可以存儲在localStorage裡面。此後,用戶端每次與伺服器通信,都要帶上這個JWT。你可以把它放在Cookie裡面自動發送,但是這樣就不能跨域,是以更好的做法是放在HTTP請求的頭資訊Authorization字段裡面。
Authorization: Bearer <token>
另一種做法是在跨域的時候将JWT放在POST請求的資料體裡面。
JWT的幾個特點
1.JWT預設是不加密,但是也是可以加密的。生成原始的Token以後,可以用密鑰再加密一次。JWT不加密的情況下,不能将秘密資料寫入JWT。
2.JWT不僅可以用于認證,也可以用于交換資訊。有效地使用JWT,可以降低伺服器查詢資料庫的次數。
3.JWT的最大缺點是由于伺服器不儲存Session會話狀态,是以無法在使用過程中廢止某個Token或者更改Token的權限。意思就是說,一旦JWT簽發了,在到期之前就會始終有效,除非伺服器有部署額外的處理邏輯。
4.JWT本身包含了認證資訊,一旦洩露,任何人都可以獲得該令牌的所有權限。為了減少盜用,JWT的有效期應該設定得比較短。對于一些比較重要的權限,使用時應該再次對使用者進行認證。
5.為了減少盜用,JWT不應該使用HTTP協定明碼傳輸,要使用HTTPS協定傳輸。
JWT的使用執行個體(Demo)
在Maven項目中添加JWT的依賴、相關依賴和插件。
<!-- JWT -->
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
建立一個單元測試類。
public class JWTDemo {
// 加密的KEY
private static final String SECRET_KEY = "123456";
@Test
public void jwtTest() throws InterruptedException {
// 建立JWT
long time = System.currentTimeMillis() + 30*60*1000;
String jwt = this.buildJwt(new Date(time));
System.out.println("JWT字元串:" + jwt);
// 驗證token是否可用
boolean vaild = this.isJwtValid(jwt);
System.out.println("Token可用:" + vaild);
}
/**
* 建立JWT
* @param exp 過期時間
* @return JWT String
*/
private String buildJwt(Date exp) {
return Jwts.builder()
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // SECRET_KEY是加密算法對應的密鑰,這裡使用額是HS256加密算法
.claim("name", "yanggb")
.setExpiration(exp) // expTime是過期時間
.compact();
}
private boolean isJwtValid(String jwt) {
try {
// 解析JWT字元串中的資料,并進行最基礎的驗證
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY) // SECRET_KEY是加密算法對應的密鑰,jjwt可以自動判斷機密算法
.parseClaimsJws(jwt) // jwt是JWT字元串
.getBody();
System.out.println(claims);
}
// 在解析JWT字元串時,如果密鑰不正确,将會解析失敗,抛出SignatureException異常,說明該JWT字元串是僞造的
// 在解析JWT字元串時,如果過期時間字段已經早于目前時間,将會抛出ExpiredJwtException異常,說明本次請求已經失效
catch (SignatureException | ExpiredJwtException e) {
return false;
}
return true;
}
}
運作之後就可以在控制台看到列印出來的資訊,然後可以使用JWT的解析工具解析出内容(https://jwt.io/):
這些就是JWT的入門知識。
"我以為回憶終究會慢慢擱淺,化作泡沫,消失在時光の海的邊陲。可是記憶的種子卻時而開花,時而凋零,告訴我們:今後無論去哪,都要勇敢。"