全棧之路-小程序API-JWT令牌詳細剖析與使用


  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后端到全棧》視頻課程

七月老師課程鏈接:https://class.imooc.com/sale/javafullstack


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM