1. 程式人生 > 其它 >Java基於JWT的token認證

Java基於JWT的token認證

一、背景引入
由於Http協議本身是無狀態的,那麼伺服器是怎麼識別兩次請求是不是來自同一個客戶端呢,傳統使用者識別是基於seeion和cookie實現的。大致流程如下:

使用者向伺服器傳送使用者名稱和密碼請求
使用者進行校驗,校驗通過後建立session繪畫,並將使用者相關資訊儲存到session中
伺服器將sessionId回寫到使用者瀏覽器cookie中
使用者以後的請求,都會鞋帶cookie傳送到伺服器
伺服器得到cookie中的sessionId,從session集合中找到該使用者的session回話,識別使用者
這種模式有很多缺點,對於分散式架構的支援以及擴充套件性不是很好。而且session是儲存在記憶體中,單臺伺服器部署如果登陸使用者過多佔用伺服器資源也多,做叢集必須得實現session共享的話,叢集數量又不易太多,否則伺服器之間頻繁同步session也會非常耗效能。當然也可以引入持久層,將session儲存在資料庫或者redis中,儲存資料庫的話效率不高,存redis效率高,但是對redis依賴太重,如果redis掛了,影響整個應用。還有一種辦法就是不存伺服器,而是把使用者標識資料存在瀏覽器,瀏覽器每次請求都攜帶該資料,伺服器做校驗,這也是JWT的思想。

二、JWT介紹
2.1 概念介紹

Json Web Token(JWT)是目前比較流行的跨域認證解決方案,是一種基於JSON的開發標準,由於資料是可以經過簽名加密的,比較安全可靠,一般用於前端和伺服器之間傳遞資訊,也可以用在移動端和後臺傳遞認證資訊。

2.2 組成結構

JWT就是一段字串,格式如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxIn0.qfd0GelhE1aGr15LrnYlIZ_3UToaOM5HeMcXrmDG
由於三部分組成,之間用"."接。第一部分是頭資訊Header,中間部分是載荷Payload,最後部分是簽名信息Signature。

頭資訊Header:描述JWT基本資訊,typ表示採用JWT令牌,alg(algorithm)表示採用什麼演算法進行簽名,常見演算法有HmacSHA256(HS256)、HmacSHA384(HS384)、HmacSHA512(HS512)、SHA256withECDSA(ES256)、SHA256withRSA(RS256)、SHA512withRSA(RS512)等。如果採用HS256則頭資訊結構為:

{
"typ": "JWT",
"alg": "HS256
}
載荷Payload:載荷(也可以叫載體)是具體的傳輸內容,包括一些標準屬性,iss: 該JWT的簽發者,exp: 過期時間戳,iat: 簽發時間戳,jti: JWTID等等。也可以新增其他需要傳遞的內容資料。結構為:

{
"iss": "kkk",
"iat": 1548818203,
"exp": 1548818212,
"sub": "test.com
}
簽名Signature:對頭資訊和載荷進行簽名,保證傳輸過程中資訊不被篡改,比如:將頭資訊和載荷分別進行base64加密得到字串a和b,將字串a和b以點相連並簽名得到字串c,將字串a、b、c以點相連得到最終token。

2.3 驗證流程

使用JWT的驗證流程為:

使用者提交使用者名稱,密碼到伺服器後臺
後臺驗證通過,伺服器端生成Token字串,返回到客戶端
客戶端儲存Token,下一次請求資源時,附帶上Token資訊
伺服器端驗證Token是否由伺服器簽發的(一般在攔截器中驗證),若Token驗證通過,則返回需要的資源
驗證流程和基於session大體相同,只不過不是基於session,而是採用攔截器在程式碼中實驗驗證,返回給客戶端的也不是sessionid,而是經過一定演算法得出來的token字串。

2.4 原始碼分析

Java中有封裝好的開源哭JWT可以直接使用,下面就分析下關鍵程式碼驗證以下內容。

Header頭資訊結構分析關鍵原始碼如下:

//token生成方法
public static void main(String[] args) {
String token= JWT.create().withAudience("audience")
.withIssuedAt(new Date())
.withSubject("subject")
.withExpiresAt(new Date()).withJWTId("jtiid")
.sign(Algorithm.HMAC256(user.getPassword()));
}
public abstract class Algorithm {
private final String name;
private final String description;
//...其他方法省略...
public static Algorithm HMAC256(String secret) throws IllegalArgumentException {
return new HMACAlgorithm("HS256", "HmacSHA256", secret);
}
//...其他方法省略...
}
class HMACAlgorithm extends Algorithm {
private final CryptoHelper crypto;
private final byte[] secret;
//...其他方法省略...
HMACAlgorithm(String id, String algorithm, byte[] secretBytes)
throws IllegalArgumentException {
this(new CryptoHelper(), id, algorithm, secretBytes);
}
//...其他方法省略..
}
public String sign(Algorithm algorithm) throws IllegalArgumentException,
JWTCreationException {
if (algorithm == null) {
throw new IllegalArgumentException("The Algorithm cannot be null.");
} else {
this.headerClaims.put("alg", algorithm.getName());
this.headerClaims.put("typ", "JWT");
String signingKeyId = algorithm.getSigningKeyId();
if (signingKeyId != null) {
this.withKeyId(signingKeyId);
}
public final class JWTCreator {
private final Algorithm algorithm;
private final String headerJson;
private final String payloadJson;

private JWTCreator(Algorithm algorithm,
Map<String, Object> headerClaims,
Map<String, Object> payloadClaims) throws JWTCreationException {
this.algorithm = algorithm;
try {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ClaimsHolder.class, new PayloadSerializer());
mapper.registerModule(module);
mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
this.headerJson = mapper.writeValueAsString(headerClaims);
this.payloadJson =
mapper.writeValueAsString(new ClaimsHolder(payloadClaims));
} catch (JsonProcessingException var6) {
throw new JWTCreationException(
"Some of the Claims couldn't be converted to a valid JSON format.",
var6);
}
}
//...其他方法省略...

headerClaims是一個Map,包括兩個屬性typ和alg,typ值固定JWT,alg傳過來的簽名演算法這裡使用的

HmacSHA256簡稱HS256。typ和alg組成Header頭資訊。

Payload載荷結構分析關鍵原始碼如下:

public abstract class JWT {
public JWT() {
}
public static DecodedJWT decode(String token) throws JWTDecodeException {
return new JWTDecoder(token);
}
public static Verification require(Algorithm algorithm) {
return JWTVerifier.init(algorithm);
}
public static Builder create() {
return JWTCreator.init();
}
}
public static class Builder {
private final Map<String, Object> payloadClaims = new HashMap();
private Map<String, Object> headerClaims = new HashMap();

Builder() {
}
public JWTCreator.Builder withHeader(Map<String, Object> headerClaims) {
this.headerClaims = new HashMap(headerClaims);
return this;
}
public JWTCreator.Builder withKeyId(String keyId) {
this.headerClaims.put("kid", keyId);
return this;
}
public JWTCreator.Builder withIssuer(String issuer) {
this.addClaim("iss", issuer);//簽發人
return this;
}
public JWTCreator.Builder withSubject(String subject) {
this.addClaim("sub", subject);//主題
return this;
}
public JWTCreator.Builder withAudience(String... audience) {
this.addClaim("aud", audience);//接受一方
return this;
}
public JWTCreator.Builder withExpiresAt(Date expiresAt) {
this.addClaim("exp", expiresAt);//過期時間
return this;
}
public JWTCreator.Builder withNotBefore(Date notBefore) {
this.addClaim("nbf", notBefore);//生效時間
return this;
}
public JWTCreator.Builder withIssuedAt(Date issuedAt) {
this.addClaim("iat", issuedAt);//簽發時間
return this;
}
public JWTCreator.Builder withJWTId(String jwtId) {
this.addClaim("jti", jwtId);//編號
return this;
}
public JWTCreator.Builder withClaim(String name, Boolean value)
throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, Integer value)
throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
//...其他方法省略...
}

Payload是一個json物件,存放需要傳遞的資料,JTW預設規定了幾個屬性,如果需要新增其他屬性可以呼叫其過載方法witchClaim()新增。

Signature簽名部分原始碼如下:

private String sign() throws SignatureGenerationException {
String header = Base64.encodeBase64URLSafeString(
this.headerJson.getBytes(StandardCharsets.UTF_8));
String payload = Base64.encodeBase64URLSafeString(
this.payloadJson.getBytes(StandardCharsets.UTF_8));
String content = String.format("%s.%s", header, payload);
byte[] signatureBytes = this.algorithm.sign(
content.getBytes(StandardCharsets.UTF_8));
String signature = Base64.encodeBase64URLSafeString(signatureBytes);
return String.format("%s.%s", content, signature);
}
從這裡可以看出,所謂token就是分別對header和payload的json字串做Base64加密得到a和b,並將結果拼接一起,在進行簽名得到c,最終把a、b、c三部分內容以點拼接起來形成token,返回客戶端儲存,客戶端以後每次請求都在header中加入token,伺服器採用攔截器方式獲取header中的token做校驗,識別使用者。

三、示例
3.1 資料準備

建立使用者表

3.2 搭建springboot工程

設定工程Group、Artifact、Version、Name等資訊

Spring Boot的版本選擇2.0.8,選擇匯入web的起步器

建立工程成功之後,將各個包創建出來,工程目錄結構如下:

3.3 引入pom依賴

3.4 編寫application.yml配置檔案

3.5 編寫User實體類

Result類:用於統一返回訊息的封裝

TokenUtil類,用於生成token

VerifyToken註解類:加到controller方法上表示該方法需要驗證token。

3.6 編寫mapper介面和service層

mapper類:

UserService介面:

UserServiceImpl實現類:

3.7 編寫攔截器和全域性異常處理器

AuthInterceptor攔截器類:用於token驗證。

全域性異常處理器GloabllExceptionHandler:用於異常的捕獲。

3.8 編寫配置類及controller

攔截器配置類InterceptorConfig:配置攔截所有請求

UserController類:

3.9 測試

測試1:使用postman傳送get請求http://localhost:8088/user/getUser?id=1

測試2:傳送post請求http://localhost:8088/user/login 密碼故意輸錯

測試3:傳送post請求http://localhost:8088/user/login 填正確的使用者名稱密碼
————————————————

原文連結:https://blog.csdn.net/itcast_cn/article/details/97757813