zoukankan      html  css  js  c++  java
  • 最全面的改造Zuul网关为Spring Cloud Gateway(包含Zuul核心实现和Spring Cloud Gateway核心实现)

    前言:

    最近开发了Zuul网关的实现和Spring Cloud Gateway实现,对比Spring Cloud Gateway发现后者性能好支持场景也丰富。在高并发或者复杂的分布式下,后者限流和自定义拦截也很棒。

     

    提示:

    本文主要列出本人开发的Zuul网关核心代码以及Spring Cloud Gateway核心代码实现。因为本人技术有限,主要是参照了 Spring Cloud Gateway 如有不足之处还请见谅并留言指出。

     

    1:为什么要做网关

    (1)网关层对外部和内部进行了隔离,保障了后台服务的安全性。
    (2)对外访问控制由网络层面转换成了运维层面,减少变更的流程和错误成本。
    (3)减少客户端与服务的耦合,服务可以独立运行,并通过网关层来做映射。
    (4)通过网关层聚合,减少外部访问的频次,提升访问效率。
    (5)节约后端服务开发成本,减少上线风险。
    (6)为服务熔断,灰度发布,线上测试提供简单方案。
    (7)便于进行应用层面的扩展。 
     
    相信在寻找相关资料的伙伴应该都知道,在微服务环境下,要做到一个比较健壮的流量入口还是很重要的,需要考虑的问题也比较复杂和众多。
     
    2:网关和鉴权基本实现架构(图中包含了auth组件,或SSO,文章结尾会提供此组件的实现)
     
     
    3:Zuul的实现
     
    (1)第一代的zuul使用的是netflix开发的,在pom引用上都是用的原来的。
    复制代码
     1        <!-- zuul网关最基本要用到的 -->
     2        <!-- 封装原来的jedis,用处是在网关里来放token到redis或者调redis来验证当前是否有效,或者说直接用redis负载-->
     3        <dependency>
     4             <groupId>org.springframework.boot</groupId>
     5             <artifactId>spring-boot-starter-data-redis</artifactId>
     6         </dependency>
     7         <!-- 客户端注册eureka使用的,微服务必备 -->
     8         <dependency>
     9             <groupId>org.springframework.cloud</groupId>
    10             <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    11         </dependency>
    12         <!-- zuul -->
    13         <dependency>
    14             <groupId>org.springframework.cloud</groupId>
    15             <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    16         </dependency>
    17        <!-- 熔断支持 -->
    18       <dependency>
    19             <groupId>org.springframework.cloud</groupId>
    20             <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    21         </dependency>
    22         <!--负载均衡 -->
    23         <dependency>
    24             <groupId>org.springframework.cloud</groupId>
    25             <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    26         </dependency>
    27         <!-- 调用feign -->
    28         <dependency>
    29             <groupId>org.springframework.cloud</groupId>
    30             <artifactId>spring-cloud-starter-openfeign</artifactId>
    31         </dependency>
    32         <!-- 健康 -->
    33         <dependency>
    34             <groupId>org.springframework.boot</groupId>
    35             <artifactId>spring-boot-starter-actuator</artifactId>
    36         </dependency>
    复制代码

    (2)修改application-dev.yml 的内容

    给个提示,在原来的starter-web中 yml的 context-path是不需要用的,微服务中只需要用application-name去注册中心找实例名即可,况且webflux后context-path已经不存在了。

    复制代码
     1 spring:
     2   application:
     3     name: gateway
     4 
     5 #eureka-gateway-monitor-config 每个端口+1
     6 server:
     7   port: 8702
     8 
     9 #eureka注册配置
    10 eureka:
    11   instance:
    12     #使用IP注册
    13     prefer-ip-address: true
    14     ##续约更新时间间隔设置5秒,m默认30s
    15     lease-renewal-interval-in-seconds: 30
    16     ##续约到期时间10秒,默认是90秒
    17     lease-expiration-duration-in-seconds: 90
    18   client:
    19     serviceUrl:
    20       defaultZone: http://localhost:8700/eureka/
    21 
    22 # route connection
    23 zuul:
    24   host:
    25     #单个服务最大请求
    26     max-per-route-connections: 20
    27     #网关最大连接数
    28     max-total-connections: 200
    29     #routes to serviceId
    30     routes:
    31       api-product.path: /api/product/**
    32       api-product.serviceId: product
    33       api-customer.path: /api/customer/**
    34       api-customer.serviceId: customer
    35 
    36 
    37 
    38 #移除url同时移除服务
    39 auth-props:
    40   #accessIp: 127.0.0.1
    41   #accessToken: admin
    42   #authLevel: dev
    43   #服务
    44   api-urlMap: {
    45     product: 1&2,
    46     customer: 1&1
    47   }
    48   #移除url同时移除服务
    49   exclude-urls:
    50     - /pro
    51     - /cust
    52 
    53 
    54 #断路时间
    55 hystrix:
    56   command:
    57     default:
    58       execution:
    59         isolation:
    60           thread:
    61             timeoutInMilliseconds: 300000
    62 
    63 #ribbon
    64 ribbon:
    65   ReadTimeout: 15000
    66   ConnectTimeout: 15000
    67   SocketTimeout: 15000
    68   eager-load:
    69     enabled: true
    70     clients: product, customer
    复制代码

    如果仅仅是转发,那很简单,如果要做好场景,则需要添加白名单和黑名单,在zuul里只需要加白名单即可,存在链接或者实例名才能通过filter转发。

    重点在:

    api-urlMap: 是实例名,如果链接不存在才会去校验,因为端口+链接可以访问,如果加实例名一起也能访问,防止恶意带实例名攻击或者抓包请求后去猜链接后缀来攻击。
    exclude-urls: 白名单连接,每个微服务的请求入口地址,包含即通过。

     
    (3)上面提到白名单,那需要初始化白名单
    复制代码
     1 package org.yugh.gateway.config;
     2 
     3 import lombok.Data;
     4 import lombok.extern.slf4j.Slf4j;
     5 import org.springframework.beans.factory.InitializingBean;
     6 import org.springframework.boot.context.properties.ConfigurationProperties;
     7 import org.springframework.context.annotation.Configuration;
     8 import org.springframework.stereotype.Component;
     9 
    10 import java.util.ArrayList;
    11 import java.util.List;
    12 import java.util.Map;
    13 import java.util.regex.Pattern;
    14 
    15 /**
    16  * //路由拦截配置
    17  *
    18  * @author: 余根海
    19  * @creation: 2019-07-02 19:43
    20  * @Copyright © 2019 yugenhai. All rights reserved.
    21  */
    22 @Data
    23 @Slf4j
    24 @Component
    25 @Configuration
    26 @ConfigurationProperties(prefix = "auth-props")
    27 public class ZuulPropConfig implements InitializingBean {
    28 
    29     private static final String normal = "(\w|\d|-)+";
    30     private List<Pattern> patterns = new ArrayList<>();
    31     private Map<String, String> apiUrlMap;
    32     private List<String> excludeUrls;
    33     private String accessToken;
    34     private String accessIp;
    35     private String authLevel;
    36 
    37     @Override
    38     public void afterPropertiesSet() throws Exception {
    39         excludeUrls.stream().map(s -> s.replace("*", normal)).map(Pattern::compile).forEach(patterns::add);
    40         log.info("============> 配置的白名单Url:{}", patterns);
    41     }
    42 
    43 
    44 }
    复制代码

     

    (4)核心代码zuulFilter

    复制代码
      1 package org.yugh.gateway.filter;
      2 
      3 import com.netflix.zuul.ZuulFilter;
      4 import com.netflix.zuul.context.RequestContext;
      5 import lombok.extern.slf4j.Slf4j;
      6 import org.springframework.beans.factory.annotation.Autowired;
      7 import org.springframework.beans.factory.annotation.Value;
      8 import org.springframework.util.CollectionUtils;
      9 import org.springframework.util.StringUtils;
     10 import org.yugh.gateway.common.constants.Constant;
     11 import org.yugh.gateway.common.enums.DeployEnum;
     12 import org.yugh.gateway.common.enums.HttpStatusEnum;
     13 import org.yugh.gateway.common.enums.ResultEnum;
     14 import org.yugh.gateway.config.RedisClient;
     15 import org.yugh.gateway.config.ZuulPropConfig;
     16 import org.yugh.gateway.util.ResultJson;
     17 
     18 import javax.servlet.http.Cookie;
     19 import javax.servlet.http.HttpServletRequest;
     20 import javax.servlet.http.HttpServletResponse;
     21 import java.util.Arrays;
     22 import java.util.HashMap;
     23 import java.util.Map;
     24 import java.util.function.Function;
     25 import java.util.regex.Matcher;
     26 
     27 /**
     28  * //路由拦截转发请求
     29  *
     30  * @author: 余根海
     31  * @creation: 2019-06-26 17:50
     32  * @Copyright © 2019 yugenhai. All rights reserved.
     33  */
     34 @Slf4j
     35 public class PreAuthFilter extends ZuulFilter {
     36 
     37 
     38     @Value("${spring.profiles.active}")
     39     private String activeType;
     40     @Autowired
     41     private ZuulPropConfig zuulPropConfig;
     42     @Autowired
     43     private RedisClient redisClient;
     44 
     45     @Override
     46     public String filterType() {
     47         return "pre";
     48     }
     49 
     50     @Override
     51     public int filterOrder() {
     52         return 0;
     53     }
     54 
     55 
     56     /**
     57      * 部署级别可调控
     58      *
     59      * @return
     60      * @author yugenhai
     61      * @creation: 2019-06-26 17:50
     62      */
     63     @Override
     64     public boolean shouldFilter() {
     65         RequestContext context = RequestContext.getCurrentContext();
     66         HttpServletRequest request = context.getRequest();
     67         if (activeType.equals(DeployEnum.DEV.getType())) {
     68             log.info("请求地址 : {}      当前环境  : {} ", request.getServletPath(), DeployEnum.DEV.getType());
     69             return true;
     70         } else if (activeType.equals(DeployEnum.TEST.getType())) {
     71             log.info("请求地址 : {}      当前环境  : {} ", request.getServletPath(), DeployEnum.TEST.getType());
     72             return true;
     73         } else if (activeType.equals(DeployEnum.PROD.getType())) {
     74             log.info("请求地址 : {}      当前环境  : {} ", request.getServletPath(), DeployEnum.PROD.getType());
     75             return true;
     76         }
     77         return true;
     78     }
     79 
     80 
     81     /**
     82      * 路由拦截转发
     83      *
     84      * @return
     85      * @author yugenhai
     86      * @creation: 2019-06-26 17:50
     87      */
     88     @Override
     89     public Object run() {
     90         RequestContext context = RequestContext.getCurrentContext();
     91         HttpServletRequest request = context.getRequest();
     92         String requestMethod = context.getRequest().getMethod();
     93         //判断请求方式
     94         if (Constant.OPTIONS.equals(requestMethod)) {
     95             log.info("请求的跨域的地址 : {}   跨域的方法", request.getServletPath(), requestMethod);
     96             assemblyCross(context);
     97             context.setResponseStatusCode(HttpStatusEnum.OK.code());
     98             context.setSendZuulResponse(false);
     99             return null;
    100         }
    101         //转发信息共享 其他服务不要依赖MVC拦截器,或重写拦截器
    102         if (isIgnore(request, this::exclude, this::checkLength)) {
    103             String token = getCookieBySso(request);
    104             if(!StringUtils.isEmpty(token)){
    105                 //context.addZuulRequestHeader(JwtUtil.HEADER_AUTH, token);
    106             }
    107             log.info("请求白名单地址 : {} ", request.getServletPath());
    108             return null;
    109         }
    110         String serverName = request.getServletPath().substring(1, request.getServletPath().indexOf('/', 1));
    111         String authUserType = zuulPropConfig.getApiUrlMap().get(serverName);
    112         log.info("实例服务名: {}  对应用户类型: {}", serverName, authUserType);
    113         if (!StringUtils.isEmpty(authUserType)) {
    114             //用户是否合法和登录
    115             authToken(context);
    116         } else {
    117             //下线前删除配置的实例名
    118             log.info("实例服务: {}  不允许访问", serverName);
    119             unauthorized(context, HttpStatusEnum.FORBIDDEN.code(), "请求的服务已经作废,不可访问");
    120         }
    121         return null;
    122 
    123         /******************************以下代码可能会复用,勿删,若使用Gateway整个路由项目将不使用 add by - yugenhai 2019-0704********************************************/
    124 
    125         /*String readUrl = request.getServletPath().substring(1, request.getServletPath().indexOf('/', 1));
    126         try {
    127             if (request.getServletPath().length() <= Constant.PATH_LENGTH || zuulPropConfig.getRoutes().size() == 0) {
    128                 throw new Exception();
    129             }
    130             Iterator<Map.Entry<String,String>> zuulMap = zuulPropConfig.getRoutes().entrySet().iterator();
    131             while(zuulMap.hasNext()){
    132                 Map.Entry<String, String> entry = zuulMap.next();
    133                 String routeValue = entry.getValue();
    134                 if(routeValue.startsWith(Constant.ZUUL_PREFIX)){
    135                     routeValue = routeValue.substring(1, routeValue.indexOf('/', 1));
    136                 }
    137                 if(routeValue.contains(readUrl)){
    138                     log.info("请求白名单地址 : {}     请求跳过的真实地址  :{} ", routeValue, request.getServletPath());
    139                     return null;
    140                 }
    141             }
    142             log.info("即将请求登录 : {}       实例名 : {} ", request.getServletPath(), readUrl);
    143             authToken(context);
    144             return null;
    145         } catch (Exception e) {
    146             log.info("gateway路由器请求异常 :{}  请求被拒绝 ", e.getMessage());
    147             assemblyCross(context);
    148             context.set("isSuccess", false);
    149             context.setSendZuulResponse(false);
    150             context.setResponseStatusCode(HttpStatusEnum.OK.code());
    151             context.getResponse().setContentType("application/json;charset=UTF-8");
    152             context.setResponseBody(JsonUtils.toJson(JsonResult.buildErrorResult(HttpStatusEnum.UNAUTHORIZED.code(),"Url Error, Please Check It")));
    153             return null;
    154         }
    155         */
    156     }
    157 
    158 
    159     /**
    160      * 检查用户
    161      *
    162      * @param context
    163      * @return
    164      * @author yugenhai
    165      * @creation: 2019-06-26 17:50
    166      */
    167     private Object authToken(RequestContext context) {
    168         HttpServletRequest request = context.getRequest();
    169         HttpServletResponse response = context.getResponse();
    170         /*boolean isLogin = sessionManager.isLogined(request, response);
    171         //用户存在
    172         if (isLogin) {
    173             try {
    174                 User user = sessionManager.getUser(request);
    175                 log.info("用户存在 : {} ", JsonUtils.toJson(user));
    176                // String token = userAuthUtil.generateToken(user.getNo(), user.getUserName(), user.getRealName());
    177                 log.info("根据用户生成的Token :{}", token);
    178                 //转发信息共享
    179                // context.addZuulRequestHeader(JwtUtil.HEADER_AUTH, token);
    180                 //缓存 后期所有服务都判断
    181                 redisClient.set(user.getNo(), token, 20 * 60L);
    182                 //冗余一份
    183                 userService.syncUser(user);
    184             } catch (Exception e) {
    185                 log.error("调用SSO获取用户信息异常 :{}", e.getMessage());
    186             }
    187         } else {
    188             //根据该token查询该用户不存在
    189             unLogin(request, context);
    190         }*/
    191         return null;
    192 
    193     }
    194 
    195 
    196     /**
    197      * 未登录不路由
    198      *
    199      * @param request
    200      */
    201     private void unLogin(HttpServletRequest request, RequestContext context) {
    202         String requestURL = request.getRequestURL().toString();
    203         String loginUrl = getSsoUrl(request) + "?returnUrl=" + requestURL;
    204         //Map map = new HashMap(2);
    205         //map.put("redirctUrl", loginUrl);
    206         log.info("检查到该token对应的用户登录状态未登录  跳转到Login页面 : {} ", loginUrl);
    207         assemblyCross(context);
    208         context.getResponse().setContentType("application/json;charset=UTF-8");
    209         context.set("isSuccess", false);
    210         context.setSendZuulResponse(false);
    211         //context.setResponseBody(ResultJson.failure(map, "This User Not Found, Please Check Token").toString());
    212         context.setResponseStatusCode(HttpStatusEnum.OK.code());
    213     }
    214 
    215 
    216     /**
    217      * 判断是否忽略对请求的校验
    218      * @param request
    219      * @param functions
    220      * @return
    221      */
    222     private boolean isIgnore(HttpServletRequest request, Function<HttpServletRequest, Boolean>... functions) {
    223         return Arrays.stream(functions).anyMatch(f -> f.apply(request));
    224     }
    225 
    226 
    227     /**
    228      * 判断是否存在地址
    229      * @param request
    230      * @return
    231      */
    232     private boolean exclude(HttpServletRequest request) {
    233         String servletPath = request.getServletPath();
    234         if (!CollectionUtils.isEmpty(zuulPropConfig.getExcludeUrls())) {
    235             return zuulPropConfig.getPatterns().stream()
    236                     .map(pattern -> pattern.matcher(servletPath))
    237                     .anyMatch(Matcher::find);
    238         }
    239         return false;
    240     }
    241 
    242 
    243     /**
    244      * 校验请求连接是否合法
    245      * @param request
    246      * @return
    247      */
    248     private boolean checkLength(HttpServletRequest request) {
    249         return request.getServletPath().length() <= Constant.PATH_LENGTH || CollectionUtils.isEmpty(zuulPropConfig.getApiUrlMap());
    250     }
    251 
    252 
    253     /**
    254      * 会话存在则跨域发送
    255      * @param request
    256      * @return
    257      */
    258     private String getCookieBySso(HttpServletRequest request){
    259         Cookie cookie = this.getCookieByName(request, "");
    260         return cookie != null ? cookie.getValue() : null;
    261     }
    262 
    263 
    264     /**
    265      * 不路由直接返回
    266      * @param ctx
    267      * @param code
    268      * @param msg
    269      */
    270     private void unauthorized(RequestContext ctx, int code, String msg) {
    271         assemblyCross(ctx);
    272         ctx.getResponse().setContentType("application/json;charset=UTF-8");
    273         ctx.setSendZuulResponse(false);
    274         ctx.setResponseBody(ResultJson.failure(ResultEnum.UNAUTHORIZED, msg).toString());
    275         ctx.set("isSuccess", false);
    276         ctx.setResponseStatusCode(HttpStatusEnum.OK.code());
    277     }
    278 
    279 
    280     /**
    281      * 获取会话里的token
    282      * @param request
    283      * @param name
    284      * @return
    285      */
    286     private Cookie getCookieByName(HttpServletRequest request, String name) {
    287         Map<String, Cookie> cookieMap = new HashMap(16);
    288         Cookie[] cookies = request.getCookies();
    289         if (!StringUtils.isEmpty(cookies)) {
    290             Cookie[] c1 = cookies;
    291             int length = cookies.length;
    292             for(int i = 0; i < length; ++i) {
    293                 Cookie cookie = c1[i];
    294                 cookieMap.put(cookie.getName(), cookie);
    295             }
    296         }else {
    297             return null;
    298         }
    299         if (cookieMap.containsKey(name)) {
    300             Cookie cookie = cookieMap.get(name);
    301             return cookie;
    302         }
    303         return null;
    304     }
    305 
    306 
    307     /**
    308      * 重定向前缀拼接
    309      *
    310      * @param request
    311      * @return
    312      */
    313     private String getSsoUrl(HttpServletRequest request) {
    314         String serverName = request.getServerName();
    315         if (StringUtils.isEmpty(serverName)) {
    316             return "https://github.com/yugenhai108";
    317         }
    318         return "https://github.com/yugenhai108";
    319 
    320     }
    321 
    322     /**
    323      * 拼装跨域处理
    324      */
    325     private void assemblyCross(RequestContext ctx) {
    326         HttpServletResponse response = ctx.getResponse();
    327         response.setHeader("Access-Control-Allow-Origin", "*");
    328         response.setHeader("Access-Control-Allow-Headers", ctx.getRequest().getHeader("Access-Control-Request-Headers"));
    329         response.setHeader("Access-Control-Allow-Methods", "*");
    330     }
    331 
    332 
    333 }
    复制代码

     

    在 if (isIgnore(request, this::exclude, this::checkLength)) {  里面可以去调鉴权组件,或者用redis去存放token,获取直接用redis负载抗流量,具体可以自己实现。

     

    4:Spring Cloud Gateway的实现

    (1)第二代的Gateway则是由Spring Cloud开发,而且用了最新的Spring5.0和响应式Reactor以及最新的Webflux等等,比如原来的阻塞式请求现在变成了异步非阻塞式。
       那么在pom上就变了,变得和原来的starer-web也不兼容了。
    复制代码
     1         <dependency>
     2             <groupId>org.yugh</groupId>
     3             <artifactId>global-auth</artifactId>
     4             <version>0.0.1-SNAPSHOT</version>
     5             <exclusions>
     6                 <exclusion>
     7                     <groupId>org.springframework.boot</groupId>
     8                     <artifactId>spring-boot-starter-web</artifactId>
     9                 </exclusion>
    10             </exclusions>
    11         </dependency>
    12         <!-- gateway -->
    13         <dependency>
    14             <groupId>org.springframework.cloud</groupId>
    15             <artifactId>spring-cloud-starter-gateway</artifactId>
    16         </dependency>
    17         <dependency>
    18             <groupId>org.springframework.cloud</groupId>
    19             <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    20         </dependency>
    21         <!-- feign -->
    22         <dependency>
    23             <groupId>org.springframework.cloud</groupId>
    24             <artifactId>spring-cloud-starter-openfeign</artifactId>
    25         </dependency>
    26         <dependency>
    27             <groupId>org.springframework.boot</groupId>
    28             <artifactId>spring-boot-starter-actuator</artifactId>
    29         </dependency>
    30         <dependency>
    31             <groupId>org.springframework.boot</groupId>
    32             <artifactId>spring-boot-configuration-processor</artifactId>
    33         </dependency>
    34         <!-- redis -->
    35         <dependency>
    36             <groupId>org.springframework.boot</groupId>
    37             <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    38         </dependency>
    39         <dependency>
    40             <groupId>com.google.guava</groupId>
    41             <artifactId>guava</artifactId>
    42             <version>23.0</version>
    43         </dependency>
    44         <dependency>
    45             <groupId>org.springframework.boot</groupId>
    46             <artifactId>spring-boot-starter-test</artifactId>
    47             <scope>test</scope>
    48         </dependency>
    复制代码

     

    (2)修改application-dev.yml 的内容

    复制代码
      1 server:
      2   port: 8706
      3 #setting
      4 spring:
      5   application:
      6     name: gateway-new
      7   #redis
      8   redis:
      9     host: localhost
     10     port: 6379
     11     database: 0
     12     timeout: 5000
     13   #遇到相同名字,允许覆盖
     14   main:
     15     allow-bean-definition-overriding: true
     16   #gateway
     17   cloud:
     18     gateway:
     19       #注册中心服务发现
     20       discovery:
     21         locator:
     22           #开启通过服务中心的自动根据 serviceId 创建路由的功能
     23           enabled: true
     24       routes:
     25         #服务1
     26         - id: CompositeDiscoveryClient_CUSTOMER
     27           uri: lb://CUSTOMER
     28           order: 1
     29           predicates:
     30             # 跳过自定义是直接带实例名 必须是大写 同样限流拦截失效
     31             - Path= /api/customer/**
     32           filters:
     33             - StripPrefix=2
     34             - AddResponseHeader=X-Response-Default-Foo, Default-Bar
     35             - name: RequestRateLimiter
     36               args:
     37                 key-resolver: "#{@gatewayKeyResolver}"
     38                 #限额配置
     39                 redis-rate-limiter.replenishRate: 1
     40                 redis-rate-limiter.burstCapacity: 1
     41         #用户微服务
     42         - id: CompositeDiscoveryClient_PRODUCT
     43           uri: lb://PRODUCT
     44           order: 0
     45           predicates:
     46             - Path= /api/product/**
     47           filters:
     48             - StripPrefix=2
     49             - AddResponseHeader=X-Response-Default-Foo, Default-Bar
     50             - name: RequestRateLimiter
     51               args:
     52                 key-resolver: "#{@gatewayKeyResolver}"
     53                 #限额配置
     54                 redis-rate-limiter.replenishRate: 1
     55                 redis-rate-limiter.burstCapacity: 1
     56           #请求路径选择自定义会进入限流器
     57           default-filters:
     58             - AddResponseHeader=X-Response-Default-Foo, Default-Bar
     59             - name: gatewayKeyResolver
     60               args:
     61                 key-resolver: "#{@gatewayKeyResolver}"
     62               #断路异常跳转
     63             - name: Hystrix
     64               args:
     65                 #网关异常或超时跳转到处理类
     66                 name: fallbackcmd
     67                 fallbackUri: forward:/fallbackController
     68 
     69 #safe path
     70 auth-skip:
     71   instance-servers:
     72     - CUSTOMER
     73     - PRODUCT
     74   api-urls:
     75     #PRODUCT
     76     - /pro
     77     #CUSTOMER
     78     - /cust
     79 
     80     #gray-env
     81     #...
     82 
     83 #log
     84 logging:
     85   level:
     86     org.yugh: INFO
     87     org.springframework.cloud.gateway: INFO
     88     org.springframework.http.server.reactive: INFO
     89     org.springframework.web.reactive: INFO
     90     reactor.ipc.netty: INFO
     91 
     92 #reg
     93 eureka:
     94   instance:
     95     prefer-ip-address: true
     96   client:
     97     serviceUrl:
     98       defaultZone: http://localhost:8700/eureka/
     99 
    100 
    101 ribbon:
    102   eureka:
    103     enabled: true
    104   ReadTimeout: 120000
    105   ConnectTimeout: 30000
    106 
    107 
    108 #feign
    109 feign:
    110   hystrix:
    111     enabled: false
    112 
    113 #hystrix
    114 hystrix:
    115   command:
    116     default:
    117       execution:
    118         isolation:
    119           thread:
    120             timeoutInMilliseconds: 20000
    121 
    122 management:
    123   endpoints:
    124     web:
    125       exposure:
    126         include: '*'
    127       base-path: /actuator
    128   endpoint:
    129     health:
    130       show-details: ALWAYS
    复制代码

     

    网关限流用的 spring-boot-starter-data-redis-reactive 做令牌桶IP限流。

    具体实现在这个类gatewayKeyResolver

     

    (3)令牌桶IP限流,限制当前IP的请求配额

    复制代码
     1 package org.yugh.gatewaynew.config;
     2 
     3 import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
     4 import org.springframework.stereotype.Component;
     5 import org.springframework.web.server.ServerWebExchange;
     6 import reactor.core.publisher.Mono;
     7 
     8 /**
     9  * //令牌桶IP限流
    10  *
    11  * @author 余根海
    12  * @creation 2019-07-05 15:52
    13  * @Copyright © 2019 yugenhai. All rights reserved.
    14  */
    15 @Component
    16 public class GatewayKeyResolver implements KeyResolver {
    17 
    18     @Override
    19     public Mono<String> resolve(ServerWebExchange exchange) {
    20         return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    21     }
    22 
    23 }
    复制代码

     

    (4)网关的白名单和黑名单配置

    复制代码
     1 package org.yugh.gatewaynew.properties;
     2 
     3 
     4 import lombok.Data;
     5 import lombok.extern.slf4j.Slf4j;
     6 import org.springframework.beans.factory.InitializingBean;
     7 import org.springframework.boot.context.properties.ConfigurationProperties;
     8 import org.springframework.context.annotation.Configuration;
     9 import org.springframework.stereotype.Component;
    10 
    11 import java.util.ArrayList;
    12 import java.util.List;
    13 import java.util.regex.Pattern;
    14 
    15 /**
    16  * //白名单和黑名单属性配置
    17  *
    18  * @author  余根海
    19  * @creation  2019-07-05 15:52
    20  * @Copyright © 2019 yugenhai. All rights reserved.
    21  */
    22 @Data
    23 @Slf4j
    24 @Component
    25 @Configuration
    26 @ConfigurationProperties(prefix = "auth-skip")
    27 public class AuthSkipUrlsProperties implements InitializingBean {
    28 
    29     private static final String NORMAL = "(\w|\d|-)+";
    30     private List<Pattern> urlPatterns = new ArrayList(10);
    31     private List<Pattern> serverPatterns = new ArrayList(10);
    32     private List<String> instanceServers;
    33     private List<String> apiUrls;
    34 
    35     @Override
    36     public void afterPropertiesSet() {
    37         instanceServers.stream().map(d -> d.replace("*", NORMAL)).map(Pattern::compile).forEach(serverPatterns::add);
    38         apiUrls.stream().map(s -> s.replace("*", NORMAL)).map(Pattern::compile).forEach(urlPatterns::add);
    39         log.info("============> 配置服务器ID : {} , 白名单Url : {}", serverPatterns, urlPatterns);
    40     }
    41 
    42 }
    复制代码

     

    (5)核心网关代码GatewayFilter

    复制代码
      1 package org.yugh.gatewaynew.filter;
      2 
      3 import lombok.extern.slf4j.Slf4j;
      4 import org.springframework.beans.factory.annotation.Autowired;
      5 import org.springframework.beans.factory.annotation.Qualifier;
      6 import org.springframework.cloud.gateway.filter.GatewayFilterChain;
      7 import org.springframework.cloud.gateway.filter.GlobalFilter;
      8 import org.springframework.core.Ordered;
      9 import org.springframework.core.io.buffer.DataBuffer;
     10 import org.springframework.http.HttpStatus;
     11 import org.springframework.http.MediaType;
     12 import org.springframework.http.server.reactive.ServerHttpRequest;
     13 import org.springframework.http.server.reactive.ServerHttpResponse;
     14 import org.springframework.util.CollectionUtils;
     15 import org.springframework.web.server.ServerWebExchange;
     16 import org.yugh.gatewaynew.config.GatewayContext;
     17 import org.yugh.gatewaynew.properties.AuthSkipUrlsProperties;
     18 import org.yugh.globalauth.common.constants.Constant;
     19 import org.yugh.globalauth.common.enums.ResultEnum;
     20 import org.yugh.globalauth.pojo.dto.User;
     21 import org.yugh.globalauth.service.AuthService;
     22 import org.yugh.globalauth.util.ResultJson;
     23 import reactor.core.publisher.Flux;
     24 import reactor.core.publisher.Mono;
     25 
     26 import java.nio.charset.StandardCharsets;
     27 import java.util.concurrent.ExecutorService;
     28 import java.util.regex.Matcher;
     29 
     30 /**
     31  * // 网关服务
     32  *
     33  * @author 余根海
     34  * @creation 2019-07-09 10:52
     35  * @Copyright © 2019 yugenhai. All rights reserved.
     36  */
     37 @Slf4j
     38 public class GatewayFilter implements GlobalFilter, Ordered {
     39 
     40     @Autowired
     41     private AuthSkipUrlsProperties authSkipUrlsProperties;
     42     @Autowired
     43     @Qualifier(value = "gatewayQueueThreadPool")
     44     private ExecutorService buildGatewayQueueThreadPool;
     45     @Autowired
     46     private AuthService authService;
     47 
     48 
     49     @Override
     50     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
     51         GatewayContext context = new GatewayContext();
     52         ServerHttpRequest request = exchange.getRequest();
     53         ServerHttpResponse response = exchange.getResponse();
     54         response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
     55         log.info("当前会话ID : {}", request.getId());
     56         //防止网关监控不到限流请求
     57         if (blackServersCheck(context, exchange)) {
     58             response.setStatusCode(HttpStatus.FORBIDDEN);
     59             byte[] failureInfo = ResultJson.failure(ResultEnum.BLACK_SERVER_FOUND).toString().getBytes(StandardCharsets.UTF_8);
     60             DataBuffer buffer = response.bufferFactory().wrap(failureInfo);
     61             return response.writeWith(Flux.just(buffer));
     62         }
     63         //白名单
     64         if (whiteListCheck(context, exchange)) {
     65             authToken(context, request);
     66             if (!context.isDoNext()) {
     67                 byte[] failureInfo = ResultJson.failure(ResultEnum.LOGIN_ERROR_GATEWAY, context.getRedirectUrl()).toString().getBytes(StandardCharsets.UTF_8);
     68                 DataBuffer buffer = response.bufferFactory().wrap(failureInfo);
     69                 response.setStatusCode(HttpStatus.UNAUTHORIZED);
     70                 return response.writeWith(Flux.just(buffer));
     71             }
     72             ServerHttpRequest mutateReq = exchange.getRequest().mutate().header(Constant.TOKEN, context.getSsoToken()).build();
     73             ServerWebExchange mutableExchange = exchange.mutate().request(mutateReq).build();
     74             log.info("当前会话转发成功 : {}", request.getId());
     75             return chain.filter(mutableExchange);
     76         } else {
     77             //黑名单
     78             response.setStatusCode(HttpStatus.FORBIDDEN);
     79             byte[] failureInfo = ResultJson.failure(ResultEnum.WHITE_NOT_FOUND).toString().getBytes(StandardCharsets.UTF_8);
     80             DataBuffer buffer = response.bufferFactory().wrap(failureInfo);
     81             return response.writeWith(Flux.just(buffer));
     82         }
     83     }
     84 
     85 
     86     @Override
     87     public int getOrder() {
     88         return Integer.MIN_VALUE;
     89     }
     90 
     91     /**
     92      * 检查用户
     93      *
     94      * @param context
     95      * @param request
     96      * @return
     97      * @author yugenhai
     98      */
     99     private void authToken(GatewayContext context, ServerHttpRequest request) {
    100         try {
    101             // boolean isLogin = authService.isLoginByReactive(request);
    102             boolean isLogin = true;
    103             if (isLogin) {
    104                 //User userDo = authService.getUserByReactive(request);
    105                 try {
    106                     // String ssoToken = authCookieUtils.getCookieByNameByReactive(request, Constant.TOKEN);
    107                     String ssoToken = "123";
    108                     context.setSsoToken(ssoToken);
    109                 } catch (Exception e) {
    110                     log.error("用户调用失败 : {}", e.getMessage());
    111                     context.setDoNext(false);
    112                     return;
    113                 }
    114             } else {
    115                 unLogin(context, request);
    116             }
    117         } catch (Exception e) {
    118             log.error("获取用户信息异常 :{}", e.getMessage());
    119             context.setDoNext(false);
    120         }
    121     }
    122 
    123 
    124     /**
    125      * 网关同步用户
    126      *
    127      * @param userDto
    128      */
    129     public void synUser(User userDto) {
    130         buildGatewayQueueThreadPool.execute(new Runnable() {
    131             @Override
    132             public void run() {
    133                 log.info("用户同步成功 : {}", "");
    134             }
    135         });
    136 
    137     }
    138 
    139 
    140     /**
    141      * 视为不能登录
    142      *
    143      * @param context
    144      * @param request
    145      */
    146     private void unLogin(GatewayContext context, ServerHttpRequest request) {
    147         String loginUrl = getSsoUrl(request) + "?returnUrl=" + request.getURI();
    148         context.setRedirectUrl(loginUrl);
    149         context.setDoNext(false);
    150         log.info("检查到该token对应的用户登录状态未登录  跳转到Login页面 : {} ", loginUrl);
    151     }
    152 
    153 
    154     /**
    155      * 白名单
    156      *
    157      * @param context
    158      * @param exchange
    159      * @return
    160      */
    161     private boolean whiteListCheck(GatewayContext context, ServerWebExchange exchange) {
    162         String url = exchange.getRequest().getURI().getPath();
    163         boolean white = authSkipUrlsProperties.getUrlPatterns().stream()
    164                 .map(pattern -> pattern.matcher(url))
    165                 .anyMatch(Matcher::find);
    166         if (white) {
    167             context.setPath(url);
    168             return true;
    169         }
    170         return false;
    171     }
    172 
    173 
    174     /**
    175      * 黑名单
    176      *
    177      * @param context
    178      * @param exchange
    179      * @return
    180      */
    181     private boolean blackServersCheck(GatewayContext context, ServerWebExchange exchange) {
    182         String instanceId = exchange.getRequest().getURI().getPath().substring(1, exchange.getRequest().getURI().getPath().indexOf('/', 1));
    183         if (!CollectionUtils.isEmpty(authSkipUrlsProperties.getInstanceServers())) {
    184             boolean black = authSkipUrlsProperties.getServerPatterns().stream()
    185                     .map(pattern -> pattern.matcher(instanceId))
    186                     .anyMatch(Matcher::find);
    187             if (black) {
    188                 context.setBlack(true);
    189                 return true;
    190             }
    191         }
    192         return false;
    193     }
    194 
    195 
    196     /**
    197      * @param request
    198      * @return
    199      */
    200     private String getSsoUrl(ServerHttpRequest request) {
    201         return request.getPath().value();
    202     }
    203 
    204 }
    复制代码

     

    在 private void authToken(GatewayContext context, ServerHttpRequest request) { 这个方法里可以自定义做验证。

     

    结束语:

    我实现了一遍两种网关,发现还是官网的文档最靠谱,也是能落地到项目中的。如果你需要源码的请到 我的Github 去clone,如果帮助到了你,还请点个 star。

     

    原文地址:https://www.cnblogs.com/KuJo/p/11306361.html
  • 相关阅读:
    图片采集
    资源(先备着)
    Memories of Vladimir Arnold 摘录
    Tribute to Vladimir Arnold 摘录
    Textbooks, Testing, Training: How We Discourage Thinking 笔记
    每天都有杰出数学家去世?(挖坑)
    I'm angry——人教B版选修2-3上的一个错误
    How to Study as a Mathematics Major 的笔记
    读《桑榆忆往》做的一点笔记
    《近代欧氏几何学》笔记
  • 原文地址:https://www.cnblogs.com/jpfss/p/11906689.html
Copyright © 2011-2022 走看看