物聯網架構成長之路(56)-SpringCloudGateway+JWT實現網關鑒權


0. 前言
  結合前面兩篇博客,前面博客實現了Gateway網關的路由功能。此時,如果每個微服務都需要一套帳號認證體系就沒有必要了。可以在網關處進行權限認證。然后轉發請求到后端服務。這樣后面的微服務就可以直接調用,而不需要每個都單獨一套鑒權體系。參考了Oauth2和JWT,發現基於微服務,使用JWT會更方便一些,所以准備集成JWT作為微服務架構的認證方式。
  【https://www.cnblogs.com/wunaozai/p/12512753.html】  物聯網架構成長之路(54)-基於Nacos+Gateway實現動態路由
  【https://www.cnblogs.com/wunaozai/p/12512850.html】  物聯網架構成長之路(55)-Gateway+Sentinel實現限流、熔斷

 

1. Gateway增加一個過濾器
  在上一篇博客中實現的Gateway,增加一個AuthFilter過濾器。目的就是對所有的請求進行認證。
  代碼可以參考官方的幾個標准過濾器


  AuthFilter.java

  1 package com.wunaozai.demo.gateway.config.filter;
  2 
  3 import java.nio.charset.StandardCharsets;
  4 import java.util.Arrays;
  5 import java.util.List;
  6 import java.util.Map;
  7 
  8 import org.springframework.beans.factory.annotation.Autowired;
  9 import org.springframework.cloud.gateway.filter.GatewayFilterChain;
 10 import org.springframework.cloud.gateway.filter.GlobalFilter;
 11 import org.springframework.context.annotation.Bean;
 12 import org.springframework.context.annotation.Configuration;
 13 import org.springframework.core.annotation.Order;
 14 import org.springframework.core.io.buffer.DataBuffer;
 15 import org.springframework.http.HttpCookie;
 16 import org.springframework.http.HttpStatus;
 17 import org.springframework.http.server.reactive.ServerHttpRequest;
 18 import org.springframework.http.server.reactive.ServerHttpResponse;
 19 import org.springframework.util.MultiValueMap;
 20 import org.springframework.util.StringUtils;
 21 import org.springframework.web.client.RestTemplate;
 22 import org.springframework.web.server.ServerWebExchange;
 23 
 24 import com.wunaozai.demo.gateway.config.JsonResponseUtils;
 25 import reactor.core.publisher.Mono;
 26 
 27 @Configuration
 28 public class AuthFilter {
 29 
 30     private static final String JWT_TOKEN = "jwt-token";
 31     
 32     @Autowired
 33     private RestTemplate restTemplate;
 34     
 35     @Bean
 36     @Order
 37     public GlobalFilter authJWT() {
 38         GlobalFilter auth = new GlobalFilter() {
 39             @Override
 40             public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
 41                 System.out.println("filter auth....");
 42                 ServerHttpRequest request = exchange.getRequest();
 43                 ServerHttpResponse response = exchange.getResponse();
 44                 //判斷是否需要過濾
 45                 String path = request.getURI().getPath();
 46                 List<String> pages = Arrays.asList("/auth/v1/login", 
 47                         "/auth/v1/refresh", "/auth/v1/check");
 48                 for(int i=0; i<pages.size(); i++) {
 49                     if(pages.get(i).equals(path)) {
 50                         //直接通過,傳輸到下一級
 51                         return chain.filter(exchange); 
 52                     }
 53                 }
 54                 
 55                 //判斷是否存在JWT
 56                 String jwt = "";
 57                 List<String> headers = request.getHeaders().get(JWT_TOKEN);
 58                 if(headers != null && headers.size() > 0) {
 59                     jwt = headers.get(0);
 60                 }
 61                 if(StringUtils.isEmpty(jwt)) {
 62                     MultiValueMap<String, HttpCookie> cookies = request.getCookies();
 63                     if(cookies != null && cookies.size() > 0) {
 64                         List<HttpCookie> cookie = cookies.get(JWT_TOKEN);
 65                         if(cookie != null && cookie.size() > 0) {
 66                             HttpCookie ck = cookie.get(0);
 67                             jwt = ck.getValue();
 68                         }
 69                     }
 70                 }
 71                 if(StringUtils.isEmpty(jwt)) {
 72                     //返回未授權錯誤
 73                     return error(response, JsonResponseUtils.AUTH_UNLOGIN_ERROR);
 74                 }
 75 
 76                 //通過遠程調用判斷JWT是否合法
 77                 String json = "";
 78                 try {
 79                     Map<?, ?> ret = restTemplate.getForObject("http://jieli-story-auth/auth/v1/info?jwt=" + jwt, Map.class);
 80                     String code = ret.get("code").toString();
 81                     if(!"0".equals(code)) {
 82                         //返回認證錯誤
 83                         return error(response, JsonResponseUtils.AUTH_EXP_ERROR);
 84                     }
 85                     json = ret.get("data").toString();
 86                 } catch (Exception e) {
 87                     e.printStackTrace();
 88                     return error(response, JsonResponseUtils.AUTH_EXP_ERROR);
 89                 }
 90                 //將登錄信息保存到下一級
 91                 ServerHttpRequest newRequest = request.mutate().header("auth", json).build();
 92                 ServerWebExchange newExchange = 
 93                         exchange.mutate().request(newRequest).build();
 94                 return chain.filter(newExchange);
 95             }
 96         };
 97         return auth;
 98     }
 99 
100     private Mono<Void> error(ServerHttpResponse response, String json) {
101         //返回錯誤
102         response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
103         response.setStatusCode(HttpStatus.UNAUTHORIZED);
104         DataBuffer buffer = response.bufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8));
105         return response.writeWith(Mono.just(buffer));
106     }
107 }

  BeanConfig.java

 1 package com.wunaozai.demo.gateway.config.filter;
 2 
 3 import org.springframework.cloud.client.loadbalancer.LoadBalanced;
 4 import org.springframework.context.annotation.Bean;
 5 import org.springframework.stereotype.Component;
 6 import org.springframework.web.client.RestTemplate;
 7 
 8 @Component
 9 public class BeanConfig {
10     
11     /**
12      * 消費者
13      * @return
14      */
15     @Bean
16     @LoadBalanced
17     public RestTemplate restTemplate() {
18         return new RestTemplate();
19     }
20 }

  JsonResponseUtils.java

 1 package com.wunaozai.demo.gateway.config;
 2 
 3 /**
 4  * 常量返回
 5  * @author wunaozai
 6  * @Date 2020-03-18
 7  */
 8 public class JsonResponseUtils {
 9     
10     public static final String BLOCK_FLOW_ERROR = "{\"code\": -1, \"data\": null, \"msg\": \"系統限流\"}";
11     public static final String AUTH_UNLOGIN_ERROR = "{\"code\": -1, \"data\": null, \"msg\": \"未授權\"}";
12     public static final String AUTH_EXP_ERROR = "{\"code\": -1, \"data\": null, \"msg\": \"授權過期\"}";
13     public static final String AUTH_PARAM_ERROR = "{\"code\": -1, \"data\": null, \"msg\": \"參數異常\"}";
14     
15 }

 

2. Auth授權服務
  這里使用JWT作為微服務間的鑒權協議
  pom.xml

1         <!-- JWT -->
2         <dependency>
3             <groupId>io.jsonwebtoken</groupId>
4             <artifactId>jjwt</artifactId>
5             <version>0.9.1</version>
6         </dependency>

  AuthController.java(這里面包含了部分數據庫操作代碼,如果測試,刪除即可)

 1 package com.wunaozai.demo.auth.controller;
 2 
 3 import java.util.HashMap;
 4 import java.util.Map;
 5 
 6 import org.springframework.beans.factory.annotation.Autowired;
 7 import org.springframework.web.bind.annotation.RequestMapping;
 8 import org.springframework.web.bind.annotation.RestController;
 9 
10 import com.alibaba.fastjson.JSONObject;
11 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
12 import com.baomidou.mybatisplus.extension.api.R;
13 
14 import io.jsonwebtoken.Claims;
15 import com.wunaozai.demo.auth.common.utils.SecretUtils;
16 import com.wunaozai.demo.auth.common.utils.jwt.JWTToken;
17 import com.wunaozai.demo.auth.common.utils.jwt.JWTUtils;
18 import com.wunaozai.demo.auth.model.entity.AuthUserModel;
19 import com.wunaozai.demo.auth.service.IAuthUserService;
20 
21 @RestController
22 @RequestMapping(value="/auth/v1")
23 public class AuthController {
24 
25     @Autowired
26     private IAuthUserService authuserService;
27     
28     @RequestMapping(value="/login")
29     public R<Object> login(String username, String password, String type){
30         AuthUserModel user = getUser(username);
31         if(user == null) {
32             return R.failed("帳號密碼錯誤");
33         }
34         if(user.getStatus() == false) {
35             return R.failed("當前賬號被禁用");
36         }
37         if (checkPwd(user, password) == false) {
38             return R.failed("帳號密碼錯誤");
39         }
40         Map<String, String> map = new HashMap<>();
41         map.put("userId", user.getUserId().toString());
42         map.put("username", user.getUsername());
43         String body = JSONObject.toJSONString(map);
44         JWTToken token = JWTUtils.getJWT(body, "admin");
45         return R.ok(token);
46     }
47     @RequestMapping(value="/check")
48     public R<Object> check(String jwt){
49         boolean flag = JWTUtils.checkJWT(jwt);
50         return R.ok(flag);
51     }
52     @RequestMapping(value="/refresh")
53     public R<Object> refresh(String jwt){
54         boolean flag = JWTUtils.checkJWT(jwt);
55         if(flag == false) {
56             return R.ok("Token已過期");
57         }
58         JWTToken token = JWTUtils.refreshJWT(jwt);
59         return R.ok(token);
60     }
61     @RequestMapping(value="/info")
62     public R<Object> info(String jwt){
63         boolean flag = JWTUtils.checkJWT(jwt);
64         if(flag == false) {
65             return R.ok("Token已過期");
66         }
67         Claims claims = JWTUtils.infoJWT(jwt);
68         return R.ok(claims);
69     }
70     
71     /**
72      * 匹配密碼
73      * @param user
74      * @param password
75      * @return
76      */
77     private boolean checkPwd(AuthUserModel user, String password) {
78         if(user == null) {
79             return false;
80         }
81         return SecretUtils.matchBcryptPassword(password, user.getPassword());
82     }
83     /**
84      * 獲取用戶模型
85      * @param username
86      * @return
87      */
88     private AuthUserModel getUser(String username) {
89        QueryWrapper<AuthUserModel> query = new QueryWrapper<>();
90        query.eq("username", username);
91        return authuserService.getOne(query);
92     }
93 }

  JWTToken.java

 1 package com.wunaozai.demo.auth.common.utils.jwt;
 2 
 3 import lombok.Builder;
 4 import lombok.Getter;
 5 import lombok.Setter;
 6 
 7 @Getter
 8 @Setter
 9 @Builder
10 public class JWTToken {
11     private String access_token;
12     private String token_type;
13     private Long expires_in;
14 }

  JWTUtils.java

  1 package com.wunaozai.demo.auth.common.utils.jwt;
  2 
  3 import java.util.Base64;
  4 import java.util.Date;
  5 import java.util.UUID;
  6 
  7 import javax.crypto.SecretKey;
  8 import javax.crypto.spec.SecretKeySpec;
  9 
 10 import io.jsonwebtoken.Claims;
 11 import io.jsonwebtoken.JwtBuilder;
 12 import io.jsonwebtoken.Jwts;
 13 import io.jsonwebtoken.SignatureAlgorithm;
 14 
 15 /**
 16  * JWT 工具類
 17  * @author wunaozai
 18  * @Date 2020-03-18
 19  */
 20 public class JWTUtils {
 21 
 22     private static final String JWT_KEY = "test";
 23     /**
 24      * 生成JWT
 25      * @param body
 26      * @param role
 27      * @return
 28      */
 29     public static JWTToken getJWT(String body, String role) {
 30         Long expires_in = 1000 * 60 * 60 * 24L; //一天
 31         long time = System.currentTimeMillis();
 32         time = time + expires_in;
 33         JwtBuilder builder = Jwts.builder()
 34                 .setId(UUID.randomUUID().toString()) //設置唯一ID
 35                 .setSubject(body) //設置內容,這里用JSON包含帳號信息
 36                 .setIssuedAt(new Date()) //簽發時間
 37                 .setExpiration(new Date(time)) //過期時間
 38                 .claim("roles", role) //設置角色
 39                 .signWith(SignatureAlgorithm.HS256, generalKey()) //設置簽名 使用HS256算法,並設置密鑰
 40                 ;
 41         String code = builder.compact();
 42         JWTToken token = JWTToken.builder()
 43                                         .access_token(code)
 44                                         .expires_in(expires_in / 1000)
 45                                         .token_type("JWT")
 46                                         .build();
 47         return token;
 48     }
 49     /**
 50      * 解析JWT
 51      * @param jwt
 52      * @return
 53      */
 54     public static Claims parseJWT(String jwt) {
 55         Claims body = Jwts.parser().setSigningKey(generalKey()).parseClaimsJws(jwt).getBody();
 56         return body;
 57     }
 58     /**
 59      * 刷新JWT
 60      * @param jwt
 61      * @return
 62      */
 63     public static JWTToken refreshJWT(String jwt) {
 64         Claims claims = parseJWT(jwt);
 65         String body = claims.getSubject();
 66         String role = claims.get("roles").toString();
 67         return getJWT(body, role);
 68     }
 69     /**
 70      * 獲取JWT信息
 71      * @param jwt
 72      * @return
 73      */
 74     public static Claims infoJWT(String jwt) {
 75         Claims claims = parseJWT(jwt);
 76         return claims;
 77     }
 78     /**
 79      * 驗證JWT
 80      * @param jwt
 81      * @return
 82      */
 83     public static boolean checkJWT(String jwt) {
 84         try {
 85             Claims body = Jwts.parser().setSigningKey(generalKey()).parseClaimsJws(jwt).getBody();
 86             if(body != null) {
 87                 return true;
 88             }
 89         } catch (Exception e) {
 90             return false;
 91         }
 92         return false;
 93     }
 94 
 95     /**
 96      * 生成加密后的秘鑰 secretKey
 97      * @return
 98      */
 99     public static SecretKey generalKey() {
100         byte[] encodedKey = Base64.getDecoder().decode(JWT_KEY);
101         SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
102         return key;
103     }
104 }

 

3. Res資源服務
  測試是否轉發到后端服務
  IndexController.java

 1 package com.wunaozai.demo.res.controller.web;
 2 
 3 import java.util.HashMap;
 4 import java.util.Map;
 5 
 6 import javax.servlet.http.HttpServletRequest;
 7 
 8 import org.springframework.beans.factory.annotation.Autowired;
 9 import org.springframework.web.bind.annotation.RequestBody;
10 import org.springframework.web.bind.annotation.RequestMapping;
11 import org.springframework.web.bind.annotation.RestController;
12 
13 import com.baomidou.mybatisplus.extension.api.R;
14 
15 @RestController
16 @RequestMapping(value="/res/v1/")
17 public class IndexController {
18 
19     @Autowired
20     private HttpServletRequest request;
21     
22     @RequestMapping(value="/login")
23     public R<Map<String, Object>> login(){
24         Map<String, Object> data = new HashMap<String, Object>();
25         data.put("", "");
26         return R.ok(data);
27     }
28     
29     @RequestMapping(value="/test")
30     public R<Object> test(String msg, @RequestBody String body){
31         System.out.println(msg);
32         System.out.println(body);
33         System.out.println(request.getHeader("auth"));
34         return R.ok("ok");
35     }
36 }

 

4. 系統架構圖
  整體的架構流程圖,就是一個請求經過Nginx,進行前后端分離。后端請求轉發到Gateway,Gateway通過Nacos上配置的route(路由轉發規則,限流Sentinel規則)。判斷是否攜帶JWT-Token信息,請求訪問Auth授權服務,查詢是否正確的JWT-Token合法用戶。如果是合法用戶,將對應的請求轉發到后端各個微服務中,以本例子,將/res/v1 開頭轉發到StoryRes服務,將/aiml/v1 開頭的請求轉發到StoryAIML服務。
  架構流程圖

  各個微服務

  各個微服務注冊到Nacos上

  本項目所有Nacos上的配置信息

 

5. 測試過程
  通過PostMan進行模擬測試
5.1 請求/auth/v1/login
  注意保存返回的access_token,以后每次請求都需要設置到Header上

 

5.2 請求/auth/v1/info
  注意將jwt-token設置到Header上,這里就是返回用戶信息。一般是給后端服務查詢用的。不會暴露給用戶。可以看到AuthFilter.java 這個類就是調用這個微服務,實現驗證當前用戶是否合法。同時將這個返回保存到Header上,並將登錄信息保存到下一級。這樣后面的微服務可以通過判斷Header里面的這個登錄信息userId。作為外鍵。

 

5.3 請求/res/v1/test
  注意將jwt-token設置到Header上,這里模擬測試,通過QueryParam方式傳參數和Body傳參數。后端都是可以正常接收並打印

  后續就會出基於vue-element-admin的前端開發框架,結合到本項目。實現前后端分離。【期待】

 

參考資料:
  https://blog.csdn.net/tianyaleixiaowu/article/details/83375246
  https://www.cnblogs.com/fdzang/p/11812348.html

本文地址:https://www.cnblogs.com/wunaozai/p/12522485.html
本系列目錄: https://www.cnblogs.com/wunaozai/p/8067577.html
個人主頁:https://www.wunaozai.com/


免責聲明!

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



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