一、背景引入
由于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%20編寫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 填正確的用戶名密碼