五、SpringCloud alibaba 之 网关GateWay
详细的网关学习可以参考:
https://www.jianshu.com/p/8749dfe9832e
https://www.zhihu.com/column/c_1357481230536216576
https://www.cnblogs.com/bjlhx/category/1273947.html
6.1、网关介绍
什么是网关
网络的关口。数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。
更通俗的来讲,网关就像是以前园区传达室的大爷。
-
外面的人要想进入园区,必须经过大爷的认可,如果你是不怀好意的人,肯定被直接拦截。
-
外面的人要传话或送信,要找大爷。大爷帮你带给目标人。
现在,微服务网关就起到同样的作用。前端请求不能直接访问微服务,而是要请求网关:
-
网关可以做安全控制,也就是登录身份校验,校验通过才放行
-
通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去
在SpringCloud当中,提供了两种网关实现方案:
-
Netflix Zuul:早期实现,目前已经淘汰
-
Spring Cloud Gateway 是由 WebFlux + Netty + Reactor 实现的响应式的 API 网关。
它不能在传统的 servlet 容器中工作,也不能构建成 war包
。Spring Cloud Gateway 旨在为微服务架构提供一种简单且有效的 API 路由的管理方式,并基于Filter
的方式提供网关的基本功能,例如说安全认证、监控、限流等等。
课堂中我们以SpringCloudGateway为例来讲解,官方网站:https://spring.io/projects/spring-cloud-gateway/#learn
6.2、快速入门
我们先看下如何利用网关实现请求路由。由于网关本身也是一个独立的微服务,因此也需要创建一个模块开发功能。大概步骤如下:
-
创建网关微服务
-
引入SpringCloudGateway、NacosDiscovery依赖
-
编写启动类
-
配置网关路由
创建一个module
首先,我们要在spring-cloud下创建一个新的module,命名为spring-cloud-gateway,作为网关微服务:
引入依赖
1、网关服务也是一个微服务,因此需要引入spring-cloud依赖
2、网关服务需要把前端的请求转发给别的微服务,因此需要引入网关依赖
3、网关服务要做请求转发就需要知道其他微服务的地址。网关服务获取其他服务地址的方式也是通过注册中心获取,因此需要引入nacos相关依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
discovery:
username: nacos
server-addr: localhost:8848
password: 123456
namespace: 495e2ede-c37a-4258-8e11-72f21b0cf1cd
group: java-coder
gateway:
routes:
- id: order-service # 路由规则id,自定义,唯一
uri: lb://order-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/order/** # 这里是以请求路径作为判断规则
- id: stock-service
uri: lb://stock-service
predicates:
- Path=/stock/**
feign:
okhttp:
enabled: true # 开启OKHttp功能
package com.java.coder.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication springApplication=new SpringApplication();
springApplication.run(GatewayApplication.class,args);
}
}
spring-boot-starter-web
依赖,正如前面说的,Spring Cloud Gateway 是由 WebFlux + Netty + Reactor 实现的响应式的 API 网关。它不能在传统的 servlet 容器中工作,也不能构建成 war包
通过8081接口直接访问order-service
:http://localhost:8081/order/feignTest
运行结果如下:
order-service
:
整体的调用过程如下:
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-starter
6.3.1、路由
路由(route)
:路由是网关中最基础的部分,路由信息包括一个ID、一个目的URI、一组断言工厂、一组Filter组成。如果断言为真,则说明请求的URL和配置的路由匹配。
路由示例
spring:
cloud:
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: order-service # 路由规则id,自定义,唯一
uri: lb://order-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表,uri也可以直接写地址
order: 1 #路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件) ,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/order/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters: #过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
‐StripPrefix=1 #转发之前去掉1层路径
gateway的配置文件
spring.cloud.gateway对应的配置类是
@ConfigurationProperties(GatewayProperties.PREFIX)
@Validated
public class GatewayProperties {
/**
* 属性前缀
*/
public static final String PREFIX = "spring.cloud.gateway";
/**
* 路由列表
*/
@NotNull
@Valid
private List<RouteDefinition> routes = new ArrayList<>();
/**
* 被应用到所有路由的过滤器定义列表,这些过滤器会作用到所有的路由
*/
private List<FilterDefinition> defaultFilters = new ArrayList<>();
private List<MediaType> streamingMediaTypes = Arrays
.asList(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_STREAM_JSON);
private boolean failOnRouteDefinitionError = true;
private Metrics metrics = new Metrics();
}
routes
其实就是一个RouteDefinition
的列表。
RouteDefinition
RouteDefinition
的定义如下:
public class RouteDefinition {
// 路由id
private String id;
// 路由断言
@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList<>();
// 路由过滤器的定义
@Valid
private List<FilterDefinition> filters = new ArrayList<>();
// 要路由的地址
@NotNull
private URI uri;
//
private Map<String, Object> metadata = new HashMap<>();
// 优先级
private int order = 0;
}
四个属性含义如下:
-
id
:路由的唯一标示 -
predicates
:路由断言,其实就是匹配条件 -
filters
:路由过滤条件,后面讲 -
uri
:路由目标地址,lb://
代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。uri也可以直接配置成访问路径,比如http://localhost:8082
spring:
cloud:
gateway:
routes:
- id: route-service
uri: http://localhost:8082
predicates:
- Path=/stock/**
6.3.2、断言
断言(predicates)
:Java8中的断言函数,SpringCloud Gateway中的断言函数类型是Spring5.0框架中的ServerWebExchange。断言函数允许开发者去定义匹配Http request中的任何信息,比如请求头和参数等。
PredicateDefinition
断言对应的类就是PredicateDefinition
@Validated
public class PredicateDefinition {
@NotNull
private String name;
private Map<String, String> args = new LinkedHashMap<>();
}
SpringCloudGateway中支持的断言类型有很多,这些断言的实现都是由spring cloud gateway的断言工厂来实现的:
说明 | 示例 | |
---|---|---|
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 | 权重处理 |
此类型的断言根据时间做判断,主要有三个:
AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
时间的格式可以通过ZonedDateTime.now()来获取
基于远程地址的断言工厂
RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中
基于Cookie的断言工厂
CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求
cookie是否具有给定名称且值与正则表达式匹配。
基于Header的断言工厂
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否具有给定名称且值与正则表达式匹配。
基于Host的断言工厂
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
基于Method请求方法的断言工厂
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
基于Path请求路径的断言工厂
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
基于Query请求参数的断言工厂
QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。
自定义路由断言工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法的逻辑。在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。
1、 必须spring组件 bean
2、 类必须加上RoutePredicateFactory作为结尾
3、 必须继承AbstractRoutePredicateFactory
4、 必须声明静态内部类 声明属性来接收 配置文件中对应的断言的信息
5、 需要结合shortcutFieldOrder进行绑定
6、 通过apply进行逻辑判断 true就是匹配成功 false匹配失败
定义一个断言工厂
@Slf4j
@Component
public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> {
public CheckAuthRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
log.info("调用CheckAuthRoutePredicateFactory" +config.getName());
if(config.getName().equals("java-coder")){
return true;
}
return false;
}
};
}
/**
* Returns hints about the number of args and the order for shortcut parsing.
* 返回有关参数数量和快捷方式分析顺序的提示。
*
* @return the list of hints
*/
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("name");
}
/**
* 用于接收配置信息
*/
public static class Config{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
在配置文件中使用自定义的过滤器工厂
spring:
cloud:
gateway:
routes:
- id: route-service
uri: http://localhost:8082
predicates:
- Path=/stock/**
- CheckAuth=xushu #此时就在配置文件中使用了自定义的断言工厂
6.3.4、过滤器
过滤器(Filter)
:SpringCloud Gateway中的filter分为网关过滤器 Gateway FilIer和全局过滤器Global Filter。内置Filter都实现GatewayFilter接口。
GateFilter
:默认是不生效的,要配置到特定的路由下才会针对特定的路由生效。如果想要GateFilter对所有的路由都生效,需要把GateFilter配置在default-filters
下面。
GlobalFilter
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
内置过滤器
SpringCloud Gateway中内置了很多的过滤器工厂,我们通过一些过滤器工厂可以进行一些业务逻辑处理器,比如添加剔除响应头,添加去除参数等
说明 | |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
AddRequestParameter | 为原始请求添加请求参数 |
AddResponseHeader | 给响应结果中添加一个响应头 |
DedupeResponseHeader | 去掉重复请求头 |
Spring Cloud CircuitBreaker | 断路器 |
FallbackHeaders | 添加熔断后的异常信息到请求头 |
MapRequestHeader | 将上游请求头的值赋值到下游请求头 |
PrefixPath | 匹配的路由添加前缀 |
PreserveHostHeader | 保留原请求头 |
RequestRateLimiter | 限制请求的流量 |
RedirectTo | 重定向 |
RemoveRequestHeader | 移除请求中的一个请求头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RemoveRequestParameter | 移除请求参数 |
RewritePath | 重写路径 |
RewriteLocationResponseHeader | 重写响应头中Location的值 |
RewriteResponseHeader | 重写响应头 |
SaveSession | 向下游转发请求前前置执行WebSession::save的操作 |
SecureHeaders | 禁用默认值 |
SetPath | 设置路径 |
SetRequestHeader | 重置请求头 |
SetResponseHeader | 修改响应头 |
SetStatus | 修改响应的状态码 |
StripPrefix | 对指定数量的路径前缀进行去除 |
Retry | 重试 |
RequestSize | 请求大小大于限制时,限制请求到达下游服务 |
SetRequestHostHeader | 重置请求头值 |
Modify a Request Body | 修改请求体内容 |
Modify a Response Body | 修改响应体内容 |
Relay | 将 OAuth2 访问令牌向下游转发到它所代理的服务 |
CacheRequestBody |
自定义局部过滤器
自定义GatewayFilter
不是直接实现GatewayFilter
,而是实现AbstractGatewayFilterFactory
。
1、继承AbstractGatewayFilterFactory
2、自定义名称必须要以GatewayFilterFactory结尾并交给spring管理。
@Component
@Slf4j
public class CheckAuthGatewayFilterFactory extends AbstractGatewayFilterFactory<CheckAuthGatewayFilterFactory.Config> {
public static final String KEY="key";
public static final String VALUE="value";
public CheckAuthGatewayFilterFactory() {
super(CheckAuthGatewayFilterFactory.Config.class);
}
/**
* 配置用于接收配置文件中值的变量
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(KEY, VALUE);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getRawPath();
log.info("请求路径:{}",path);
log.info("{}={}",config.getKey(),config.getValue());
return chain.filter(exchange.mutate().request(request).build());
}
};
}
/**
* 自定义配置属性,成员变量名称很重要,下面会用到
*/
@Data
public static class Config {
private String key;
private String value;
}
}
在配置文件中可以配置
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: stock-service
filters:
- CheckAuth=age,15
uri: lb://stock-service
predicates:
- Path=/stock/**
自定义全局全局过滤器
自定义全局过滤器,直接实现GlobalFilter
接口就行,而且也无法设置动态参数。Ordered
是spring的核心接口,用于来对bean进行排序。
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 编写过滤器逻辑
System.out.println("未登录,无法访问");
// 放行
// return chain.filter(exchange);
// 拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}
常用的一些全局过滤器如下
-
Gateway Client向Gateway Server发送请求
-
请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
-
然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping
-
RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
-
如果过断言成功,由FilteringWebHandler创建过滤器链并调用
-
如图所示:
-
客户端请求进入网关后由
HandlerMapping
对请求做判断,找到与当前请求匹配的路由规则(Route
),然后将请求交给WebHandler
去处理。 -
WebHandler
则会加载当前路由下需要执行的过滤器链(Filter chain
),然后按照顺序逐一执行过滤器(后面称为Filter
)。 -
图中
Filter
被虚线分为左右两部分,是因为Filter
内部的逻辑分为pre
和post
两部分,分别会在请求路由到微服务之前和之后被执行。 -
只有所有
Filter
的pre
逻辑都依次顺序执行通过后,请求才会被路由到微服务。 -
微服务返回结果后,再倒序执行
Filter
的post
逻辑。 -
最终把响应结果返回。
如图中所示,最终请求转发是有一个名为NettyRoutingFilter
的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter
之前,这就符合我们的需求了!
登录校验的思路:
1、登录请求先到网关,网关把登录请求转发到user-service,user-service完成登录操作并生成token返回给前端
2、用户请求到网关,网关对用户请求进行校验,判断用户是否登录。
(1)如果没有登录则报错
(2)如果登录了,则对token进行解析,然后把解析出来的用户信息添加到请求头中,然后把路由转发到业务微服务中。
3、微服务之间的调用是不走网关,此时也会有校验,需要把请求的token也加入到服务之间调用的请求头里面
第一步用户登录
此代码写在spring-cloud-user服务里面
/**
* 模拟用户登录,登录账号为zhangsan,密码为123456
* @param name
* @param password
* @return
* @throws Exception
*/
@RequestMapping("/login")
public String login(String name,String password) throws Exception {
System.out.println("用户名:"+name+",password:"+password);
if("zhangsan".equals(name)&&"123456".equals(password)){
String key=RedisPrefixConstants.USER_TOKEN_PREFIX+":"+name;
String token = EncryptionUtils.encrypt(name, EncryptionUtils.SECRET_KEY);
// 登录成功,用密钥把登录的账户加密,生成的密串返回给前端(类似jwt), 并把用户信息存入到redis中
redisTemplate.opsForValue().set(key,name,30, TimeUnit.MINUTES);
return token;
}
throw new RuntimeException("登录失败");
}
第二步gateway拦截器
此代码写在spring-cloud-gateway里面
在网关定义一个全局拦截器,用来校验用户是否登录
package com.java.coder.gateway;
import com.java.coder.common.config.AuthProperties;
import com.java.coder.common.constants.RedisPrefixConstants;
import com.java.coder.common.utils.EncryptionUtils;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@Slf4j
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private RedisTemplate<String,Object> redisTemplate;
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 2.判断是否不需要拦截
if(isExclude(request.getPath().toString())){
// 无需拦截,直接放行
return chain.filter(exchange);
}
List<String> list = request.getHeaders().get("token");
// 既不是白名单路径,又没有token,则禁止访问
if(list==null||list.size()==0){
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
// 对token进行解密,如果解密不成功,则禁止访问
String token = list.get(0);
String name = null;
try{
name=EncryptionUtils.decrypt(token, EncryptionUtils.SECRET_KEY);
}catch (Exception e){
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
// 把解密出来的账号信息放入请求头中
String finalName = name;
exchange.mutate().request(b->b.header("user-info", finalName)).build();
return chain.filter(exchange);
}
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
private boolean isExclude(String antPath) {
for (String pathPattern : authProperties.getExcludePaths()) {
if(antPathMatcher.match(pathPattern, antPath)){
return true;
}
}
return false;
}
}
第三步获取当前登录用户的工具类
此代码写在spring-cloud-common
@Component
public class UserUtil {
private static RedisTemplate<String,String> redisTemplate;
@Autowired
public UserUtil(RedisTemplate<String,String> redisTemplate){
UserUtil.redisTemplate=redisTemplate;
}
public static String getCurrentUser(){
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
String name = request.getHeader("user-info");
String key= RedisPrefixConstants.USER_TOKEN_PREFIX+":"+name;
return redisTemplate.opsForValue().get(key);
}
}
因此类在spring-cloud-common里面,spring-cloud-user、spring-cloud-stock等服务无法扫描到。此时有两种解决办法。
第一种方式: 利用spi机制
在spring.factories里面配置
第二种方式: 利用SpringBootApplication注解里面的scanBasePackages来指定UserUtil所在的包的路径
因为我们在spring-cloud-common中定义了一个配置Redis的配置类,有可能出现注入RedisTemplate冲突的情况。
此时的解决方案是加一个@AutoConfigureBefore注解
因此spring-boot-starter-data-redis中的自动配置类是RedisAutoConfiguration,这个类在注入RedisTemplate时用了一个@ConditionalOnMissingBean注解,只有当spring容器中不存在名称为redisTemplate的Bean时才会注入。因此当我们的Redis配置先于RedisAutoConfiguration配置类执行,那么RedisAutoConfiguration配置类就不再会注入RedisTemplate。
原文链接:https://blog.csdn.net/m0_51681531/article/details/130507029
https://blog.csdn.net/m0_51681531/article/details/130507029
第四步验证
我们调用stock-service服务,如果不带上token,且/stock/getStock不是白名单里面的路径,则会返回403状态码
如果带上之前登录接口返回的token,则可以顺利调用