一、Gateway 网关描叙及说明

1、微服务网关描叙

微服务网关是整个微服务API请求的入口,可以实现日志拦截、权限控制、解决跨域问题、
限流、熔断、负载均衡、黑名单与白名单拦截、授权等。

2、Gateway与Zuul的区别?

Zuul网关属于netfix公司开源的产品属于第一代微服务网关
Gateway属于SpringCloud自研发的第二代微服务网关
相比来说SpringCloudGateway性能比Zuul性能要好:
注意:Zuul基于Servlet实现的,阻塞式的Api, 不支持长连接。
SpringCloudGateway基于Spring5构建,能够实现响应式非阻塞式的Api,支持长连接,能够更好的整合Spring体系的产品。

3、过滤器与网关的区别

过滤器用于拦截单个服务
网关拦截整个的微服务

二、Gateway 网关环境搭建(Nacos+Gateway)

1、创建spring boot项目(2.0.1)

在这里插入图片描述

2、pom.xml 依赖

注意使用的是 webflux,不是web

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <artifactId>sprng-cloud-gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>sprng-cloud-gateway</name>
    <description>Demo project for Spring Boot</description>
    <dependencies>
    
        <!-- web组件 webflux -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        
        <!-- 监控中心,监控系统健康  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <!-- nacos 注册中心 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>0.2.2.RELEASE</version>
        </dependency>
        
        <!-- gateway 网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3、application.yml 配置

### 网关端口号
server:
  port: 80
spring:
  application:
    ### 服务名称,不能使用下划线
    name: alibaba-gateway
  cloud:
    ### 注册中心
    nacos:
      discovery:
        server-addr: 192.168.177.128:8848
    ### 网关
    gateway:
      ### 开启基于注册中心的路由表。gateway可以通过开启以下配置来打开根据服务的serviceId来匹配路由,
      ### 默认是大写,如果需要小写serviceId,则配置# spring.cloud.gateway.locator.lowerCaseServiceId:true
      discovery:
        locator:
          enabled: true
      ###路由策略
      routes:
        ### 配置方式一:绝对路径
          ### 路由id, 如果不写的话默认是uuid 唯一值
        - id: baidu
          ####转发http://www.mayikt.com/
          uri: http://www.baidu.com/
          ### 匹配规则
          predicates:
            - Path=/baidu/**
        ### 配置方式二:根据serviceId 动态获取url路径
        - id: member
          #### 基于lb负载均衡形式转发, 而是lb://开头,加上serviceId
          uri: lb://alibaba-server
          ### 这个是过滤器,对应的是filters 配置,有写好的过滤器,应该也可以自定义
          filters:
            - StripPrefix=1
          ### 匹配规则,可以配置多个,使用正则匹配,请求地址携带***(/***/)跳转到我们配置的uri,如:uri/***
          predicates:
            - Path=/alibaba-server/**

4、启动类

@SpringBootApplication
class SprngCloudGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(SprngCloudGatewayApplication.class, args);
    }
}

5、测试效果

配置方式一:访问 127.0.0.1/baidu 会直接跳转到百度首页
配置方式二:
alibaba-server 是我本地的服务,已注册到nacos,并提供了getUserId 接口
如下:使用网关80端口+ 服务id ,转发到具体的服务
在这里插入图片描述
nacos 服务列表
在这里插入图片描述

三、Gateway 网关过滤器

gateway 过滤器分为俩种。GatewayFilter 与 GlobalFilter。

GlobalFilter :全局过滤器
GatewayFilter :将应用到单个路由或者一个分组的路由上。
还有内置的过滤器断言机制

1、全局过滤器(token 验证)

有token 参数放行,无返回错误信息

/**
 * TODO  全局过滤器
 *
 * @return
 */
@Component
public class TestFilter implements GlobalFilter, Ordered {

    @Bean
    public GlobalFilter TestFilter() {
        return new TestFilter();
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("============执行全局过滤器==============");
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        if (token == null || token.isEmpty()) {
            // 验证不通过,返回错误信息
            String msg = "token not is null ";
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.BAD_REQUEST);
            DataBuffer buffer = response.bufferFactory().wrap(msg.getBytes());
            return response.writeWith(Mono.just(buffer));
        }
        // 验证通过,放行
        return chain.filter(exchange);
    }


    /**
     * TODO  过滤顺序指定,过滤Web处理程序会将的所有实例GlobalFilter和所有特定GatewayFilter于路由的实例添加到过滤器链中。
     */
    @Override
    public int getOrder() {
        return -1;
    }
}

2、token获取方法

 /**
     * TODO  获取前端传递的token,先请求头同获取token,如果请求头没有获取到,从queryParams获取
     *
     * @return
     * @author ws
     * @mail 1720696548@qq.com
     * @date 2020/2/12 0012 9:57
     */
    public static String getToken(ServerHttpRequest request) {
        String token = null;
        // 请求头同获取token
        List<String> tokenHeaders = request.getHeaders().get("TOKEN");
        if (tokenHeaders != null) {
            token = tokenHeaders.get(0);
        }
        // 如果请求头没有获取到,从QueryParams获取
        if (token == null) {
            token = request.getQueryParams().getFirst("TOKEN");
        }
        return token;
    }

网关集群,那么具体的服务怎么获取到是哪个网关转发过来的呢

3、网关向header 添加自定义参数


/**
 * TODO  全局过滤器
 *
 * @return
 */
@Component
public class TestFilter implements GlobalFilter{

    @Value("${server.port}")
    private String serverPort;

    @Bean
    public GlobalFilter TestFilter() {
        return new TestFilter();
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
      
        // 添加当前网关消息request 在转发到具体的服务,如:ip,端口
        ServerHttpRequest request = exchange.getRequest().mutate().header("serverPort", serverPort).build();
        exchange.mutate().request(request).build();
        // 验证通过,放行
        return chain.filter(exchange);
    }

}

3、自定义过滤器(暂不说明)

四、Gateway 网关集群

如需要添加黑名单,白名单功能,自行配置可获取用户真实ip,默认无法获取

1、使用Nginx/集群

可使用Nginx 或者 lvs虚拟vip 访问增加系统的高可用
在这里插入图片描述

2、nginx 相关配置nginx.conf

hosts 文件添加配置

127.0.0.1 a80.www.baidu.com

hosts 文件路径 : C:\windows\system32\drivers\etc

nginx.conf 配置参考

upstream mysvr { 
      server 127.0.0.1:8080;               # 这里定义可以加 http://  
      server 127.0.0.1:8081;
    }
server {
      listen       80;
      server_name  a80.www.baidu.com;
      location  ~*^.+$ {                   # 请求的url过滤,正则匹配,~为区分大小写,~*为不区,区分大小写。 默认 /
           proxy_pass  http://mysvr;       # 请求转向mysvr 定义的服务器列表 
    }

配置说明:拦截服务地址为 a80.www.baidu.com,端口为 80的 url ,
通过 mysvr 找到----> upstream mysvr 对应的配置
并轮询upstream mysvr 下配置的服务器
服务器的请求顺序为:ABABABABAB…

五、Gateway 动态配置路由

5.1、基于配置中心实现动态配置

直接把 yml 配置信息移动到配置中心即可

5.2、基于数据库 (数据层框架采用Jpa)实现动态配置

下面介绍数据库配置方式

1、添加数据表(mysql)

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for gateway_config
-- ----------------------------
DROP TABLE IF EXISTS `gateway_config`;
CREATE TABLE `gateway_config`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `route_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '路由Id',
  `route_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '名称',
  `route_pattern` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '规则',
  `route_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '类型(1:绝对路径,0:根据服务serverId匹配)',
  `route_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'url地址',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

参考配置

INSERT INTO `gateway_config` VALUES (1, 'baidu', '跳转百度首页', '/baidu/**', '1', 'https://www.baidu.com');
INSERT INTO `gateway_config` VALUES (2, 'alibaba-server', '生产者测试服务', '/alibaba-server/**', '0', 'lb://alibaba-server');

在这里插入图片描述

2、pom.xml 添加数据源等依赖

注意这里druid 的版本为1.1.10 版本会报错

    <!-- 阿里巴巴数据源  -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.17</version>
        </dependency>
        <!-- jpa -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
            <scope>runtime</scope>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
3、yml 新添加数据源配置
spring:

  ### 数据库连接信息
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://12.7.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useTimezone=true&serverTimezone=GMT%2B8
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource
    #最大活跃数
    maxActive: 20
    #初始化数量
    initialSize: 1
    #最大连接等待超时时间
    maxWait: 60000
    #打开PSCache,并且指定每个连接PSCache的大小
    poolPreparedStatements: true
    maxPoolPreparedStatementPerConnectionSize: 20
    #通过connectionProperties属性来打开mergeSql功能;慢SQL记录
    #connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    minIdle: 1
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: select 1 from dual
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    #配置监控统计拦截的filters,去掉后监控界面sql将无法统计,'wall'用于防火墙
    filters: stat, wall, log4j
  main:
    allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
  jpa:
    hibernate:
      ddl-auto: update  # 第一次建表create  后面用update
    show-sql: true
4、GateWayEntity
import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Data
@Entity
@Table(name = "gateway_config")
public class GateWayEntity {

    @Id
    private Long id;
    private String routeId;
    private String routeName;
    private String routePattern;
    private String routeType;
    private String routeUrl;
}


5、dao层
import com.example.alibabaclient.entity.GateWayEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Component;


@Component
public interface GatewayConfigDao extends JpaRepository<GateWayEntity,Long>, JpaSpecificationExecutor<GateWayEntity> {

}
6、service层
import com.example.alibabaclient.dao.GatewayConfigDao;
import com.example.alibabaclient.entity.GateWayEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class GatewayService implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher publisher;

    @Autowired
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    private RouteDefinitionWriter routeDefinitionWriter;
    @Autowired
    private GatewayConfigDao gatewayConfigDao;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

    public void initAllRoute() {
        // 从数据库查询配置的网关配置
        List<GateWayEntity> gateWayEntities = gatewayConfigDao.findAll();
        for (GateWayEntity gw : gateWayEntities) {
            loadRoute(gw);
        }
    }


    /**
     * TODO  配置更新
     */
    private String loadRoute(GateWayEntity gateWayEntity) {
        RouteDefinition definition = new RouteDefinition();
        Map<String, String> predicateParams = new HashMap<>(8);
        PredicateDefinition predicate = new PredicateDefinition();
        FilterDefinition filterDefinition = new FilterDefinition();
        Map<String, String> filterParams = new HashMap<>(8);
        // 如果配置路由type为0的话,则从注册中心获取服务
        URI uri = null;
        if (gateWayEntity.getRouteType().equals("0")) {
            uri = uri = UriComponentsBuilder.fromUriString("lb://" + gateWayEntity.getRouteUrl() + "/").build().toUri();
        } else {
            uri = UriComponentsBuilder.fromHttpUrl(gateWayEntity.getRouteUrl()).build().toUri();
        }
        // 定义的路由唯一的id
        definition.setId(gateWayEntity.getRouteId());
        predicate.setName("Path");
        //路由转发地址
        predicateParams.put("pattern", gateWayEntity.getRoutePattern());
        predicate.setArgs(predicateParams);

        // 名称是固定的, 路径去前缀
        filterDefinition.setName("StripPrefix");
        filterParams.put("_genkey_0", "1");
        filterDefinition.setArgs(filterParams);
        definition.setPredicates(Arrays.asList(predicate));
        definition.setFilters(Arrays.asList(filterDefinition));
        definition.setUri(uri);
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "success";
    }
}
6、controller 层
import com.example.alibabaclient.service.GatewayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * TODO  路由配置更新
 */
@RestController
public class RouteController {

    @Autowired
    private  GatewayService gatewayService;

    @GetMapping("/route")
    public String route(){
        gatewayService.initAllRoute();
        return "success";
    }
}

7、测试配置

1、添加数据库数据(如第一步添加了忽视)

INSERT INTO `gateway_config` VALUES (1, 'baidu', '跳转百度首页', '/baidu/**', '1', 'https://www.baidu.com');
INSERT INTO `gateway_config` VALUES (2, 'alibaba-server', '生产者测试服务', '/alibaba-server/**', '0', 'lb://alibaba-server');

加载配置,访问接口:http://127.0.0.1/route ,访问: http://127.0.0.1/baidu 直接跳到百度首页,表示配置成功

8、测试新添加/修改

1、数据库添加一条新数据

INSERT INTO `gateway_config` VALUES (3, 'gitee', '跳转码云', '/gitee/**', '1', 'https://gitee.com/');

加载配置,访问接口:http://127.0.0.1/route ,访问: http://127.0.0.1/gitee 直接跳到码云首页,表示动态配置成功

到此就结束了,该配置直接修改、添加、删除配置信息后,调用接口 /route 后都会立即生效

六、获取body+ params 数据进行参数过滤

勿用此方法获取body数据,会出现获取参数不完整的情况

//    public static String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
//        //获取请求体
//        Flux<DataBuffer> body = serverHttpRequest.getBody();
//        AtomicReference<String> bodyRef = new AtomicReference<>();
//        body.subscribe(buffer -> {
//            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
//            DataBufferUtils.release(buffer);
//            bodyRef.set(charBuffer.toString());
//            System.out.println(charBuffer.toString());
//        });
//        //获取request body
//        return bodyRef.get();
//    }

1、定义过滤器 RequestFilter

package com.gateway.filter;


import com.alibaba.fastjson.JSONObject;
import com.gateway.common.properties.AuthProperties;
import com.gateway.common.utils.HtmlEncodeUtil;
import com.gateway.common.utils.SqlEncodeUtil;
import com.gateway.error.ErrorConstantEnum;
import com.gateway.error.ErrorException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.CachedBodyOutputMessage;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;


/**
 * TODO  Request 请求参数过滤
 * 参考文章一 body数据处理: https://blog.csdn.net/tianyaleixiaowu/article/details/83375246
 * 参考文章二 请求参数获取:https://blog.csdn.net/fuck487/article/details/85166162
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/2/14 0014 0:21
 */
@Component
@Slf4j
@SuppressWarnings("all")
public class RequestFilter implements GlobalFilter, Ordered {
    /**
     * 过滤执行顺序
     */
    private int order;

    public RequestFilter(int order) {
        this.order = order;
    }

    @Override
    public int getOrder() {
        return order;
    }


    @Autowired
    private AuthProperties authProperties;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // req
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        // 重新构造request,参考ModifyRequestBodyGatewayFilterFactory
        ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
        // 请求方式 --   //if (method == HttpMethod.POST || method == HttpMethod.PUT || method == HttpMethod.DELETE || method == HttpMethod.GET) {
        String method = serverHttpRequest.getMethodValue();
        // 请求参数类型
        MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
        // 不对文件上传做处理
        if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType)) {
            return chain.filter(exchange);
        }
        //  json 格式参数传参
        if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON_UTF8.isCompatibleWith(mediaType)) {
            Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
                //验签,xss攻击等
                String newBody = handleBody(body);
                //临时保存数据
                exchange.getResponse().getHeaders().add("bodyStr", newBody);
                //返回数据
                return Mono.just(newBody);
                // return Mono.empty();
            });
            BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
            HttpHeaders headers = new HttpHeaders();
            headers.putAll(exchange.getRequest().getHeaders());
            //猜测这个就是之前报400错误的元凶,之前修改了body但是没有重新写content length
            headers.remove("Content-Length");
            //CachedBodyOutputMessage
            CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
            // return开始执行 serverRequest.bodyToMono方法
            return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
                ServerHttpRequest decorator = this.decorate(exchange, headers, outputMessage);
                return returnMono(chain, exchange.mutate().request(decorator).build());
            }));
        } else {
            //  非 json 格式参数传参,得到 queryParams 参数后,做你想做的事,可以处理sql 注入, xss 攻击处理等,java中URL 的编码和解码函数java.net.URLEncoder.encode(String s)和java.net.URLDecoder.decode(String s);
            MultiValueMap<String, String> queryParams = serverHttpRequest.getQueryParams();
            URI uri = serverHttpRequest.getURI();
            //  重写uri参数,自定义handleParam 方法进行参数替换/过滤
            URI newUri = UriComponentsBuilder.fromUri(uri)
                    .replaceQuery(this.handleParam(queryParams))
                    .build(true)
                    .toUri();
            //下面的将请求体再次封装写回到request里,传到下一级,否则,由于请求体已被消费,后续的服务将取不到值
            ServerHttpRequest request = serverHttpRequest.mutate().uri(newUri).build();
            // 临时保存请求参数
            exchange.getResponse().getHeaders().add("queryParams", queryParams.toString());
            //封装request,传给下一级
            return chain.filter(exchange.mutate().request(request).build());
        }
        // return chain.filter(exchange);
    }


    private Mono<Void> returnMono(GatewayFilterChain chain, ServerWebExchange exchange) {
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            Long startTime = exchange.getAttribute("startTime");
            if (startTime != null) {
                long executeTime = (System.currentTimeMillis() - startTime);
                log.info("耗时:{}ms", executeTime);
                log.info("状态码:{}", Objects.requireNonNull(exchange.getResponse().getStatusCode()).value());
            }
        }));
    }

    ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
        return new ServerHttpRequestDecorator(exchange.getRequest()) {
            public HttpHeaders getHeaders() {
                long contentLength = headers.getContentLength();
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                if (contentLength > 0L) {
                    httpHeaders.setContentLength(contentLength);
                } else {
                    httpHeaders.set("Transfer-Encoding", "chunked");
                }
                return httpHeaders;
            }

            public Flux<DataBuffer> getBody() {
                return outputMessage.getBody();
            }
        };
    }


    /**
     * TODO 处理params 参数
     *
     * @return 为url 字符串拼接内容,如: ?aldtype=16047&query=&keyfrom=baidu ,value值需 URLEncoder.encode
     * @author ws
     * @mail 1720696548@qq.com
     * @date 2020/2/13 0013 23:52
     */
    private String handleParam(MultiValueMap<String, String> queryParams) {
        // 处理get请求参数
        if (queryParams.size() <= 0) {
            return "";
        }
        // sql 注入攻击处理
        if (authProperties.isSqlReqAttack()) {
            boolean passSqlInjection = SqlEncodeUtil.isPassSqlInjection(queryParams.toString());
            if (!passSqlInjection) {
                throw new ErrorException(ErrorConstantEnum.IS_NO_PARAM.getCode(), queryParams.toString() + " " + ErrorConstantEnum.IS_NO_PARAM.getMsg());
            }
        }
        // xss 攻击处理,去掉[ 和 ]
        String jsonQueryParams = JSONObject.toJSONString(queryParams);
        String newJsonQueryParams = jsonQueryParams.replaceAll("\\[", "").replaceAll("\\]", "");
        Map<String, String> mapParams = new HashMap<>();
        if (authProperties.isXssReqAttack()) {
            mapParams = JSONObject.parseObject(HtmlEncodeUtil.htmlEncode(newJsonQueryParams), Map.class);
        } else {
            mapParams = JSONObject.parseObject(newJsonQueryParams, Map.class);
        }
        StringBuilder query = new StringBuilder();
        for (String key : mapParams.keySet()) {
            // 对原有的每个参数进行操作
            query.append(key + "=" + java.net.URLEncoder.encode(mapParams.get(key)) + "&");
        }
        return query.toString().substring(0, query.length() - 1);
    }


    /**
     * TODO 处理 body参数
     *
     * @param bodyStr 请求boay数据
     * @return boay
     * @author ws
     * @mail 1720696548@qq.com
     * @date 2020/2/13 0013 23:52
     */
    private String handleBody(String bodyStr) {
        if (bodyStr.isEmpty()) {
            return "";
        }
        // sql 注入攻击处理
        if (authProperties.isSqlReqAttack()) {
            boolean passSqlInjection = SqlEncodeUtil.isPassSqlInjection(bodyStr);
            if (!passSqlInjection) {
                throw new ErrorException(ErrorConstantEnum.IS_NO_PARAM);
            }
        }
        // xss攻击处理
        if (authProperties.isXssReqAttack()) {
            return HtmlEncodeUtil.htmlEncode(bodyStr);
        } else {
            return bodyStr;
        }
    }

}

2、htmlEncode

package com.gateway.common.utils;

/**
  * TODO  html 特殊字符转换工具
  * @author ws
  * @mail  1720696548@qq.com
  * @date  2020/2/13 0013 11:01
  */
@SuppressWarnings("ALL")
public class HtmlEncodeUtil {

    /**
     * TODO  Html 文本特殊符号转换,防止 xss攻击字符转换配置工具类
     *
     * @param source
     * @return java.lang.String
     * @author ws
     * @mail 1720696548@qq.com
     * @date 2020/2/9 0009 16:31
     */
    public static String htmlEncode(String source) {
        if (source == null) {
            return "";
        }
        String html = "";
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < source.length(); i++) {
            char c = source.charAt(i);
            switch (c) {
                case '<':
                    buffer.append("&lt;");
                    break;
                case '>':
                    buffer.append("&gt;");
                    break;
                case '&':
                    buffer.append("&amp;");
                    break;
//                case '"':
//                    buffer.append("&quot;");
//                    break;
                default:
                    buffer.append(c);
            }
        }
        html = buffer.toString();
        return html;
    }
}

3、SqlEncodeUtil

package com.gateway.common.utils;

import org.apache.commons.lang.StringUtils;

/**
  * TODO  防止 sql注入攻击字段检查配置工具类
  * @author ws
  * @mail  1720696548@qq.com
  * @date  2020/2/14 0014 17:35 
  */
@SuppressWarnings("all")
public class SqlEncodeUtil {

    private static String[] badStr = {
            "and", "exec", "execute", "insert", "select", "delete", "update",
            "count", "chr", "mid", "master", "truncate", "char", "declare", "sitename",
            "net user", "xp_cmdshell", "or", "create", "drop", "table", "from",
            "grant", "use", "group_concat", "column_name", "information_schema.columns",
            "table_schema", "union", "where", "order", "by", "like", "%"
    };

    static String[] illegalCharacterSetStr = {
            "*"
    };

    /**
     * TODO  检查传入参数
     *
     * @param value
     * @return boolean true 正常,false 存在风险字段
     * @author ws
     * @mail 1720696548@qq.com
     * @date 2020/2/14 0014 15:28
     */
    public  static boolean isPassSqlInjection(String value) {

        if (StringUtils.isBlank(value)) {
            return true;
        }
        value = value.toLowerCase();
        for (String bad : badStr) {
            if (value.indexOf(bad + " ") >= 0 || value.indexOf(" " + bad) >= 0 || value.indexOf("" + bad + " ") >= 0) {
                return false;
            }
        }
        for (String bad : illegalCharacterSetStr) {
            if (value.indexOf(bad) >= 0) {
                return false;
            }
        }
        return true;
    }
}

4、AuthProperties 参数配置

1、yml 配置

#auth:
#  adminRouteIds: baidu
#  userRouteIds: baidu
#  interfaceNoCheck:
#    -baidu-555: /baidu/555 该值随意填写,尽量填写为具体接口名称
#    -baidu-666: /baidu/666
#    -test: 测试xss
#    -test-api-test2: /test/api/test221
#  sqlReqAttack: true
#  xssReqAttack: true
#  xssRespAttack: true

** 2、AuthProperties** 类,动态获取yml 配置

package com.gateway.common.properties;
/**
 * TODO  yml配置文件中Auth节点下的所有数据
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/2/12 0012 15:54
 */

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;


/**
 * TODO  映射Org属性,yml 配置数据:参考  https://blog.csdn.net/weixin_34220834/article/details/91427948
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/2/13 0013 11:02
 * * @ConfigurationProperties("auth")  读取 auth 开头的配置数据
 * * @Configuration + @EnableConfigurationProperties({ServerProperties.class}) 注册到spring容器
 * * @RefreshScope 配置中心可直接修改更新立即生效
 */
@Data
@RefreshScope
@Configuration
@ConfigurationProperties("auth")
@EnableConfigurationProperties({AuthProperties.class})
public class AuthProperties {
    /**
     * 是否对请求参数进行 sql注入过滤
     */
    private boolean sqlReqAttack = false;
    /**
     * 是否对请求参数进行 xss过滤
     */
    private boolean xssReqAttack = false;
    /**
     * 是否对响应参数进行过滤
     */
    private boolean xssRespAttack = false;
    /**
     * 后端需要token验证的服务,网关路由Id,多个逗号分隔
     */
    private String adminRouteIds;
    /**
     * 前端需要token验证的服务,网关路由Id,多个逗号分隔
     */
    private String userRouteIds;
    /**
     * 不需要token 验证的接口集
     */
    private Map<String, String> interfaceNoCheck = new HashMap<>();
}

七、获取Resp 返回参数进行过滤

1、定义 ResponseFilter 过滤器

package com.gateway.filter;

import com.gateway.common.properties.AuthProperties;
import com.gateway.common.utils.HtmlEncodeUtil;
import com.gateway.config.GatewayLogConfig;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.Charset;

/**
 * TODO  xss 攻击拦截处理器,Response 返回数据过滤order必须小于-1
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/2/13 0013 10:48
 */
@Component
@NoArgsConstructor
@Slf4j
@SuppressWarnings("all")
public class ResponseFilter implements GlobalFilter, Ordered {

    /**
     * 过滤执行顺序
     */
    private int order;

    public ResponseFilter(int order) {
        this.order = order;
    }

    @Override
    public int getOrder() {
        return order;
    }

    @Autowired
    private AuthProperties authProperties;
    @Autowired
    private  GatewayLogConfig gatewayLogConfig;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        ServerHttpResponse originalResponse = exchange.getResponse();
        DataBufferFactory bufferFactory = originalResponse.bufferFactory();
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    Mono<Void> voidMono = super.writeWith(fluxBody.map(dataBuffer -> {
                        byte[] content = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(content);
                        //释放掉内存
                        DataBufferUtils.release(dataBuffer);
                        //原数据,想修改、查看就随意而为了
                        String data = new String(content, Charset.forName("UTF-8"));
                        // 不对html 页面进行过滤
                        // if (data.indexOf("<html>") == -1) {   // }
                        // 判断是否为swagger文档,v2/api-docs  ,是不进行xss过滤
                        if (data.toString().indexOf("v2/api-docs") == -1 && authProperties.isXssRespAttack()) {
                            data = HtmlEncodeUtil.htmlEncode(data);
                        }
                        //byte[] uppedContent = new String(data, Charset.forName("UTF-8")).getBytes();
                        return bufferFactory.wrap(data.getBytes());
                    }));
                    // 注意,body数据在exchange 只能读取一次
                    gatewayLogConfig.putLog(exchange, "响应成功");
                    return voidMono;
                }
                // if body is not a flux. never got there.
                return super.writeWith(body);
            }
        };
        // replace response with decorator
        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }
}

八、定义全局异常和自定义异常

1、自定义返回–Result

@Data
public class Result implements Serializable {
    private static final long serialVersionUID = 0L;
    /**
     *    响应结果码
     */
    private Integer code;
    /**
     *  响应结果信息
     */
    private String msg;

    public Result(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Result(ErrorConstantEnum errorConstantEnum) {
        this.code = errorConstantEnum.getCode();
        this.msg = errorConstantEnum.getMsg();
    }
}

2、 异常状态枚举类 ErrorConstantEnum

package com.gateway.error;

import lombok.Getter;
import lombok.NoArgsConstructor;

/**
 * TODO  异常常量类
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/2/9 0009 11:16
 * @return
 */

@Getter
@NoArgsConstructor
public enum ErrorConstantEnum {
 
    IS_NO_TOKEN(10001, "没有 token"),
    IS_NO_TOKEN_INVALID(10002, "无效 token"),
    IS_NO_PARAM(10003, "存在非法参数"),
    IS_NO_SENTINEL_MAX(10004, "QBS已到达阀值,请稍后重试"),

    private Integer code;
    private String msg;

    ErrorConstantEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

3、自定义异常类 ErrorException

package com.gateway.error;


import lombok.Data;
import org.springframework.stereotype.Component;

/**
 * TODO  自定义异常类,通过此类可返回各种自定义异常信息,由GlobalExceptionHandler 处理返回
 * <p>
 * 使用:throw new ErrorException("1000000","自定义异常测试");
 * 返回:{"code": 1000000,"msg": "自定义异常测试"}
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/2/9 0009 12:44
 * @return
 */
@Data
@Component
public class ErrorException extends RuntimeException {

    private Integer code;
    private String msg;

    //直接传递
    public ErrorException(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    //枚举传递(建议先定义枚举)
    public ErrorException(ErrorConstantEnum errorConstantEnum) {
        this.code = errorConstantEnum.getCode();
        this.msg = errorConstantEnum.getMsg();
    }
}

4、beng配置类 ExceptionConfig


import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import java.util.Collections;
import java.util.List;

/**
 * TODO    网关全局异常捕获配置
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/2/14 0014 11:01
 * @return
 */
@Configuration
public class ExceptionConfig {

    /**
     * 自定义异常处理[@@]注册Bean时依赖的Bean,会从容器中直接获取,所以直接注入即可
     */
    @Primary
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                                             ServerCodecConfigurer serverCodecConfigurer) {
        JsonExceptionHandler jsonExceptionHandler = new JsonExceptionHandler();
        jsonExceptionHandler.setViewResolvers(viewResolversProvider.getIfAvailable(Collections::emptyList));
        jsonExceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
        jsonExceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
        return jsonExceptionHandler;
    }

}

5、全局异常处理类 JsonExceptionHandler

package com.gateway.error;

import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.fastjson.JSONObject;
import com.gateway.common.vo.Result;
import com.gateway.config.GatewayLogConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * TODO  gateway 全局异常处理类
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/2/14 0014 12:04
 */
@Slf4j
public class JsonExceptionHandler implements ErrorWebExceptionHandler {
    @Autowired
    private GatewayLogConfig gatewayLogConfig;

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        // 按照异常类型进行处理
        HttpStatus httpStatus;
        String msg;
        Result data;
        if (ex instanceof ErrorException) {
            // 自定义异常错误
            data = new Result(((ErrorException) ex).getCode(), ((ErrorException) ex).getMsg());
        } else if (ex instanceof FlowException) {
            // sentinel 限流
            data = new Result(ErrorConstantEnum.IS_NO_SENTINEL_MAX);
        } else if (ex instanceof NotFoundException) {
            // 404错误
            data = new Result(ErrorConstantEnum.NOT_FOUND);
        } else if (ex instanceof ResponseStatusException) {
            ResponseStatusException responseStatusException = (ResponseStatusException) ex;
            httpStatus = responseStatusException.getStatus();
            msg = responseStatusException.getMessage();
            data = new Result(httpStatus.value(), msg);
        } else {
            // 500错误
            data = new Result(500, ex.toString());
            ex.printStackTrace();
        }
        String dataJson = JSONObject.toJSONString(data);
        //封装响应体,此body可修改为自己的jsonBody
        Map<String, Object> result = new HashMap<>(2, 1);
        result.put("httpStatus", HttpStatus.OK);
        result.put("body", dataJson);
        // 记录日志
        gatewayLogConfig.putLog(exchange, "异常:" + dataJson);
        //参考AbstractErrorWebExceptionHandler
        if (exchange.getResponse().isCommitted()) {
            return Mono.error(ex);
        }
        exceptionHandlerResult.set(result);
        ServerRequest newRequest = ServerRequest.create(exchange, this.messageReaders);
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse).route(newRequest)
                .switchIfEmpty(Mono.error(ex))
                .flatMap((handler) -> handler.handle(newRequest))
                .flatMap((response) -> write(exchange, response));

    }


    /**
     * MessageReader
     */
    private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();

    /**
     * MessageWriter
     */
    private List<HttpMessageWriter<?>> messageWriters = Collections.emptyList();

    /**
     * ViewResolvers
     */
    private List<ViewResolver> viewResolvers = Collections.emptyList();

    /**
     * 存储处理异常后的信息
     */
    private ThreadLocal<Map<String, Object>> exceptionHandlerResult = new ThreadLocal<>();

    /**
     * 参考AbstractErrorWebExceptionHandler
     */
    public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) {
        Assert.notNull(messageReaders, "'messageReaders' must not be null");
        this.messageReaders = messageReaders;
    }

    /**
     * 参考AbstractErrorWebExceptionHandler
     */
    public void setViewResolvers(List<ViewResolver> viewResolvers) {
        this.viewResolvers = viewResolvers;
    }

    /**
     * 参考AbstractErrorWebExceptionHandler
     */
    public void setMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
        Assert.notNull(messageWriters, "'messageWriters' must not be null");
        this.messageWriters = messageWriters;
    }


    /**
     * 参考DefaultErrorWebExceptionHandler
     */
    protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
        Map<String, Object> result = exceptionHandlerResult.get();
        return ServerResponse.status((HttpStatus) result.get("httpStatus"))
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(BodyInserters.fromObject(result.get("body")));
    }

    /**
     * 参考AbstractErrorWebExceptionHandler
     */
    private Mono<? extends Void> write(ServerWebExchange exchange,
                                       ServerResponse response) {
        exchange.getResponse().getHeaders()
                .setContentType(response.headers().getContentType());
        return response.writeTo(exchange, new ResponseContext());
    }

    /**
     * 参考AbstractErrorWebExceptionHandler
     */
    private class ResponseContext implements ServerResponse.Context {

        @Override
        public List<HttpMessageWriter<?>> messageWriters() {
            return JsonExceptionHandler.this.messageWriters;
        }

        @Override
        public List<ViewResolver> viewResolvers() {
            return JsonExceptionHandler.this.viewResolvers;
        }
    }
}

九 、整合swagger2 文档

1、pom.xml 依赖

    <!-- gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.0.4.RELEASE</version>
        </dependency>
        <!--  网关整合 swagger2 查看所有服务api文档 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

2、SwaggerHandler

package com.gateway.swagger;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.*;

import java.util.Optional;
/**
  * TODO  因为Gateway里没有配置SwaggerConfig,而运行Swagger-ui又需要依赖一些接口,所以我的想法是自己建立相应的swagger-resource端点。
  * @author ws
  * @mail  1720696548@qq.com
  * @date  2020/3/2 0002 12:06
  */
@RestController
@RequestMapping("/swagger-resources")
public class SwaggerHandler {
    @Autowired(required = false)
    private SecurityConfiguration securityConfiguration;
    @Autowired(required = false)
    private UiConfiguration uiConfiguration;
    private final SwaggerResourcesProvider swaggerResources;

    @Autowired
    public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
        this.swaggerResources = swaggerResources;
    }


    @GetMapping("/configuration/security")
    public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
        return Mono.just(new ResponseEntity<>(
                Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
    }

    @GetMapping("/configuration/ui")
    public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
        return Mono.just(new ResponseEntity<>(
                Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
    }

    @GetMapping("")
    public Mono<ResponseEntity> swaggerResources() {
        return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
    }
}

3、SwaggerHeaderFilter

package com.gateway.swagger;

import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

/**
 * TODO  路由配置添加SwaggerHeaderFilter 过滤器【Spring Boot版本超过2.0.6的应该可以跳过这一步,最新源码也更新了。Spring修复了bug给我们添加上了这个Header】
 * 另外,我发现在路由为admin/test/{a}/{b},在swagger会显示为test/{a}/{b},缺少了admin这个路由节点。
 * 断点源码时发现在Swagger中会根据X-Forwarded-Prefix这个Header来获取BasePath,将它添加至接口路径与host中间,这样才能正常做接口测试,
 * 而Gateway在做转发的时候并没有这个Header添加进Request,所以发生接口调试的404错误。
 * 解决思路是在Gateway里加一个过滤器来添加这个header。
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/3/2 0002 11:56
 */
@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
    private static final String HEADER_NAME = "X-Forwarded-Prefix";

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getURI().getPath();
            if (!StringUtils.endsWithIgnoreCase(path, SwaggerProvider.API_URI)) {
                return chain.filter(exchange);
            }
            String basePath = path.substring(0, path.lastIndexOf(SwaggerProvider.API_URI));
            ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
            ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
            return chain.filter(newExchange);
        };
    }
}

4、SwaggerProvider

package com.gateway.swagger;

import lombok.AllArgsConstructor;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;

import java.util.ArrayList;
import java.util.List;

/**
 * TODO  因为Swagger暂不支持webflux项目,所以Gateway里不能配置SwaggerConfig,
 * 也就是说Gateway无法提供自身API。但我想一般也不会在网关项目代码里写业务API代码吧。。
 * 所以这里的集成只是基于基于WebMvc的微服务项目。
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/3/2 0002 11:54
 */
@Component
@Primary
@AllArgsConstructor
public class SwaggerProvider implements SwaggerResourcesProvider {
    public static final String API_URI = "/v2/api-docs";
    private final RouteLocator routeLocator;
    private final GatewayProperties gatewayProperties;

    @Override
    public List<SwaggerResource> get() {
        List<SwaggerResource> resources = new ArrayList<>();
        List<String> routes = new ArrayList<>();
        //取出gateway的route
        routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
        //结合配置的route-路径(Path),和route过滤,只获取有效的route节点
        gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId()))
                .forEach(routeDefinition -> routeDefinition.getPredicates().stream()
                        .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
                        .forEach(predicateDefinition -> resources.add(swaggerResource(routeDefinition.getId(),
                                predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
                                        .replace("/**", API_URI)))));
        return resources;
    }

    private SwaggerResource swaggerResource(String name, String location) {
        SwaggerResource swaggerResource = new SwaggerResource();
        swaggerResource.setName(name);
        swaggerResource.setLocation(location);
        swaggerResource.setSwaggerVersion("2.0");
        return swaggerResource;
    }
}

5、yml 添加 SwaggerHeaderFilter 过滤器

需要swagger 的路由下添加 - SwaggerHeaderFilter 过滤器

#        - id: lplb-coupon-api
#          uri: lb://lplb-coupon-api
#          predicates:
#            - Path=/lplb-coupon-api/**
#          filters:
#            - SwaggerHeaderFilter
#            - StripPrefix=1

十、整合sentinel 限流

1、pom.xml

      <!-- sentinel+gateway 限流 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
            <version>1.6.0</version>
        </dependency>

2、RoutesProperties 获取路由YML 配置

/**
 * TODO  获取路由相关配置
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/2/19 0019 10:28
 */
@Data
@RefreshScope
@Configuration
@ConfigurationProperties("spring.cloud.gateway")
@EnableConfigurationProperties({RoutesProperties.class})
public class RoutesProperties {

    private List<Map<String,Object>> routes;
}

3、添加Bean 配置 GatewayConfiguration

package com.gateway.sentinel;

import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import java.util.Collections;
import java.util.List;
/**
  * TODO  sentinel 限流核心配置
  * @author ws
  * @mail  1720696548@qq.com
  * @date  2020/2/17 0017 14:08
  */
@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }
}

4、添加 SentinelApplicationRunner 动态配置限流

initGatewayRules() 方法配置限流属性
handle 方法是nacos 监听路由相关配置yml 修改,在调用initGatewayRules() 实现刷新’

package com.gateway.sentinel;

import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.gateway.common.properties.RoutesProperties;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.endpoint.event.RefreshEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import java.util.*;

/**
 * TODO  sentinel限流规则配置
 *
 * @author ws
 * @mail 1720696548@qq.com
 * @date 2020/2/17 0017 14:09
 */
@SuppressWarnings("all")
@Slf4j
@Component
public class SentinelApplicationRunner {

    @Autowired
    private RoutesProperties routesProperties;

    /**
     * TODO  监听nacos配置中心数据修改重置sentinel 配置
     *
     * @param event
     * @return void
     * @author ws
     * @mail 1720696548@qq.com
     * @date 2020/2/22 0022 17:26
     */
    @EventListener
    public void handle(RefreshEvent event) {
        if (event.getEventDesc().equals("Refresh Nacos entity")) {
            //等待10秒,让配置中心数据已经修改完本地数据在修改限流配置
            new Thread(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    Thread.sleep(10*1000);
                    initGatewayRules();
                }
            }).start();
        }
    }

    /**
     * TODO    初始化配置sentinel限流规则
     *
     * @return void
     * @author ws
     * @mail 1720696548@qq.com
     * @date 2020/2/22 0022 16:10
     */
    private void initGatewayRules() {
        //限流规则容器
        Set<GatewayFlowRule> rules = new HashSet<>();
        // 获取到所有路由配置
        List<Map<String, Object>> routes = routesProperties.getRoutes();
        System.out.println(routes.toString());
        // 获取到路由配置
        for (Map<String, Object> route : routes) {
            //获取限流配置
            if (route.get("sentinel") == null) {
                continue;
            }
            // 获取路由Id
            String ruleId = route.get("id").toString();
            try {
                Map<String, String> sentinelMap = (HashMap<String, String>) route.get("sentinel");
                // 获取限流阈值
                Integer count = Integer.parseInt(sentinelMap.get("count"));
                //获取限流时间单位(秒),如未配置设置为1
                Integer intervalSec = Integer.parseInt(sentinelMap.get("intervalSec"));
                // 1、路由Id,  2  、限流阈值,   3、统计时间窗口,单位是秒,默认是 1 秒
                GatewayFlowRule gatewayFlowRule = new GatewayFlowRule(ruleId).setCount(count).setIntervalSec(intervalSec);
                rules.add(gatewayFlowRule);
                log.info("路由Id [" + ruleId + "] 限流配置:" + gatewayFlowRule.toString());
            } catch (Exception e) {
                log.info("请检查路由Id [" + ruleId + "] 下sentinel 下的 [count] [intervalSec] 是否配置正确");
            }
        }
        GatewayRuleManager.loadRules(rules);
    }
}

5、yml 配置

sentinel:限流配置 -> intervalSec:限流时间单位(秒), count:限流数量(秒),修改后等待10秒后生效

#        - id: lplb-coupon-api
#          uri: lb://lplb-coupon-api
#          predicates:
#            - Path=/lplb-coupon-api/**
#          filters:
#            - SwaggerHeaderFilter
#            - StripPrefix=1
#          sentinel:
#            intervalSec: 1
#            count: 10
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐