目录
前面我们已经学习了注册中心Eureka、Nacos和配置管理中心Nacos;但是此时存在很多安全的问题,服务器摆在那里谁都可以进行访问!
网关功能:
①身份认证和权限校验:微服务直接摆在那里允许任何人都可以访问,不太安全;需要进行身份验证,一切请求先到网关Gateway再到微服务,验证过后在进行放行!
②服务路由、负载均衡:放行过后,问题又来了,当用户放松请求处理业务时,网关肯定处理不了业务,需要把请求给对应的微服务;但是需要判断是发给order-service还是user-service进行处理?每一个微服务后面肯定有很多实例,所以还需要进行服务路由和负载均衡!
③请求限流:允许用户的请求量,限量;是对微服务的一种保护机制!
网关的技术实现:
在SpringCloud中网关的实现包括两种:
①Gateway:SpringCloudGateway是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
②Zuul:Zuul是基于Servlet的实现,属于阻塞式编程。
总结网关的作用:
①对用户请求做身份认证、权限校验;?
②将用户请求路由到微服务,并实现负载均衡 ;
③对用户请求做限流;
搭建网关服务的步骤:
第一步:创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖
注:这里需要Nacos依赖是因为也要把网关Gateway也注入注册中心Nacos里!
<!--网关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖,把自己注入Nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
第二步:服务的启动需要启动类
package cn.itcast.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class,args);
}
}
第三步:application.yml中编写路由规则配置及nacos地址
server:
port: 10010 # 服务端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
gateway: # 服务路由配置
routes: # 表示规则
- id: userservice # 路由标识
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://user-service # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates:
- Path=/user/** # 路径断言,判断是否以/user开头
- id: orderservice
uri: lb://order-service
predicates:
- Path=/order/**
启动服务,此时访问就不需要:http://localhost:8080/order/101?而是http://localhost:10010/order/101?这种形式
成功把把请求从网关路由到微服务!
原理剖析
①首先发起请求,端口是10010,而网关端口号也是10010,一定会进入网关!网关无法处理业务,只能基于路由规则进行判断(前面定义了两个路由规则)。
②根据路由规则匹配到的是user-service,然后就可以找到nacos注册中心进行服务拉取,再去负载均衡挑一个。
网关搭建步骤:
1. 创建项目,引入nacos服务发现和gateway依赖;
2. 配置application.yml,包括服务基本信息、nacos地址、路由;
路由配置包括:
1. 路由id:路由的唯一标示;
2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡;
3. 路由断言(predicates):判断路由的规则;
4. 路由过滤器(filters):对请求或响应做处理;(后面会讲)
网关路由可以配置的内容包括:
①路由id:路由唯一标示;
②uri:路由目的地,支持lb和http两种;
③predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地;
④filters:路由过滤器,处理请求或响应;
路由断言工厂Route Predicate Factory
注:我们在配置文件中写的断言规则只是字符串,这些字符串会被路由断言工厂Predicate Factory读取并处理解析,转变为路由判断的条件。例如:Path=/user/**是按照路径匹配,这个规则是org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的像这样的断言工厂在SpringCloudGateway还有十几个!
Spring提供了11种基本的Predicate工厂:
名称 | 说明 | 示例 |
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 Cloud Gateway
注:如果此时路由规则不符合,浏览器页面包404错误!
增加时间路由规则:给order-service增加在2023后访问才符合规则
predicates:
- Path=/order/**
- After=2031-01-20T17:42:47.789-07:00[America/Denver] # 表明在2023年后访问符合
执行结果:?
1. PredicateFactory的作用是什么?
读取用户定义的断言条件,对请求进行解析并做出判断。
2. Path=/user/**是什么含义?
对请求对路进行解析,路径是以/user开头的就认为是符合的。
过滤器工厂 GatewayFilter
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理!
Spring提供了31种不同的路由过滤器工厂。例如:
更详细的可以参考官方网站:Spring Cloud Gateway
名称 | 说明 |
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
... |
案例:给所有进入user-service的请求添加一个请求头Truth=Itcast is freaking awesome!
注:key和value之间是以?逗号 的方式连接!
验证执行结果:在UserController中使用@RequestHeader注解拿到请求头信息
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth",required = false) String truth
) {
// 进行打印
System.out.println("Truth: "+truth);
return userService.queryById(id);
}
思考:此时只是给某个微服务增加请求头信息,那么如果是所有的微服务都添加呢?
注:使用默认过滤器default-filter。配置的某一个微服务的过滤器,其filter在route的下面一级;而全局过滤器default-filter是与route同级!
全局过滤器 GlobalFilter
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样!区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现。
定义方式是实现GlobalFilter接口
exchange参数:?请求上下文,里面可以获取Request、Response等信息;
chain参数:过滤器链,用来把请求委托给下一个过滤器,放行;
Mono<Void>: 返回标示当前过滤器业务结束;
package org.springframework.cloud.gateway.filter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public interface GlobalFilter {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件
参数中是否有authorization,authorization参数值是否为admin,如果同时满足则放行,否则拦截!
在gateway启动类的同包下定义一个过滤器
①首先通过exchange参数获取到request对象,调用request对象的getQueryParams方法获取到所有的请求参数。然后从请求参数中通过authorization这个key获取value值admin。如果这个值存在:就调用chain执行链的filter方法,把exchange传下去;如果这个值不存在:就通过exchange参数获取到response对象,通过这个对象的setComplete方法进行拦截。在拦截之前还可以通过通过response方法设置状态码,增加用户的体验感!
②增加@Component注解组件扫描注解,纳入Spring的管理。
③增加@Order注解,顺序组件;将来可能会定义很对组件,这里是为了先执行。还可以实现Ordered接口,重写getOrder方法进行设置。
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;
// @Order(-1)
@Component
public class AuthrizeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 第一步:获取所有参数
// 获取request对象
ServerHttpRequest request = exchange.getRequest();
// 获取所有请求参数
MultiValueMap<String, String> params = request.getQueryParams();
// 第二步:根据authorization参数获取value值
String auth = params.getFirst("authorization");
// 第三步:判断请求参数的值是不是等于admin
if ("admin".equals(auth)){
// 是,放行
return chain.filter(exchange);
}
// 不是,拦截
// 结束之前设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 实现Ordered接口的方法也可以
@Override
public int getOrder() {
return -1;
}
}
进行访问:如果不增加参数就会报401错误(未登录错误)
1.?全局过滤器的作用是什么?
对所有路由都生效的过滤器(这点和默认过滤器default-filter效果相同),并且可以自定义处理逻辑,比较灵活;
2. 实现全局过滤器的步骤?
①实现GlobalFilter接口;
②添加@Order注解或实现Ordered接口 和 添加组件扫描@Component注解;
③编写处理逻辑;
过滤器执行顺序
前面已经讲解了三个过滤器:路由过滤球、默认的过滤器、全局过滤器;接下来就分析一下这三个过滤器的执行顺序!
注:请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
思考1:从目前来看,这三个过滤器不是同一种类型,怎么能放到同一个集合当中呢?
1. 对于路由过滤器和默认过滤器default-filter从配置文件来看配置方式相同:
其本质上实际都是AddRequestHeaderGatewayFilterFactory对象!这个过滤器的工厂就会读取配置文件生成一个真正的过滤器GatewayFilter;所以路由过滤器默认过滤器都是同一类:GatewayFilter!
2. 在FilteringWebHandler类里面有一个FilteringWebHandler(过滤器适配器)这个类适配器实现了GatewayFilter接口,在适配器内部又接收了一个全局过滤器参数GlobalFiter;通过适配器模式进行传参当做GatewayFilter来使用,这样就建立了联系!
所以可以认为这三种过滤器都是GatewayFilter类型,同一种类型就可以放到List集合当中进行排序!
思考2:这样新的问题就引出来了,怎么进行排序呢?
①我们已经知道,每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
②对于全局过滤器GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定。
③对于路由过滤器和默认过滤器我们并没有去指定顺序!路由过滤器和默认过滤器defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。例如:
各排各的:?
④如果此时过滤器的order值都是1怎么办呢?
当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。
详情可以参考源码:很清晰,可以自己看一下
①org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。②org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链。
跨域问题处理
在微服务当中,所有的请求都要先经过网关,在到微服务;这样就不要在每个微服务进行处理,只需要在网关中进行处理!
跨域:域名不一致就是跨域,主要包括:
域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com
域名相同,端口不同:localhost:8080和localhost8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题!
解决方案:CORS(浏览器去询问服务器的方式)
通过axios发送get请求,请求地址就是网关的地址
<!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: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
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>
以8090端口进行运行,此时在控制台就可以看到报错请求:
进行配置
server:
port: 10010 # 服务端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
gateway: # 服务路由配置
routes:
- id: userservice # 路由标识
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://user-service # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates:
- Path=/user/** # 路径断言,判断是否以/user开头
# filters:
# - AddRequestHeader=Truth,Itcast is freaking awesome! # 注意key和value之间是以逗号隔开
- id: orderservice
uri: lb://order-service
predicates:
- Path=/order/**
- Before=2031-01-20T17:42:47.789-07:00[America/Denver]
default-filters:
- AddRequestHeader=Truth,Itcast is freaking awesome! # 注意key和value之间是以逗号隔开
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题(防止CORS浏览器询问服务器拦截) corsConfigurations:
corsConfigurations:
'[/**]': # 拦截所有的请求
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期,有效期内直接放行
?重启网关,此时再次以8090端口发送请求就可以跨域访问啦!