为什么需要网关
网关功能:
- 身份认证和权限校验
- 服务路由,负载均衡
- 请求限流
网关的技术实现
在SpringCloud中网关的实现包括两种:
- gateway
- zuul
Zuul是基于Servlet的视线,属于阻塞式编程,而SpringCloudGateway则是属于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
搭建网关服务
搭建网关服务的步骤:
- 1、创建新的module,引入SpringCloudGateway的依赖和cacos的服务发现依赖:
<!-- 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>
- 2、编写路由配置及nacos地址:
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:1111 #nacos地址
gateway:
routes: # 网关的配置
- id: user-service # 路由id,自定义,只要唯一即可
uri:
lb://userserver # 路由的目标地址,如果以http开头就是固定地址,如果以lb开头就是负载均衡的集群地址,后面跟上服务的名称即可
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头的就符合要求
然后我们访问的时候,就不是访问之前的了,而是使用网关的IP和端口去访问之前的服务:
这里我们用网关的服务,依然可以访问到之前的服务,这就表示网关已经成功配置并完成了路由这一功能。
网关代理流程
首先我们有一个Nacos注册中心,然后在里面注册了两个业务服务,一个网关服务,这时候用户向网关服务的端口发送了一个请求,这个请求首先会被网关所接收,但是网关无法做业务逻辑的处理,所以他首先回到路由中匹配请求的路由规范,此时他发现有一个能够匹配的路由规范,那么就会在Nacos中找到对应的服务,做负载均衡之后,发送请求,流程图如下:
总结
网关搭建步骤:
- 创建项目,引入nacos服务发现和gateway依赖
- 配置application.yml,包括服务基本信息,nacos地址,路由
路由配置包括:
- 路由id:路由的唯一标识
- 路由目标(uri):路由的目标地址,http代表固定地址,lb表示根据服务名负载均衡
- 路由断言(predicates):判断路由的规则
路由断言工厂Router Predicate Factory
网关路由可以配置的内容包括:
- 路由id:路由唯一标识
- uri:路由目的地,支持lb和http两种
- predicates:路由判断,判断请求是否符合要求,符合则转发到路由目的地
- filter:路由过滤器,处理请求或响应
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
例如Path=/user/**是按照路由匹配,这个规则是由
org.springframework.cloud.gateway.handler.prodicate.PathRouterPrecateFactory类处理的
像这样的断言工厂在SpringCLoudGateway中还有十几个。
SpringCLoudGateway提供了11种基本的Predicate工厂:
当然,这么多的工厂我们肯定是不需要全部记住的,在官网上他都有对应每个工厂的示例和使用方法:Spring Cloud Gateway
比如我们看到第一个,After Route Predicate Factory,他表示只能在某个节点之后才能访问,同样的,在官网上他给出了一个示例,我们直接复制就可以:
我们把这句话复制到自己项目的网关服务的对应配置文件中的位置上:
我们修改一下配置,将时间修改成2031年,这样就表示只有在2031年之后才能访问,然后我们重启网关服务。
等待重启完成之后,我们来到浏览器,首先对userserver进行访问:
很好,没有问题,然后对order服务进行访问:
出现404就表示你的请求被网关拒绝了。
路由过滤器GatewayFilter
GatewayFilter是网关提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。也就是除了请求,还会对响应做处理。
过滤器就是我们在访问真正到达服务器之前,对请求信息做一些调整或者校验,同样的,在相应信息真正到达用户之前,也可以做对应的调整,这就和之前我们在SpringMVC中学过的Filter是一样的:
Spring提供了31种不同的路由:Spring Cloud Gateway
可以看到,在这个页面中展示出来很多很多的过滤器,其中大部分的过滤器其实一看名字就知道是干什么的。
并且在过滤器里面都有详细的介绍,告诉你他是干什么用的,只不过用的是英文介绍,机翻可能有一些问题,需要自己看明白。
案例
给所有进入userserver的请求添加一个请求头:Truth=itcast is freaking awesome!
实现方式:在gateway中修改application.yml文件,给userserver的路由添加过滤器:
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:1111 #nacos地址
gateway:
routes: # 网关的配置
- id: user-service # 路由id,自定义,只要唯一即可
uri:
lb://userserver # 路由的目标地址,如果以http开头就是固定地址,如果以lb开头就是负载均衡的集群地址,后面跟上服务的名称即可
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
-
Path=/user/** # 这个是按照路径匹配,只要以/user/开头的就符合要求
-
id: order-service
uri:
lb://orderserver
predicates:
-
Path=/order/**
-
After=2031-01-20T17:42:47.789-07:00[America/Denver]
filters: # 过滤器
- AddRequestHeader=Truth,Itcast is freaking awesome! # 添加请求头
配置文件写好之后,我们就需要来到对应的配置了过滤器的服务的Controller中,因为我们要验证是否添加成功,我们需要在Controller中获取对应的请求头,也就是我们在配置文件中添加的请求头的内容:
package cn.itcast.order.web;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId,
@RequestHeader(value = "Truth",required = false) String Truth) {
// 根据id查询订单并返回
System.out.println("Truth"+Truth);
return orderService.queryOrderById(orderId);
}
}
在方法的参数列表中使用@RequestHeader,然后value属性是要拿到的请求头的名称,required为false表示并不是必须的,然后用一个变量接受,之后在方法中直接输出这个变量即可,这样我们在调用这个方法的时候就可以看到有对应请求头的内容输出在控制台中。
然后我们重启网关服务和对应的修改了Controller的服务,清空日志之后,来到浏览器做请求:
首先请求没有问题,说明网关在线,然后来到控制台查看日志:
在这里也看到了我们在配置文件中写入的请求头对应的信息,这就表示我们的配置文件也是正确的,确实是添加成功了对应的信息。
默认过滤器
如果要对所有的路由都生效,则可以将过滤器工厂写到default下,这样对所有的路由都生效:
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:1111 #nacos地址
gateway:
routes: # 网关的配置
- id: user-service # 路由id,自定义,只要唯一即可
uri:
lb://userserver # 路由的目标地址,如果以http开头就是固定地址,如果以lb开头就是负载均衡的集群地址,后面跟上服务的名称即可
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
-
Path=/user/** # 这个是按照路径匹配,只要以/user/开头的就符合要求
-
id: order-service
uri:
lb://orderserver
predicates:
- Path=/order/**
- After=2031-01-20T17:42:47.789-07:00[America/Denver]
filters: # 过滤器
- AddRequestHeader=Truth,Itcast is freaking awesome! # 添加请求头
default-filters:
- AddRequestHeader=Truth,Itcast is freaking awesome! # 添加请求头
然后我们再次重启网关服务,这次我们在浏览器再次对之前的服务进行访问,这次我注释掉了之前在单个服务中配置的过滤器,如果这次依然能够拿到请求头信息,则表示这个属性配置的确实是全局的路由属性:
这次我们请求两次,可以看到我们都成功拿到了对应请求头中的数据。
总结
- 过滤器的作用是什么?
- 对路由的请求或响应做加工处理,比如添加请求头
- 配置在路由下的过滤器只对当前路由的请求生效
- defaultFilters的作用是什么?
- 对所有的路由都生效的过滤器
全局过滤器GlobalFilter
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。
区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现。
定义方式是实现GlobalFilter接口:
package org.example.cn.itcast.other;
import org.springframework.web.server.ServerWebExchange;
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
* @param exchange 请求上下文,里面可以获取Request,Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<Void>} 返回表示当前业务结束
*/
Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain);
}
看到这个过滤器接口,其中比较重要的就是方法里面的两个参数,我们可以通过第一个参数拿到对应的数据进行处理,然后用第二个方法放行,交给下一个过滤器处理。
案例
定义全局过滤器,拦截并判断用户身份
需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
- 参数中是否有authorization
- authorization参数值是否为admin
如果同时满足则放行,否则拦截
自定义过滤器步骤:
- 1、自定义类,实现GlobalFilter接口,添加@Order注解:
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;
@Component
//@Order(-1)
public class AuthorizeFilter implements GlobalFilter , Ordered { // 实现接口
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> queryParams = request.getQueryParams();
String first = queryParams.getFirst("authorization");
// 判断参数的值
if (first.equals("admin")){
// 如果符合逻辑,则放行
return chain.filter(exchange);
}else {
// 如果不符合,则拦截
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
@Override
public int getOrder() {
return -1;
}
}
通过exchange获取数据,通过chine选择放行或者拦截,同时,这个类要是一个JavaBean,所以要添加@Component注解,同时要添加一个@Order注解,这个注解表示过滤器的优先级,这个值越小,优先级越高,默认是一个很大的数,一般我们写-1。除了使用注解的方式设置过滤器的优先级,还可以通过实现Ordered的方式去设置过滤器的优先级。过滤器一定要有顺序,使用方式无所谓。
过滤器执行顺序
请求进入网关会碰到三类过滤器:当前路由过滤器,DefaultFilter,GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter,GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器。
其中,当前路由过滤器和DefaultFilter都是在配置文件中配置的,无非就是作用范围不一样,其底层都是读取配置文件,然后封装到一个叫做GatewayFilter的类中,并实现对应的逻辑。那么GlobalFilter和GatewayFilter并不是一个类,但是在底层,通过一个叫做GatewayFilterAdapter的适配器,将GlobalFilter适配成一个GatewayFilter来使用,那么最终,这三类过滤器就都变成了同一个数据类型,就可以放在同一个数组中进行排序。
那么剩下的问题就是如何对过滤器进行排序。
- 每一个过滤器都必须指定一个int类型的Order值,order值越小,优先级越高,执行顺序越靠前
- GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
- 路由过滤器和DefaultFilter由SpringB指定,默认是按照生命顺序从1递增
- 当过滤器的order值一样时,会按照DefaultFilter > 当前路由过滤器 > GlobalFilter的顺序执行
结论
- order值越小,优先级越高
- 当order值一样时,顺序是DefaultFilter最先,其次是局部的路由过滤器,最后是全局过滤器。
网关的跨域问题处理
跨域:域名不一致就是跨域,主要包括:
- 域名不同
- 域名相同,端口不同
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题。其中关键词在于浏览器,和Ajax请求。之前我们访问不同端口之间的服务,首先他不是通过浏览器,其次,他也不是Ajax请求,所以不会有跨域问题。
解决方案:CORS
网关处理跨域采用的同样是CORS方案,并且只需要【简单】配置即可实现:
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许那些网站的跨域请求
allowedMethods: # 允许那些方法的跨域请求
-
"GET"
-
"POST"
-
"DELETE"
-
"PUT"
-
"OPTIONS"
allowedHeaders: "*" # 允许跨域请求携带请求头
allowCredentials: true # 是否允许携带Cookie
maxAge: 360000 # 这次跨域的检测的有效期
这些配置先放在一边,要测试跨域问题,我们就要先做出一个跨域请求,我们在前端页面中使用axis发起一个对userserver服务的调用代码:
<!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>
<pre>
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
allowedMethods: # 允许的跨域ajax的请求方式
-
"GET"
-
"POST"
-
"DELETE"
-
"PUT"
-
"OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
</pre>
</body>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
axios.get("http://localhost:10010/user/1?authorization=admin")
.then(resp => console.log(resp.data))
.catch(err => console.log(err))
</script>
</html>
然后我们以服务的方式打开这个前端页面,将网页配置成服务的方式很简单,首先我们创建一个文件夹,然后文件夹中只放我们的前端页面文件:
然后在VScode中打开这个文件夹:
打开之后,我们打开终端,使用npm下载一个插件,叫做live-server:
npm install -g live-server
他的官网如下:实时服务器 - npm (npmjs.com)
下载完成之后,我们在终端中输入以下内容:
live-server --port=8090
其中端口号就是我们在网关配置中配置的允许跨域的网址端口号即可,只要保证这两个地方的参数相同,并且端口号没有被占用即可,只要输入命令按下回车,他应该会自动跳转到浏览器中,但是这里有一个坑,他自动跳转的网址是用127.0.0.1跳转的:
但是我们在网关服务的配置中使用的是localhost配置的,在某些时候,如果你发现你的配置正确,但就是死活访问不了,试一下把你的127.0.0.1换成localhost,可能就能成功了:
这都是后话,当你遇到这个坑的时候再来看,我们继续往下看正常开服务之后的默认界面:
打开之后,首先他会报错,因为跨域问题报的错,他告诉我们说:
因为CORS策略也就是跨域解决策略的问题,而导致请求被拦截,不允许请求,然后我们将刚才的配置,粘贴到网关服务的配置文件中,把冲突的位置调整一下,效果就是这样的:
然后我们重启网关服务,重启完成之后,我们来到前端页面中进行刷新:
可以看到此时就已经能够收到服务器的响应信息了,这就说明我们的跨域问题已经解决了。
总结
CORS跨域要配置的参数包括那几个?
- 允许那些域名跨域?
- 允许那些请求头?
- 允许那些请求方式?
- 是否允许使用cookie?
- 有效期是多久?