物联网架构成长之路(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/

posted @ 2020-04-01 09:56  无脑仔的小明  阅读(7067)  评论(0编辑  收藏  举报