前面学的Nacos是对内负载均衡,现在学的Gateway网关是对外负载均衡和校验,不冲突
一、网关的作用
网关功能:
1、身份认证和权限校验
2、服务路由、负载均衡
3、请求限流
在SpringCloud中有两个组件可以实现网关,分别是gateway、zuul
Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于spriing5中提供的WebFlux,属于响应式编程(非阻塞式)的实现,具备更好的性能
我们都会使用SpringCloudGateway
二、网关的快速入门
网关路由可以配置的内容包括如下
路由id: 路由唯一标识
uri: 路由目的地,支持lb和http两种
predicates: 路由断言,判断请求是否符合要求,符合则转发到路由目的地
filters: 路由过滤器,处理请求或响应
搭建网关服务的步骤如下
第一步: 由于网关是一个服务,所以需要在cloud-demo总项目中新建一个模块,模块名为gateway
第二步: 在gateway微服务项目的pom.xml修改为如下
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gateway</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--nacos服务注册发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--网关gateway依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
</project>
第三步: 在gateway微服务项目的src/main/java目录下新建cn.itcast.gateway.GatewayApplication类,写入如下
package cn.itcast.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author 35238
* @date 2023/5/28 0028 14:06
*/
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
第四步: 在gateway微服务项目的src/main/resources目录下新建File,File名为application.yml,写入如下
server:
port: 10010 # 服务端口
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 自定义网关路由规则,多个规则就用-
- id: user-service # 路由标示,必须唯一
uri: lb://UserService # 路由的目标地址
predicates:
- Path=/user/** # 路由断言,判断路径是否以/user开头,如果是则符合
- id: order-service
uri: lb://OrderService
predicates:
- Path=/order/**
第五步。测试(先确保你的nacos已经启动),先运行GatewayApplication类,然后重启OrderApplication、UserApplication、UserApplication2服务
浏览器访问: http://localhost:10010/user/1,注意要http://localhost:10010/user/开头,因为我们在第四步指定了路由断言
可以发现,我们并没有在gateway微服务项目写任何业务代码,但是却能用gateway微服务项目的路径访问到数据,原因就是网关路由,把gateway微服务项目的请求路由到我们指定的其他微服务去了,我们访问这个gateway微服务项目,实际访问的是OrderService和UserService微服务
三、路由断言工厂
路由断言工厂Route Predicate Factory
网关路由可以配置的内容包括如下
路由id: 路由唯一标识
uri: 路由目的地,支持lb和http两种
predicates: 路由断言,判断请求是否符合要求,符合则转发到路由目的地
filters: 路由过滤器,处理请求或响应
接下来,就重点学习predicates的配置
我们在上面快速入门的application.yml配置文件中写的断言规则只是字符串,这些字符串会被断言工厂(Predicate Factory)读取并处理,转变为路由判断的条件
例如 Path=/user/**是按照路径匹配,这个规则是由
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的
像这样的断言工厂在SpringCloudGateway还有几十个
spring提供了11种基本的Predicate工厂,如下表。为了避免md语法冲突,我在下表写的※其实就是*
名称 | 说明 | 示例 |
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host (域名) | - Host=**.somehost.org,※※.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则,多个路径的话逗号隔开,只要符合其中一个就算符合 | - Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
Weight | 权重处理 |
不会写也没事,spring官网有提供12种断言工厂的示例
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
演示如下,在刚刚快速入门的gateway微服务项目的application.yml里面添加如下
# 演示After断言工厂,也就是用户必须在亚洲上海时区2037-01-20之后访问,才算符合规则,才会让用户去请求路由到达OrderService服务
- After=2037-01-20T17:42:47.789-07:00[America/Denver] #明显我们现在的时间是不符合要求的,所以等下演示会报404
然后重启GatewayApplication服务,浏览器访问http://localhost:10010/order/102
四、路由过滤工厂
路由的过滤器配置,路由过滤器GatewayFilter
网关路由可以配置的内容包括如下
路由id: 路由唯一标识
uri: 路由目的地,支持lb和http两种
predicates: 路由断言,判断请求是否符合要求,符合则转发到路由目的地
filters: 路由过滤器,处理请求或响应
接下来,就重点学习filters的配置
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。不仅可以对请求做处理,还可以对响应做处理,下面是流程图
spring提供了37种不同的路由过滤工厂,如下表
名称 | 说明 |
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
... | ... |
不会写也没事,spring官网有提供37种过滤器工厂的示例
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
案例:
演示如下
第一步: 在刚刚快速入门的gateway微服务项目的application.yml里面添加如下。
表示给所有进入UserService服务的请求添加一个请求头KEKE:keke is you god
server:
port: 10010 # 服务端口
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 自定义网关路由规则,多个规则就用-
- id: user-service # 路由标示,必须唯一
uri: lb://UserService # 路由的目标地址
predicates:
- Path=/user/** # 路由断言,判断路径是否以/user开头,如果是则符合
filters:
- AddRequestHeader=KEKE,keke is you god
- id: order-service
uri: lb://OrderService
predicates:
- Path=/order/**
- Before=2037-01-20T17:42:47.789-07:00[America/Denver]
然后重启GatewayApplication服务
第二步: 由于请求UserService服务时,网关给路径自动追加了请求头参数,所以我们需要去user-service微服务的src/main/java/cn.itcast.user/web目录的UserController类里面稍微修改一下请求,加一个接收参数的参数
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "KEKE",required = false) String keke) {
System.out.println("获取了请求头" + keke);
return userService.queryById(id);
}
然后重启UserService服务
第三步: 测试。浏览器访问http://localhost:10010/user/1,访问之后回到终端看一下UserService的控制台日志信息,看有没有打印sout那个语句
思考:
我们只是在gateway微服务项目的application.yml配置里面给访问UserService服务(user-service微服务项目)的请求添加了请求头,要是需要给所有请求微服务项目的请求都加上请求头,那岂不是在这个application.yml里面给所以相关微服务都写一遍这个代码,太麻烦了
解决:
默认过滤器,如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下(和routes同一级)
#演示默认过滤器,会对所有的路由请求都生效
default-filters:
- AddRequestHeader=KEKE,keke is you god #格式: - AddRequestHeader=key,value
测试:
注释掉之前在user-service模块下配置文件中刚才配置的路由过滤器。然后重启GatewayApplication服务,浏览器访问http://localhost:10010/user/2,访问之后回到终端看一下UserService的日志信息,看有没有打印sout那个语句
server:
port: 10010 # 服务端口
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 自定义网关路由规则,多个规则就用-
- id: user-service # 路由标示,必须唯一
uri: lb://UserService # 路由的目标地址
predicates:
- Path=/user/** # 路由断言,判断路径是否以/user开头,如果是则符合
# filters:
# - AddRequestHeader=KEKE,keke is you god
- id: order-service
uri: lb://OrderService
predicates:
- Path=/order/**
- Before=2037-01-20T17:42:47.789-07:00[America/Denver]
#演示默认过滤器,会对所有的路由请求都生效
default-filters:
- AddRequestHeader=KEKE,keke is you god #格式: - AddRequestHeader=key,value
五、全局过滤器
全局过滤器GlobalFilter的作用是拦截所有进入网关的请求和微服务响应,与GatewayFilter默认过滤器的作用一样
上面刚学的默认过滤器虽然可以作用于所有进入网关的请求和微服务响应,但是默认过滤器是通过配置的方式来定义的,配置的仅仅是参数,过滤器的业务逻辑是无法控制的,由spring写死的,功能有限。但是,如果某些业务比较复杂,例如请求进来后端,但是我想知道是谁发起的,身份是什么,有没有权限访问我,那么这些额外是功能就需要自定义自己来写,而全局过滤器GlobalFilter就能实现这个,特点是可以自定义功能
如何才能使用全局过滤器GlobalFilter,我们只需要实现GlobalFilter接口即可,GlobalFilter接口的方法如下
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
//exchange: 请求上下文,里面可以获取Request、Response等信息
//chain: 过滤器队列,用来把请求委托给下一个过滤器,也就是放行,交给下一个过滤器
//Mono<Void>: 返回值
//filter: 方法名
案例:
定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件
条件1: 参数中是否有authorization
条件2: authorization参数值是否为admin
如果同时满足则放行,否则拦截
具体实现如下
第一步: 在gateway微服务项目的src/main/java/cn.itcast.gateway目录新建AuthorizeFilter类,写入如下
package cn.itcast.gateway;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.lang.annotation.Annotation;
@Order(-1)//过滤器执行的前后顺序,值越小越先执行,可能你的同事也定义有过滤器,所以这个可以用数字设置自己这个过滤器先执行还是后执行
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//第一步:获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
//第二步:获取参数中的authorization
String auth = params.getFirst("authorization");
//判断参数是否等于admin
if("admin".equals(auth)){
//是 放行
return chain.filter(exchange);
}
//响应一个未认证的状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
//否 拦截
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return -1; //order的值越小,过滤器优先级越高
}
}
第二步: 重启GatewayApplication服务,分别在浏览器访问http://localhost:10010/user/2,
http://localhost:10010/user/2?authorization=admin
六、过滤器链执行顺序
在上面的 '全局过滤器' 的第一步里面,我们初步使用了@Order注解来指定order值(我上一节通过实现Ordered接口去指定order的值,注解和实现接口两种方式都可以),使得我们写的 'AuthorizeFilter过滤器类' 的优先级最高,不被覆盖(当然也没有谁来覆盖,因为就只定义过这个过滤器)
当请求路由之后,会将 '当前路由过滤器'、'DefaultFilter'、'GlobalFilter',合并到一个过滤器链(集合)中,然后对这些过滤器进行排序,然后依次执行每个过滤器
过滤器执行顺序:
1、每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
2、'GlobalFilter' 通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定order值大小
3、'路由过滤器'、'defaultFilter' 通过spring来指定order值,默认是按照声明顺序从1递增
从上图中,我们不难发现,'路由过滤器' 和 'defaultFilter过滤器' 的order值是有可能同样的,那这时候这俩order值不就一样了吗,这还怎么判断谁优先,另外由于'GlobalFilter过滤器'是可以通过@Order注解直接指定order值,那么此时三种过滤器的order值可能就一模一样,那靠order还怎么判断谁优先级
结论:
1、同一种过滤器中,如果order值越小,那么优先级越高
2、不同过滤器中,如果order值越小,那么优先级越高
3、不同过滤器中,如果order值相同,那么优先级为 'defaultFilter过滤器' > '路由过滤器' > 'GlobalFilter过滤器'
七、网关的cors跨域配置
跨域问题处理
在微服务项目中,所有的请求都必须先进入网关,然后由网关路由到某个具体的微服务,也就是跨域请求实际上不需要在每个微服务里面都处理一遍,仅仅在网关处理即可,所以我们需要学习如何在网关,来处理跨域请求
网关是基于Netflix来实现的
跨域也就是域名不一致,主要包括域名不同、域名相同但端口不同。
跨域问题: 浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案: CORS(浏览器会不断询问服务器: 你让不让别人跨域访问你)
上图倒数第三行的*表示允许所有请求头跨域
上图的最后一行的360000是有限期,作用是减少性能损耗(损耗来源: 浏览器不断询问服务器),当时间超过这个值,就表名跨域请求的有效期过了,浏览器将不再向服务器发起询问,而是直接放行
上图的'[/**]'表示拦截所有请求,凡是进入网关的请求都会进行跨域处理
演示跨域并解决跨域,如下
第一步: 打开前端页面并向服务器发送请求,模拟跨域请求,8090端口启动它
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div>
</div>
</body>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
axios.get("http://localhost:10010/user/1?authorization=admin").then(function (resp){
console.log(resp.data);
}).catch(function (err){
console.log(err);
})
</script>
</html>
第二步: 在网关微服务那里,写入处理跨域的代码
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
能够正常访问并且拿到数据