微服务网关:Spring Cloud Gateway
写之前 我先把官方的文档地址贴出来:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.1.0.RELEASE/single/spring-cloud-gateway.html
1.概述
Spring cloud gateway是spring官方基于Spring 5.0、Spring Boot2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供简单、有效和统一的API路由管理方式,Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filer链的方式提供了网关基本的功能,例如:安全、监控/埋点、限流等。
网关提供API全托管服务,丰富的API管理功能,辅助企业管理大规模的API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等功能。一般来说网关对外暴露的URL或者接口信息,我们统称为路由信息。
Spring Cloud Gateway中的几个重要概念:
路由。路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter组成。如果断言路由为真,则说明请求的URL和配置匹配
断言。Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于http request中的任何信息,比如请求头和参数等。
过滤器。一个标准的Spring webFilter。Spring cloud gateway中的filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理
server:
port: 9527
spring:
application:
name: ruoyi-gateway
devtools:
restart:
enabled: true
profiles:
active: dev
cloud:
config:
fail-fast: true #快速配置客户端失败
name: ${spring.application.name}
profile: ${spring.profiles.active}
discovery:
enabled: true
service-id: ruoyi-config
gateway:
discovery:
locator:
enabled: true
routes:
# 认证中心
- id: ruoyi-auth
uri: lb://ruoyi-auth
predicates:
- Path=/auth/**
filters:
# 验证码处理
- CacheRequest
- ImgCodeFilter
- StripPrefix=1
# 代码生成
- id: ruoyi-gen
uri: lb://ruoyi-gen
predicates:
- Path=/gen/**
filters:
- StripPrefix=1
# dfs
- id: ruoyi-dfs
uri: lb://ruoyi-dfs
predicates:
- Path=/dfs/**
filters:
- StripPrefix=1
#system 模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
# 限流配置
- StripPrefix=1
- name: RequestRateLimiter
args:
key-resolver: '#{@remoteAddrKeyResolver}'
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
# 降级配置
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: 'forward:/fallback'
eureka:
client: #客户端注册进eureka服务列表内
service-url:
defaultZone: http://localhost:7001/eureka
#defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true #访问路径可以显示IP地址
hystrix:
command:
default: #default全局有效,service id指定应用有效
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 5000 #断路器超时时间,默认1000ms
上述配置信息 是一个比较全面的Spring Cloud Gateway 配置信息。注册中心基于eureka
第一种路由转发配置:
参数spring.cloud.gateway.discovery.locator.enabled为true,表明Gateway开启服务注册和发现的功能,并且Spring Cloud Gateway自动根据服务发现为每一个服务创建了一个router,这个router将以服务名开头的请求路径转发到对应的服务。spring.cloud.gateway.discovery.locator.lowerCaseServiceId:true是将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了)
已认证模块为例:http://localhost:9527/ruoyi-auth/user/info 这样的地址访问,网关将把请求转发到ruoyi-auth 这个服务上,ruoyi-auth是认证模块在eureka上的服务名称(小写)
第二种路由转发配置:
spring.cloud.gateway.
routes:
# 认证中心
- id: ruoyi-auth
uri: lb://ruoyi-auth
predicates:
- Path=/auth/**
filters:
# 验证码处理
- CacheRequest
- ImgCodeFilter
- StripPrefix=1
在上面的配置中,配置了一个Path的predicate(断言),将以/auth/**开头的请求都会转发到uri为lb://ruoyi-auth 的地址上,lb://ruoyi-auth(注册中心服务的名称)即ruoyi-auth服务的负载均衡地址,并用StripPrefix的filter在转发之前将/auth去掉。
已认证模块为例:http://localhost:9527/auth/user/info 这样的地址访问,网关将把请求转发到ruoyi-auth 这个服务上,ruoyi-auth是认证模块在eureka上的服务名称(小写)
跨域:
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "http://docs.spring.io"
allowedMethods:
- GET
上边这段配置是粘贴的官方的,上面的配置的意思为 允许来自http://docs.spring.io 的get请求访问。 还可以加上 allowHeaders: - Content- Type 这么一段配置,意思是服务器允许请求头中携带Content-Type 字段。
我自己测了一下 发现这个跨域好像不太好使,所以我用了下面的代码:
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// cookie跨域
config.setAllowCredentials(Boolean.TRUE);
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
// 配置前端js允许访问的自定义响应头
config.addExposedHeader("setToken");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
Spring Cloud Gateway的filter生命周期不像Zuul那么丰富,它只有两个:“pre”和“post”:
pre:这种过滤器在请求被路由之前调用。可以利用这个过滤器实现身份验证、在集群中选择请求的微服务、记录调试的信息。
post:这种过滤器在路由到服务器之后执行。这种过滤器可用来为响应添加HTTP Header、统计信息和指标、响应从微服务发送给客户端等。
Spring Cloud gateway的filter分为两种:GatewayFilter和Globalfilter。GlobalFilter会应用到所有的路由上,而Gatewayfilter将应用到单个路由或者一个分组的路由上。
利用Gatewayfilter可以修改请求的http的请求或者是响应,或者根据请求或者响应做一些特殊的限制。更多时候可以利用Gatewayfilter做一些具体的路由配置。
下面的配置是AddRequestParameter Gatewayfilter的相关配置。
spring.cloud.gateway.
routes:
- id : parameter_route
uri: http: //localhost:8504/user/info
filters:
- AddRequestParameter=foo, bar
predicates:
- Method=GET
上述配置中指定了转发的地址,设置所有的GET方法都会自动添加foo=bar,当请求符合上述路由条件时,即可在后端服务上接收到Gateway网关添加的参数。
另外再介绍一种比较常用的filter,即StripPrefix gateway filter。
spring:
cloud:
gateway:
routes:
- id: ruoyi-gen
uri: lb://ruoyi-gen
predicates:
- Path=/gen/**
filters:
- StripPrefix=1
它会把地址 http://localhost:9527/gen/user/getname 转变为
Gateway请求匹配,spring cloud gateway 中断言的种类多达10种
我用到的是- Path 这个,根据请求路径来筛选满足条件的请求。
Gateway 熔断
Spring Cloud Gateway也可以利用Hystrix的熔断特性,在流量过大时进行服务降级,同时项目中必须加上Hystrix的依赖
spring:
cloud:
gateway:
routes:
#system 模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
# 限流配置
- StripPrefix=1
- name: RequestRateLimiter
args:
key-resolver: '#{@remoteAddrKeyResolver}'
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
# 降级配置
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: 'forward:/fallback'
hystrix:
command:
default: #default全局有效,service id指定应用有效
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 5000 #断路器超时时间,默认1000ms
上述配置种给出了熔断之后的返回路径。因此,在Gateway服务模块添加/fallback路径,以作为服务熔断时的返回路径。方法有多种, 常见的@RestControll,我这里介绍另一种写法:
熔断之后的处理 handler:
@Slf4j
@Component
public class HystrixFallbackHandler implements HandlerFunction<ServerResponse>
{
@Override
public Mono<ServerResponse> handle(ServerRequest serverRequest)
{
Optional<Object> originalUris = serverRequest.attribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
originalUris.ifPresent(originalUri -> log.error("网关执行请求:{}失败,hystrix服务降级处理", originalUri));
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromObject("服务异常"));
}
}
路由配置:
@Configuration
@AllArgsConstructor
public class RouterFunctionConfiguration
{
private final HystrixFallbackHandler hystrixFallbackHandler;
@Bean
public RouterFunction<?> routerFunction()
{
return RouterFunctions
.route(RequestPredicates.path("/fallback").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
hystrixFallbackHandler);
}
}
Gateway 重试路由器
Retry GatewayFilter通过四个参数来控制重试机制,参数说明如下:
retries:重试次数,默认值是 3 次。
statuses:HTTP 的状态返回码,取值请参考:org.springframework.http.HttpStatus。
methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值参考:org.springframework.http.HttpMethod。
series:一些列的状态码配置,取值参考:org.springframework.http.HttpStatus.Series。符合的某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有5个值。
配置方法可参考官方文档。
Gateway 的限流操作。
Spring Cloud Gateway 本身集成了限流操作。Gateway限流操作需要使用redis。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
spring:
redis:
database: 1
host: 10.1.4.182
port: 6379
password: # 密码(默认为空)
timeout: 6000ms # 连接超时时长(毫秒)
lettuce:
pool:
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
这是redis的配置信息。
限流的配置我这里就贴出关键点,完整配置上边贴过了:
filters:
# 限流配置
- StripPrefix=1
- name: RequestRateLimiter
args:
key-resolver: '#{@remoteAddrKeyResolver}'
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
BurstCapacity:令牌桶的总容量。
replenishRate:令牌通每秒填充平均速率。
Key-resolver:用于限流的解析器的Bean对象的名字。它使用SpEL表达式#{@beanName}从Spring容器中获取bean对象。
Key-resolver参数后面的bean需要自己实现,然后注入到Spring容器种。KeyResolver需要实现resolve方法,比如根据ip进行限流,则需要用hostAddress去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。还可以根据uri限流,同hostname限流是一样的。例如以ip限流为例,在gateway模块中添加以下实现:
/**
* 路由限流配置
*/
@Configuration
public class RateLimiterConfiguration
{
@Bean(value = "remoteAddrKeyResolver")
public KeyResolver remoteAddrKeyResolver()
{
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
自定义Gatewayfilter
关于自定义gatewayfilter 我这里要将两种方式:
第一种需要实现GatewayFilter和Ordered这两个接口。
/**
* 当前这种方式只能使用手动加入路由的方式, 配置不好使
* @author XJin
*
*/
public class RequestTimeFilter implements GatewayFilter,Ordered{
@Override
public int getOrder() {
return 0;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put("requestTimeBegin", System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(()->{
Long startTime = exchange.getAttribute("requestTimeBegin");
if(startTime!=null) {
System.err.println("请求路径:"+exchange.getRequest().getURI().getRawPath() + "消耗时间: " + (System.currentTimeMillis() - startTime) + "ms");
}
}));
}
}
这种方式在application.yml种配置不好使,可能是我自己的原因,我这边采用的是:
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes().route(r ->
r.path("/auth/*")
//转发路由
.uri("lb://ruoyi-auth")
//注册自定义过滤器
.filters(new RequestTimeFilter())
//给定id
.id("ruoyi-auth"))
.build();
}
我在项目中使用的是第二种方式:
/**
* 当前这种方式只能使用手动加入路由的方式, 配置不好使
* @author XJin
*
*/
@Component
public class RequestTimeFilter2 extends AbstractGatewayFilterFactory<RequestTimeFilter2.Config>{
public RequestTimeFilter2()
{
super(Config.class);
}
public static class Config
{
}
@Override
public String name()
{
return "RequestTimeFilter2";
}
@Override
public GatewayFilter apply(Config config) {
return new RequestTimeFilter();
}
}
这里边用到了第一种方法里的代码了,这种方式写的话可以在application.yml种配置:
自定义GlobalFilter
Spring Cloud Gateway根据作用范围分为GatewayFilter和GlobalFilter,二者区别如下:
GatewayFilter : 需要通过spring.cloud.gateway.routes.filters 配置在具体路由下,只作用在当前路由上或通过spring.cloud.gateway.default-filters配置在全局,作用在所有路由上。
GlobalFilter:全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上。
/**
* 网关鉴权
*/
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered
{
// 排除过滤的 uri 地址
// swagger排除自行添加
private static final String[] whiteList = {"/auth/login", "/user/register", "/system/v2/api-docs"};
@Resource(name = "stringRedisTemplate")/*RedisTemplate和StringRedisTemplate的区别:https://blog.youkuaiyun.com/guanripeng/article/details/89884167*/
private ValueOperations<String, String> ops;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
String url = exchange.getRequest().getURI().getPath();
log.info("url:{}", url);
// 跳过不需要验证的路径
if (Arrays.asList(whiteList).contains(url))
{
return chain.filter(exchange);
}
String token = exchange.getRequest().getHeaders().getFirst(Constants.TOKEN);
// token为空
if (StringUtils.isBlank(token))
{
return setUnauthorizedResponse(exchange, "token can't null or empty string");
}
String userStr = ops.get(Constants.ACCESS_TOKEN + token);
JSONObject jo = JSONObject.parseObject(userStr);
String userId = jo.getString("userId");
// 查询token信息
if (StringUtils.isBlank(userId))
{
return setUnauthorizedResponse(exchange, "token verify error");
}
// 设置userId到request里,后续根据userId,获取用户信息
ServerHttpRequest mutableReq = exchange.getRequest().mutate().header(Constants.CURRENT_ID, userId)
.header(Constants.CURRENT_USERNAME, jo.getString("loginName")).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
return chain.filter(mutableExchange);
}
private Mono<Void> setUnauthorizedResponse(ServerWebExchange exchange, String msg)
{
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] response = null;
try
{
response = JSON.toJSONString(R.error(401, msg)).getBytes(Constants.UTF8);
}
catch (UnsupportedEncodingException e)
{
e.printStackTrace();
}
DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
return originalResponse.writeWith(Flux.just(buffer));
}
@Override
public int getOrder()
{
return -200;
}
}
代码相对比较多主要是做一个token验证,并根据token取出登陆相关消息以便后续功能使用。针对所有请求。
下面是 关于Spring Cloud Gateway predicate(路由断言工厂)
https://www.cnblogs.com/wgslucky/p/11396579.html 讲的比较详细
关于Spring Cloud Gateway的全局过滤器
GlobalFilter 有十一个实现类,包括路由转发、负载均衡、ws 路由、netty 路由等全局过滤器
https://www.cnblogs.com/liukaifeng/p/10055862.html 介绍的比较详细如有需要查参考