赞
踩
Spring Cloud Gateway:
Spring Cloud Gateway是基于Spring生态系统之上构建的API网关,包括:Spring 5.x,Spring Boot 2.x和Project Reactor。Spring Cloud Gateway旨在提供一种简单而有效的方法来路由到API,并为它们提供跨领域的关注点,例如:安全性,监视/指标,限流等。
什么是服务网关:
API Gateway(APIGW / API 网关),顾名思义,是系统对外的唯一入口。
为什么要使用网关:
微服务的应用可能部署在不同机房,不同地区,不同域名下。此时客户端(浏览器/手机/软件工具)想 要请求对应的服务,都需要知道机器的具体 IP 或者域名 URL,当微服务实例众多时,这是非常难以记忆的,对 于客户端来说也太复杂难以维护。此时就有了网关,客户端相关的请求直接发送到网关,由网关根据请求标识 解析判断出具体的微服务地址,再把请求转发到微服务实例。这其中的记忆功能就全部交由网关来操作了。
核心概念:
路由(Route):路由是网关最基础的部分,路由信息由 ID、目标 URI、一组断言和一组过滤器组成。如果断言 路由为真,则说明请求的 URI 和配置匹配。
断言(Predicate):Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring 5.0 框架中 的 ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于 Http Request 中的任 何信息,比如请求头和参数等。
过滤器(Filter):一个标准的 Spring Web Filter。Spring Cloud Gateway 中的 Filter 分为两种类型,分别是 Gateway Filter 和 Global Filter。过滤器将会对请求和响应进行处理。
原理:
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。
使用网关:
1、添加依赖
<!-- spring cloud gateway 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2、resources/application.yml配置文件
server: port: 8080 spring: application: name: xxx-gateway cloud: gateway: routes: # 系统模块 - id: xxx-system #路由的ID,没有固定规则但要求唯一,建议配合服务名 uri: http://localhost:9201/ #匹配后提供服务的路由地址 predicates: - Path=/system/** filters: - StripPrefix=1
解释:配置了一个 id 为xxx-system 的路由规则,如果浏览器的请求路径,匹配上了 Path=/system/** ,则此处predicates断言为真。就会去访问对应的uri http://localhost:9201/system/**
提示:网关服务,用于路由转发、异常处理、限流、降级、接口、鉴权等等。
路由规则
Spring Cloud Gateway创建Route对象时, 使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route。
Spring Cloud Gateway包含许多内置的Route Predicate Factories。
所有这些断言都匹配 HTTP 请求的不同属性。
多个Route Predicate Factories可以通过逻辑与(and)结合起来一起使用。
路由断言工厂包含的主要实现类如图所示,包括Datetime、请求的远端地址、路由权重、请求头、Host 地址、请求方法、请求路径和请求参数等类型的路由断言。
Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。网上有一张图总结了 Spring Cloud 内置的几种 Predicate 的实现。
Datetime
匹配日期时间之后发生的请求
predicates:
- After=2021-02-23T14:20:00.000+08:00[Asia/Shanghai]
After:在这个时间之前生效
Before:在这个时间之前生效
Between在这两个时间之间生效
Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
Cookie
匹配指定名称且其值与正则表达式匹配的cookie
Cookie Route Predicate需要两个参数,一个是Cookie name ,一个是正则表达式。
路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
predicates:
- Cookie=loginname, ruoyi
Header
匹配具有指定名称的请求头,\d+值匹配正则表达式
两个参数:一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
predicates:
- Header=X-Request-Id, \d+
Host
匹配主机名的列表
predicates:
- Host=**.somehost.org,**.anotherhost.org
Method
匹配请求methods的参数,它是一个或多个参数
predicates:
- Method=GET,POST
Path
匹配请求路径
predicates:
- Path=/system/**
Query
匹配查询参数
predicates:
- Query=username, abc.
RemoteAddr
匹配IP地址和子网掩码
predicates:
- RemoteAddr=192.168.10.1/0
Weight
匹配权重
routes:
- id: xxx-system-a
uri: http://localhost:9201/
predicates:
- Weight=group1, 8
- id: xxx-system-b
uri: http://localhost:9201/
predicates:
- Weight=group1, 2
Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。
路由配置
在spring cloud gateway中配置uri有三种方式,包括
websocket配置方式
uri: ws://localhost:9090/
http地址配置方式
uri: http://localhost:9090/
注册中心配置方式
uri: lb://xxx-api
实现动态路由需要注意的是uri的协议为lb,表示启用Gateway的负载均衡功能。
当服务提供者有多个时,Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
全局过滤器
全局过滤器作用于所有的路由,不需要单独配置,我们可以用它来实现很多统一化处理的业务需求,比如权限认证,IP访问限制等等。目前网关统一鉴权AuthFilter.java就是采用的全局过滤器。
单独定义只需要实现GlobalFilter, Ordered这两个接口就可以了。
@Component public class AuthFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取请求参数中的 token String token = exchange.getRequest().getQueryParams().getFirst("token"); if (null == token) { ServerHttpResponse response = exchange.getResponse(); response.getHeaders().add("Content-Type", "application/json; charset=utf-8"); String message = "{\"message\":\"请求token信息不能为空\"}"; DataBuffer buffer = response.bufferFactory().wrap(message.getBytes()); return response.writeWith(Mono.just(buffer)); } return chain.filter(exchange); } /** * 加载过滤器顺序,数字越小优先级越高 * @return */ @Override public int getOrder() { return 0; } }
自定义过滤器的方法是实现GlobalFilter接口、实现Ordered接口。这两个接口的全路径为:
org.springframework.cloud.gateway.filter.GlobalFilter
org.springframework.core.Ordered
ServerWebExchange
是『Spring Reactive Web 世界中』HTTP 请求与响应交互的契约。提供对 HTTP 请求和响应的访问,并公开额外的服务器端处理相关属性和特性,如请求属性。
其实,ServerWebExchange 命名为『服务网络交换器』,存放着重要的请求-响应属性、请求实例和响应实例等等,有点像 Servlet 中的 Context 的角色。
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。Spring Cloud Gateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生。
PrefixPath 过滤器
作用: 它的作用和StripPrefix正相反,是在URL路径前面添加一部分的前缀
例:
spring:
cloud:
gateway:
routes:
- id: bds-lbs-service
uri: lb://bds-lbs-service
predicates:
- Path=/lbs/**
filters:
- StripPrefix=1
– StripPrefix=1
http://bds-lbs-service/lbs/hello变成http://bds-lbs-service/hello,其中去掉了/lbs
– PrefixPath=/lbs
所以对/hello的请求将会被发送到/lbs/hello
限流配置
顾名思义,限流就是限制流量。通过限流,我们可以很好地控制系统的 QPS,从而达到保护系统的目的。
常见的限流算法有:计数器算法,漏桶(Leaky Bucket)算法,令牌桶(Token Bucket)算法。
Spring Cloud Gateway官方提供了RequestRateLimiterGatewayFilterFactory过滤器工厂,使用Redis 和Lua脚本实现了令牌桶的方式。
filters:
# 作用: 去掉部分URL路径.1表示这里去掉一个前缀
- StripPrefix=1 #配置,表示网关转发到业务模块时候会自动截取前缀。
- name: RequestRateLimiter
args:
# 令牌入桶的速度为每秒100个,相当于QPS
redis-rate-limiter.replenishRate: 100 # 令牌桶每秒填充速率
# 桶内能装200个令牌,相当于峰值,要注意的是:
# 第一秒从桶内能去200个,但是第二秒只能取到100个了,
# 因为入桶速度是每秒100个
redis-rate-limiter.burstCapacity: 200 # 令牌桶总容量
# 每个请求需要的令牌数
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@pathKeyResolver}" # 使用 SpEL 表达式按名称引用bean
熔断降级
添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
配置需要熔断降级服务
filters:
- StripPrefix=1
# 降级配置
- name: Hystrix
args:
name: default
# Hystrix超时降级后调用uri地址
fallbackUri: 'forward:/fallback'
上面配置包含了一个Hystrix过滤器,该过滤器会应用Hystrix熔断与降级,会将请求包装成名为fallback的路由指令RouteHystrixCommand,RouteHystrixCommand继承于HystrixObservableCommand,其内包含了Hystrix的断路、资源隔离、降级等诸多断路器核心功能,当网关转发的请求出现问题时,网关能对其进行快速失败,执行特定的失败逻辑,保护网关安全。
配置中有一个可选参数fallbackUri,当前只支持forward模式的URI。如果服务被降级,请求会被转发到该URI对应的控制器。控制器可以是自定义的fallback接口;也可以使自定义的Handler,需要实现接口 :
HandlerFunction
org.springframework.web.reactive.function.server.HandlerFunction<T extends ServerResponse>。
实现添加熔断降级处理返回信息
/** * 熔断降级处理 */ @Component public class HystrixFallbackHandler implements HandlerFunction<ServerResponse> { private static final Logger log = LoggerFactory.getLogger(HystrixFallbackHandler.class); @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.APPLICATION_JSON) .body(BodyInserters.fromValue(JSON.toJSONString(R.fail("服务已被降级熔断")))); } }
路由配置信息加一个控制器方法用于处理重定向的/fallback请求
/** * 路由配置信息 * * @author ruoyi */ @Configuration public class RouterFunctionConfiguration { @Autowired private HystrixFallbackHandler hystrixFallbackHandler; @Autowired private ValidateCodeHandler validateCodeHandler; @SuppressWarnings("rawtypes") @Bean public RouterFunction routerFunction() { return RouterFunctions .route(RequestPredicates.path("/fallback").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), hystrixFallbackHandler) .andRoute(RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), validateCodeHandler); } }
全局配置: default-filters
spring: application: name: gateway-server # 应用名称 cloud: gateway: discovery: locator: # 是否与服务发现组件进行结合,通过 serviceId 转发到具体的微服务 enabled: true #是否开启基于服务发现得路由规则 lower-case-service-id: true # 是否将服务名称转小写 # 全局配置 default-filters: # 限流过滤器 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 # 令牌桶每秒填入令牌的速率 redis-rate-limiter.burstCapacity: 200 # 令牌桶的总容量 key-resolver: "#{@ipKeyResolver}" # 使用SpEL 表达式按名称引入Bean # 熔断降级配置 - name: Hystrix args: name: default #转发地址 fallbackUri: 'forward:/defaultFallback' # hystrix设置隔离策略为信号量,超时时间为3秒 hystrix: command: default: #default全局有效,service id指定应用有效 execution: isolation: strategy: SEMAPHORE thread: timeoutInMilliseconds: 3000
跨域配置
项目采用的是前后端分离,如果页面直接调用后端的域名或IP,故存在跨域问题。
什么是跨域问题?
跨域,指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制。
什么是同源?
所谓同源是指,域名,协议,端口均相同
http://www.ruoyi.vip --> http://admin.ruoyi.vip 跨域
http://www.ruoyi.vip --> http://www.ruoyi.vip 非跨域
http://www.ruoyi.vip --> http://www.ruoyi.vip:8080 跨域
http://www.ruoyi.vip --> https://www.ruoyi.vip 跨域
使用 CORS(跨资源共享)解决跨域问题:
CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。
CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10
整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉
因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信
跨域调用示例
// 登录方法
export function login(username, password, code, uuid) {
return request({
// 会出现跨域(通过IP或域名直接访问网关接口)
url: 'http://192.168.31.138:8080/auth/login',
// 不会出现跨域(开发环境默认通过proxy代理的方式,同理部署到生产也需要配置nginx代理)
url: '/auth/login',
headers: {
isToken: false
},
method: 'post',
data: { username, password, code, uuid }
})
}
配置方式:可以在nacos配置中心ruoyi-gateway-dev.yml文件中加入以下配置解决跨域问题
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOriginPatterns: "*"
allowed-methods: "*"
allowed-headers: "*"
allow-credentials: true
exposedHeaders: "Content-Disposition,Content-Type,Cache-Control"
代码方式:新增CorsConfig.java跨域代码配置
/** * 跨域配置 */ @Configuration public class CorsConfig { /** * 这里为支持的请求头,如果有自定义的header字段请自己添加 */ private static final String ALLOWED_HEADERS = "X-Requested-With, Content-Type, Authorization, credential, X-XSRF-TOKEN, token, Admin-Token, App-Token"; private static final String ALLOWED_METHODS = "GET,POST,PUT,DELETE,OPTIONS,HEAD"; private static final String ALLOWED_ORIGIN = "*"; private static final String ALLOWED_EXPOSE = "*"; private static final String MAX_AGE = "18000L"; @Bean public WebFilter corsFilter() { return (ServerWebExchange ctx, WebFilterChain chain) -> { ServerHttpRequest request = ctx.getRequest(); if (CorsUtils.isCorsRequest(request)) { ServerHttpResponse response = ctx.getResponse(); HttpHeaders headers = response.getHeaders(); headers.add("Access-Control-Allow-Headers", ALLOWED_HEADERS); headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS); headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN); headers.add("Access-Control-Expose-Headers", ALLOWED_EXPOSE); headers.add("Access-Control-Max-Age", MAX_AGE); headers.add("Access-Control-Allow-Credentials", "true"); if (request.getMethod() == HttpMethod.OPTIONS) { response.setStatusCode(HttpStatus.OK); return Mono.empty(); } } return chain.filter(ctx); }; } }
提示:如果可以通过部署手段(例如nginx配置代理等)解决跨域,则可以不需要添加跨域支持
server { listen 80; # 监听的端⼝ server_name localhost; # 域名或ip location / { # 访问路径配置 一般加 /api ,表示监听所有“/api”开头的http请求 #允许跨域请求的域,* 代表所有 add_header 'Access-Control-Allow-Origin' *; #允许带上cookie请求 add_header 'Access-Control-Allow-Credentials' 'true'; #允许请求的方法,比如 GET/POST/PUT/DELETE add_header 'Access-Control-Allow-Methods' *; #允许请求的header add_header 'Access-Control-Allow-Headers' *; root /usr/share/nginx/html;# 根⽬录 index index.html index.htm; # 默认⾸⻚ } error_page 500 502 503 504 /50x.html; # 错误⻚⾯ location = /50x.html { root html; } }
黑名单配置
顾名思义,就是不能访问的地址。实现自定义过滤器BlackListUrlFilter,需要配置黑名单地址列表blacklistUrl,当然有其他需求也可以实现自定义规则的过滤器。
filters:
- StripPrefix=1
- name: BlackListUrlFilter
args:
blacklistUrl:
- /user/list
白名单配置
顾名思义,就是允许访问的地址。且无需登录就能访问。
在ignore中设置whites,表示允许匿名访问。
# 不校验白名单
ignore:
whites:
- /auth/logout
- /auth/login
- /*/v2/api-docs
- /csrf
实现Sentinel限流
Sentinel 支持对 Spring Cloud Gateway、Netflix Zuul 等主流的 API Gateway 进行限流。
添加依赖
<!-- SpringCloud Alibaba Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- SpringCloud Alibaba Sentinel Gateway -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
限流规则配置类
** * 网关限流配置 * * @author ruoyi */ @Configuration public class GatewayConfig { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelFallbackHandler sentinelGatewayExceptionHandler() { return new SentinelFallbackHandler(); } @Bean @Order(-1) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } @PostConstruct public void doInit() { // 加载网关限流规则 initGatewayRules(); } /** * 网关限流规则 */ private void initGatewayRules() { Set<GatewayFlowRule> rules = new HashSet<>(); rules.add(new GatewayFlowRule("ruoyi-system") .setCount(3) // 限流阈值 .setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒 // 加载网关限流规则 GatewayRuleManager.loadRules(rules); } }
测试验证,一分钟内访问三次系统服务出现异常提示表示限流成功
Sentinel分组限流
限流规则配置类
/** * 网关限流配置 */ @Configuration public class GatewayConfig { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelFallbackHandler sentinelGatewayExceptionHandler() { return new SentinelFallbackHandler(); } @Bean @Order(-1) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } @PostConstruct public void doInit() { // 加载网关限流规则 initGatewayRules(); } /** * 网关限流规则 */ private void initGatewayRules() { Set<GatewayFlowRule> rules = new HashSet<>(); rules.add(new GatewayFlowRule("system-api") .setCount(3) // 限流阈值 .setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒 rules.add(new GatewayFlowRule("code-api") .setCount(5) // 限流阈值 .setIntervalSec(60)); // 加载网关限流规则 GatewayRuleManager.loadRules(rules); // 加载限流分组 initCustomizedApis(); } /** * 限流分组 */ private void initCustomizedApis() { Set<ApiDefinition> definitions = new HashSet<>(); // ruoyi-system 组 ApiDefinition api1 = new ApiDefinition("system-api").setPredicateItems(new HashSet<ApiPredicateItem>() { private static final long serialVersionUID = 1L; { // 匹配 /user 以及其子路径的所有请求 add(new ApiPathPredicateItem().setPattern("/system/user/**") .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); } }); // ruoyi-gen 组 ApiDefinition api2 = new ApiDefinition("code-api").setPredicateItems(new HashSet<ApiPredicateItem>() { private static final long serialVersionUID = 1L; { // 只匹配 /job/list add(new ApiPathPredicateItem().setPattern("/code/gen/list")); } }); definitions.add(api1); definitions.add(api2); // 加载限流分组 GatewayApiDefinitionManager.loadApiDefinitions(definitions); } }
访问:http://localhost:8080/system/user/list (触发限流 )
访问:http://localhost:8080/system/role/list (不会触发限流)
访问:http://localhost:8080/code/gen/list (触发限流)
访问:http://localhost:8080/code/gen/xxxx (不会触发限流)
Sentinel自定义异常
为了展示更加友好的限流提示, Sentinel支持自定义异常处理。
方案一:yml配置
# Spring
spring:
cloud:
sentinel:
scg:
fallback:
mode: response
response-body: '{"code":403,"msg":"请求超过最大数,请稍后再试"}'
方案二:GatewayConfig注入Bean
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelFallbackHandler sentinelGatewayExceptionHandler()
{
return new SentinelFallbackHandler();
}
SentinelFallbackHandler.java
/** * 自定义限流异常处理 */ public class SentinelFallbackHandler implements WebExceptionHandler { private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) { ServerHttpResponse serverHttpResponse = exchange.getResponse(); serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); byte[] datas = "{\"code\":429,\"msg\":\"请求超过最大数,请稍后再试\"}".getBytes(StandardCharsets.UTF_8); DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas); return serverHttpResponse.writeWith(Mono.just(buffer)); } @Override public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { if (exchange.getResponse().isCommitted()) { return Mono.error(ex); } if (!BlockException.isBlockException(ex)) { return Mono.error(ex); } return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange)); } private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) { return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。