通过前面的学习我们知道,通过 Feign 就可以向指定的微服务发起 http 请求,完成远程调用。但是这里有一个问题,我的微服务好像谁来都可以访问,这样会不会不安全?那是肯定的。
实际生产中,有很多的业务模块只有我们内部的人才能用,不是谁都能访问,这时候我们就需要对用户的身份进行验证,而这个验证的工作就是网关(Gateway)来完成的。
一切请求一定要先到网关,再到微服务,其实是对微服务的一种保护措施。
(1)网关功能:
① 身份认证和权限校验(认证通过后,再将请求转发到对应的微服务)
② 服务路由(根据请求信息判断应该将请求转发到哪个微服务)
③ 负载均衡(对外的服务也会有多个实例,需要做负载均衡)
④ 请求限流(限制微服务的用户请求量)
注意 Ribbon 是对内部的 userserver 做负载均衡,而网关是对外部的 orderserver 做负载均衡!
(2)网关的技术实现:
① gataway(新技术,响应式编程,非阻塞式,性能好,吞吐能力强)
② zuul(较早的技术,基于 Servlet,阻塞式,性能差)
① 创建新的 module,引入 SpringCloudGateway 的依赖和 Nacos 的服务发现依赖
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
dependencies>
② 创建启动类
③ 编写路由配置及 Nacos 地址,路由配置由三部分组成:路由 id、目标地址和路由断言(路由断言其实就是匹配规则)
路由断言:判断请求是否符合要求,符合则转发到路由目的地。
server:
port: 10010 #网关端口
spring:
application:
name: gateway #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
gateway:
routes: #网关路由配置
- id: user-server #路由id,自定义,只要唯一即可
uri: lb://userserver #路由的目标地址,lb就是负载均衡,后面跟服务名
predicates: #路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** #只要以/user/开头就符合要求
- id: order-server
uri: lb://orderserver
predicates:
- Path=/order/**
路由不止一个,用 - 来表示路由数组!
④ 依赖冲突报错
如果你的 spring-boot-starter-web 依赖是写到父工程里的,那么不出意外的话启动 GatewayApplication 时将会报错:
翻译过来就是 SpringMVC 的依赖冲突了,SpringCloudGateway 的内部是通过 netty+webflux 实现的,而 webflux 实现和 SpringMVC 的配置依赖是有冲突的。
解决办法有两种:
第一种就是去除父工程中的 spring-boot-starter-web 这个依赖, 然后在各个子工程需要的地方进行单独引入依赖即可;
第二种就是在 pom.xml 中进行修改,用以下依赖替换原来的 spring cloud gateway。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webfluxartifactId>
exclusion>
exclusions>
dependency>
⑤ 启动项目测试一下,访问 http://localhost:10010/order/2
整体流程:
用户发起 http://localhost:10010/user/2 请求,接着该请求进入网关,而网关是无法处理具体业务的,所以它会基于路由规则去做判断,然后代理到具体的服务上去处理业务。
上面步骤中我们定义了两个路由规则,以 user 开头的路径将会被代理到 userserver 服务,接着网关会拿着服务名(userserver)去找 Nacos 拉取服务列表,然后做一个负载均衡,发送请求 http://localhost:10010/user/2。
网关只是负责检验并转发请求,而最终处理业务的是具体的服务!
我们在配置文件中写的断言规则只是字符串,这些字符串会被断言工厂读取并处理,最终转变为路由判断的条件。
Spring 提供了 11 种基本的 Predicate 工厂:
Path 是我们用的最多的方式!
测试一下,时间设在 2031 年之后才可以访问,所以现在肯定是不能访问的:
只有当断言规则都满足的时候才能访问成功,路径符合,但时间不符合,照样不能访问。
GatewayFilter 是网关中提供的一种过滤器,可以处理进入网关的请求,以及微服务返回的响应。
路由之后并不是立即向微服务发起请求的,其实我们还可以给路由配置各种各样的过滤器,这些过滤器最终会形成一个过滤器链,对进入网关的请求做各种处理。
请求给了微服务,微服务处理完之后会返回一个结果,这个结果也是先到达网关,网关同样会通过过滤器逐层处理这个响应结果,最终返回给用户。
断言匹配成功相当于请求通过了网关的大门,然后过滤器会对请求做一些处理(比如添加请求头、请求参数等),最后网关会将处理好的请求代理到具体的服务,做业务处理。
Spring 提供了 31 种不同的路由过滤器工厂:
做一个测试,我们可以在 Controller 中获取一下请求头,并在控制台输出:
如果要对所有的路由都生效,则可以将过滤器工厂写到 default-filters 中,与 routes 处于同一级:
全局过滤器即自定义过滤器,全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与 GatewayFilter 的作用一样。区别在于 GatewayFilter 通过配置定义,处理逻辑是固定的,而 GlobalFilter 的逻辑需要自己写代码实现, 定义方式是实现 GlobalFilter 接口。
做一个案例,定义全局过滤器拦截请求,判断请求的参数是否满足下面两个条件:
① 参数中是否有 authorization;
② authorization 的参数值是否为 admin。
如果同时满足则放行,否则拦截。
① 创建 AuthorizeFilter 类,实现 GlobalFilter 接口,并重写 filter 方法
第一个参数用于处理业务逻辑,第二个参数是用来放行的,把请求委托给下一个过滤器去处理,其实就是调用下一个过滤器的 filter 方法,等同于放行。
② 编写核心代码
过滤器之间是有先后执行顺序的,这个我们可以通过注解的方式设置,也可以通过实现 Ordered 接口的方式来设置,数值越小,优先级越高!
package com.zxe.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;
//定义过滤器执行的顺序,数值越小优先级越高
//可以通过注解设置,也可以通过实现Ordered接口来设置
//@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
// exchange请求上下文,里面可以获取Request、Response等信息
// chain用来把请求委托给下一个过滤器
// return表示当前过滤器业务结束
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
//2.获取参数中的authorization参数
String auth = params.getFirst("authorization");
//3.判断参数是否等于admin
if ("admin".equals(auth)) {
//4.等于admin放行,其实是调用下一个过滤器的filter方法,等同于放行
return chain.filter(exchange);
}
//5.不等于就拦截,后面的业务不会再继续,我们一般会设置状态码返回给用户,提示未登录
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
//定义过滤器执行的顺序,数值越小优先级越高
@Override
public int getOrder() {
return -1;
}
}
③ 重启项目,我们可以在请求路径中携带 authorization 参数测试一下,无 authorization 参数或参数值不正确的一律被拦截,只有当 authorization = admin 时才会被放行
整体流程:
① 用户请求进入网关,断言匹配失败,报 404 错误,匹配成功直接将请求送入过滤器链;
② 过滤器分为普通过滤器和自定义过滤器,普通过滤器可以在配置文件中设置,自定义过滤器需要实现 GlobalFilter 接口,它可以处理更复杂的过滤业务;
③ 过滤不通过的请求就会被拦截,报401错误,只有层层过滤都通过了,请求才会被路由到具体的服务上,处理具体的业务。
请求进入网关会碰到三类过滤器:当前路由过滤器、默认过滤器和全局过滤器。
查看当前路由过滤器和默认过滤器的源码知道,它们的过滤器工厂首先会读取配置文件,然后生成一个真正的过滤器 GatewayFilter。再看全局过滤器的源码,可以看到 GlobalFilter 其实是被 GatewayFilterAdapter 适配到 GatewayFilter 的,也就是说网关中所有的过滤器都是 GatewayFilter 类型的。既然是同一种类型,那么就可以把它们放到同一个集合里面去排序。
所以请求路由后,会将当前路由过滤器和默认过滤器、全局过滤器合并到一个过滤器链(集合)中,排序后依次执行每个过滤器。
每个过滤器都必须指定一个 int 类型的 order 值,order 值越小,优先级越高,执行顺序就越靠前。
GlobalFilter 通过实现 Ordered 接口,或者添加 @Order 注解来指定 order 值,过滤器的执行顺序由我们自己指定。而当前路由过滤器和默认过滤器的执行顺序是由 Spring 指定的,默认按照声明顺序从 1 开始递增,这里需要注意的是当前路由过滤器和默认过滤器是分开计数的,这就会出现计数冲突的问题(比如都是 1)。
源码里面的默认过滤器是先加载的,然后再加载当前路由过滤器,最后加载全局过滤器,组织过滤器链。所以,当过滤器的 order 值一样时,会按照 默认过滤器 > 当前路由过滤器 > 全局过滤器的顺序执行。
域名不一致就是跨域,主要包括:
① 域名不同:www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com
② 域名相同,端口不同。localhost:8080 和 localhost:8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域 ajax 请求,请求被浏览器拦截。
网关处理跨域采用的是 CORS 方案,CORS的底层逻辑网关其实已经帮我们做好了,我们只需要简单的配置即可实现。
spring:
cloud:
gateway:
globalcors: #全局的跨域处理
add-to-simple-url-handler-mapping: true #解决options请求被拦截的问题
cors-configurations:
'[/**]':
allowedOrigins: #允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: #允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" #允许在请求中携带的头信息
allowCredentials: true #是否允许携带cookie
maxAge: 360000 #这次跨域检测的有效期