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

  • 相关阅读:
    哈希值
    webpack配置(二)
    点击input选中文本
    height:calc(100%
    -webkit-overflow-scrolling
    字符串转数组
    gulp报错160
    webpack配置(一)
    移动端ios中click点击失效
    Spring定时器Quartz的用法
  • 原文地址:https://www.cnblogs.com/wunaozai/p/12522485.html
Copyright © 2020-2023  润新知