SpringCloudAlibaba 五、 Gateway 网关实现/ 服务转发/ 动态配置/ 参数过滤 / 自定义异常 / 整合swagger2 /整合sentinel(注册中心为 Nacos)
一、Gateway 网关描叙及说明1、微服务网关描叙微服务网关是整个微服务API请求的入口,可以实现日志拦截、权限控制、解决跨域问题、限流、熔断、负载均衡、黑名单与白名单拦截、授权等。2、Gateway与Zuul的区别?Zuul网关属于netfix公司开源的产品属于第一代微服务网关Gateway属于SpringCloud自研发的第二代微服务网关相比来说SpringCloudGatew...
一、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("<");
break;
case '>':
buffer.append(">");
break;
case '&':
buffer.append("&");
break;
// case '"':
// buffer.append(""");
// 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
更多推荐
所有评论(0)