JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案。通過客戶端保存數據,而服務器根本不保存會話數據,每個請求都被發送回服務器。 JWT是這種解決方案的代表。
一、跨域身份驗證
1、Internet服務無法與用戶身份驗證分開。一般過程如下:
(1)用戶向服務器發送用戶名和密碼。
(2)驗證服務器后,相關數據(如用戶角色,登錄時間等)將保存在當前會話中。
(3)服務器向用戶返回session_id,session信息都會寫入到用戶的Cookie。
(4)用戶的每個后續請求都將通過在Cookie中取出session_id傳給服務器。
(5)服務器收到session_id並對比之前保存的數據,確認用戶的身份。
2、此模式存在的問題
這種模式最大的問題是,沒有分布式架構,無法支持橫向擴展。如果使用一個服務器,該模式完全沒有問題。
但是,如果它是服務器群集或面向服務的跨域體系結構的話,則需要一個統一的session數據庫庫來保存會話數據實現共享,這樣負載均衡下的每個服務器才可以正確的驗證用戶身份。
在分布式的情況下,jwt令牌的這種解決方案是比較優雅的,只是通過客戶端保存數據,而服務器根本不保存會話數據,每個請求都被發送回服務器,jwt驗證模式流程圖如下:
注意:圖片來源網絡
二、jwt詳解
(注意:jwt詳解的內容來自 https://baijiahao.baidu.com/s?id=1608021814182894637&wfr=spider&for=pc 什么鬼,我怎么會引用百家號的內容!!!)
1、jwt的原則
JWT的原則是在服務器身份驗證之后,將生成一個JSON對象並將其發送回用戶,之后,當用戶與服務器通信時,客戶在請求中發回JSON對象。服務器僅依賴於這個JSON對象來標識用戶。為了防止用戶篡改數據,服務器將在生成對象時添加簽名(有關詳細信息,請參閱下文)。服務器不保存任何會話數據,即服務器變為無狀態,使其更容易擴展
2、jwt的數據結構
一個jwt的由三部分構成,此對象為一個很長的字符串,字符之間通過"."分隔符分為三個子串。注意JWT對象為一個長字串,各字串之間也沒有換行符,此處為了演示需要,我們特意分行並用不同顏色表示了。每一個子串表示了一個功能塊,總共有以下三個部分:
jwt的三個部分如下。jwt頭、有效載荷和簽名,將它們寫成一行,如下:
注意:圖片來源於網絡
(1)jwt頭
JWT頭部分是一個描述JWT元數據的JSON對象,通常如下所示:
{"alg": "HS256","typ": "JWT"}
在上面的代碼中,alg屬性表示簽名使用的算法,默認為HMAC SHA256(寫為HS256);typ屬性表示令牌的類型,JWT令牌統一寫為JWT。
最后,使用Base64 URL算法將上述JSON對象轉換為字符串保存。
(2)有效載荷
有效載荷部分,是JWT的主體內容部分,也是一個JSON對象,包含需要傳遞的數據。 JWT指定七個默認字段供選擇。
iss:發行人
exp:到期時間
sub:主題
aud:用戶
nbf:在此之前不可用
iat:發布時間
jti:JWT ID用於標識該JWT
除以上默認字段外,我們還可以自定義私有字段,如下例:
{"sub": "1234567890","name": "chongchong","admin": true}
請注意,默認情況下JWT是未加密的,任何人都可以解讀其內容,因此不要構建隱私信息字段,存放保密信息,以防止信息泄露。
JSON對象也使用Base64 URL算法轉換為字符串保存。
(3)簽名哈希
簽名哈希部分是對上面兩部分數據簽名,通過指定的算法生成哈希,以確保數據不會被篡改。
首先,需要指定一個密碼(secret)。該密碼僅僅為保存在服務器中,並且不能向用戶公開。然后,使用標頭中指定的簽名算法(默認情況下為HMAC SHA256)根據以下公式生成簽名。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
在計算出簽名哈希后,JWT頭,有效載荷和簽名哈希的三個部分組合成一個字符串,每個部分用"."分隔,就構成整個JWT對象。
(4)Base64URL算法
如前所述,JWT頭和有效載荷序列化的算法都用到了Base64URL。該算法和常見Base64算法類似,稍有差別。
作為令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三個字符是"+","/"和"=",由於在URL中有特殊含義,因此Base64URL中對他們做了替換:"="去掉,"+"用"-"替換,"/"用"_"替換,這就是Base64URL算法,很簡單把。
三、jwt在小程序API中使用
1、整體流程梳理
(1)我們用code碼(用戶的account賬戶)通過調用微信開放的接口,獲取到一個唯一的openid
(2)分為兩種情況:(首先是通過openid查詢user表)
用戶第一次使用(相當於注冊):我們將openid寫入到user表中
用戶第二次或者多次使用之后(相當於登錄):得到userId
(3)userId寫入到jwt令牌中
(4)將jwt令牌返回給小程序
2、令牌的生成
注意:生成jwt的時候,可供選擇的庫有多個,例如:jjwt,auth0 (詳情見:https://jwt.io/)
(1)安裝auth0的依賴(版本隨意,bug自處理)
1 <dependency> 2 <groupId>com.auth0</groupId> 3 <artifactId>java-jwt</artifactId> 4 <version>3.8.3</version> 5 </dependency>
(2)生成令牌(這里只看一下生成jwt令牌的方法)
1 @Component 2 public class JwtToken { 3 4 private static String jwtKey; 5 6 private static Integer expiredTimeIn; 7 8 private static Integer defaultScope = 8; 9 10 @Value("${missyou.security.jwt-key}") 11 public void setJwtKey(String jwtKey) { 12 JwtToken.jwtKey = jwtKey; 13 } 14 15 @Value("${missyou.security.token-expired-in}") 16 public void setExpiredTimeIn(Integer expiredTimeIn) { 17 JwtToken.expiredTimeIn = expiredTimeIn; 18 } 19 20 /** 21 * 生成token 主方法(適用於有分級權限的) 22 * 23 * @param uid 用戶id 24 * @param scope 權限分級的數字 25 * @return 26 */ 27 public static String makenToken(Long uid, Integer scope) { 28 return JwtToken.getToken(uid, scope); 29 } 30 31 /** 32 * 生成token方法 (適用於沒有分級權限的) 33 * 34 * @param uid 用戶id 35 * @return 36 */ 37 public static String makeToken(Long uid) { 38 return JwtToken.getToken(uid, JwtToken.defaultScope); 39 } 40 41 /** 42 * 真正生成token的方法 43 * 44 * @param uid 45 * @param scope 46 * @return 47 */ 48 private static String getToken(Long uid, Integer scope) { 49 50 // 需要傳入一個secret 隨機字符串 這個寫到配置文件中,從配置文件中讀取,方便更改 51 Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey); 52 53 // 計算過期時間以及令牌簽發的當前時間 54 Map<String, Date> map = JwtToken.calculateExpiredIssues(); 55 56 // 過期時間是傳入Date類型,需要我們自己來轉換成一個過期時間 57 // 例如 12:00 10s后過期,我們需要得到 12:00:10 這個過期時間 58 String token = JWT.create() 59 .withClaim("uid", uid) 60 .withClaim("scope", scope) 61 .withExpiresAt(map.get("expiredTime")) 62 .withIssuedAt(map.get("now")) //令牌的簽發時間 63 .sign(algorithm); 64 return token; 65 } 66 67 /** 68 * 計算過期時間以及當前的時間 69 * 70 * @return 71 */ 72 private static Map<String, Date> calculateExpiredIssues() { 73 Map<String, Date> map = new HashMap<>(); 74 Calendar calendar = Calendar.getInstance(); 75 Date now = calendar.getTime(); 76 calendar.add(Calendar.SECOND, JwtToken.expiredTimeIn); 77 map.put("now", now); 78 map.put("expiredTime", calendar.getTime()); 79 return map; 80 } 81 }
注意:方法基本上就是這樣子,jwt令牌的create()方法可以加入多個參數的,你自己自定義的!還有那個setter方法注入的是可以值得思考學習一下的!
(3)后續
至於如何和微信小程序進行交互,接口如何設計,就不貼出來了,自己思考一下吧!
3、令牌的校驗與全局處理
思考:令牌如果在每一個請求中進行校驗的話,在每個controller中的方法進行校驗的話,會導致大量重復的代碼,我們需要做的是做一個全局處理,進行令牌的驗證
(1)令牌的校驗
主要就是在token工具類中繼續追加驗證token的方法,其實是驗證並且獲取令牌中的數據,這里用的是Optional來進行處理的,具體方法如下:
1 /** 2 * 驗證令牌token並獲取令牌中的數據(Optional的使用) 3 * 4 * @param token 加密令牌token 5 * @return 6 */ 7 public Optional<Map<String, Claim>> verifyAndGetClaims(String token) { 8 DecodedJWT decodedJWT; 9 Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey); 10 JWTVerifier jwtVerifier = JWT.require(algorithm).build(); 11 12 try{ 13 decodedJWT = jwtVerifier.verify(token); 14 }catch (JWTVerificationException e){ 15 return Optional.empty(); 16 } 17 return Optional.of(decodedJWT.getClaims()); 18 }
(2)全局處理
在springboot中,有三種方式實現攔截HTTP請求的方式,分別是filter,interceptor以及AOP
三者主要的區別:
## filter是基於servlet(servlet其實是一種為web開發制定的規范,是一種標准接口,Tomcat是一種servlet的容器)
## interceptor和AOP是基於spring框架的(interceptor相對來說實現起來簡單一些)
注意:
當一個http請求向服務器發送的時候,如果項目中同時配置了這三種攔截機制,那么他是有一個順序的,就是
filter --> interceptor --> aop (請求經過的順序)
--> controller --> (controller處理層)
aop --> interceptor --> filter (處理結果返回順序)
當然,在這里我們使用的是interceptor,開始搞起!
interceptor相關代碼:
1 public class PermissionInterceptor extends HandlerInterceptorAdapter { 2 3 @Override 4 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 5 Optional<ScopeLevel> scopeLevel = this.getScopeLevel(handler); 6 // 沒有加上@ScopeLevel注解,說明沒有訪問權限的設定 7 if (!scopeLevel.isPresent()) { 8 return true; 9 } 10 String bearerToken = request.getHeader("Authorization"); 11 // token為空 12 if (StringUtils.isEmpty(bearerToken)) { 13 throw new UnAuthenticatedException(10004); 14 } 15 // token是默認的Bearer <token>格式,以Bearer開頭,需要進行判斷一下 16 if (bearerToken.startsWith("Bearer")) { 17 throw new UnAuthenticatedException(10004); 18 } 19 String[] tokens = bearerToken.split(" "); 20 if(tokens.length != 2){ 21 throw new UnAuthenticatedException(10004); 22 } 23 String token = tokens[1]; 24 Optional<Map<String, Claim>> stringClaimMap = JwtToken.verifyAndGetClaims(token); 25 Map<String, Claim> map = stringClaimMap.orElseThrow( 26 () -> new UnAuthenticatedException(40001)); 27 return this.hasPermission(scopeLevel.get(), map); 28 } 29 30 @Override 31 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 32 super.postHandle(request, response, handler, modelAndView); 33 } 34 35 @Override 36 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 37 super.afterCompletion(request, response, handler, ex); 38 } 39 40 @Override 41 public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 42 super.afterConcurrentHandlingStarted(request, response, handler); 43 } 44 45 /** 46 * 獲取請求方法上的scope權限 47 * 48 * @param handler 49 * @return 50 */ 51 private Optional<ScopeLevel> getScopeLevel(Object handler) { 52 if (handler instanceof HandlerMethod) { 53 HandlerMethod handlerMethod = (HandlerMethod) handler; 54 ScopeLevel scopeLevel = handlerMethod.getMethod().getAnnotation(ScopeLevel.class); 55 if(scopeLevel == null){ 56 return Optional.empty(); 57 } 58 return Optional.of(scopeLevel); 59 } 60 return Optional.empty(); 61 } 62 63 /** 64 * token中的權限與注解中的權限進行比較 判斷是否有權限進行訪問 65 * 66 * @param scopeLevel 權限注解 67 * @param map token中傳遞的參數 68 * @return 69 */ 70 private boolean hasPermission(ScopeLevel scopeLevel, Map<String, Claim> map) { 71 Integer level = scopeLevel.value(); 72 Integer scope = map.get("scope").asInt(); 73 if(level > scope){ 74 throw new ForbiddenException(10005); 75 } 76 return true; 77 } 78 }
將interceptor注冊到spring容器的啟動項中:
1 @Component 2 public class InterceptorConfiguration implements WebMvcConfigurer { 3 4 @Override 5 public void addInterceptors(InterceptorRegistry registry) { 6 // 將權限攔截器注冊到spring啟動項中 7 registry.addInterceptor(new PermissionInterceptor()); 8 } 9 }
內容出處:七月老師《從Java后端到全棧》視頻課程