一、SpringCloudGateway概述
1、SpringCloudGateway项目技术栈:
Spring 5.0: 这是 Spring 框架的最新版本,提供了许多新的特性和改进。
Spring Boot 2.0: 这是 Spring Boot 的最新版本,简化了 Spring 应用程序的开发和部署。
Project Reactor: 这是一个响应式编程框架,为 Spring WebFlux 提供了基础支持。
Spring WebFlux: 这是 Spring 5.0 引入的一个新的响应式 Web 框架,用于构建反应式 Web 应用程序。
Netty: 这是 Spring WebFlux 的底层实现之一,提供了高性能的非阻塞 I/O 模型。
2、SpringCloudGateway核心概念:
路由(Route):路由是网关最基础的部分,路由信息由 ID、目标 URI、一组断言和一组过滤器组成。如果断言 路由为真,则说明请求的 URI 和配置匹配。
断言(Predicate):Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring 5.0 框架中 的 ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于 Http Request 中的任何信息,比如请求头和参数等。
过滤器(Filter):一个标准的 Spring Web Filter。Spring Cloud Gateway 中的 Filter 分为两种类型,分别是 Gateway Filter 和 Global Filter。过滤器将会对请求和响应进行处理.
二、SpringCloudGateway基础
1、SpringCloudGateway工作流程:
客户端向 Spring Cloud Gateway 发出请求。如果Gateway处理程序映射确定一个请求与路由相匹配,它将被发送到Gateway Web处理程序。这个处理程序通过一个特定于该请求的过滤器链来运行该请求。过滤器被虚线分割的原因是,过滤器可以在代理请求发送之前和之后运行逻辑。所有的 “pre” (前)过滤器逻辑都被执行。然后发出代理请求。在代理请求发出后,“post” (后)过滤器逻辑被运行。
2、SpringCloudGateway 路由谓词工厂:
SpringCloudGateway提供的所有可用谓词:
Path Predicate Factory: 根据请求路径匹配路由。例如 path("/foo/{segment}") 可以匹配 /foo/1 和 /foo/bar。
Method Predicate Factory: 根据请求方法匹配路由。例如 method(HttpMethod.GET) 可以匹配 GET 请求。
Header Predicate Factory: 根据请求头匹配路由。例如 header("X-Request-Id", "foo") 可以匹配包含指定头的请求。
Host Predicate Factory: 根据请求 Host 头匹配路由。例如 host("**.somehost.org") 可以匹配 www.somehost.org 和 beta.somehost.org。
Query Predicate Factory: 根据请求参数匹配路由。例如 query("name") 可以匹配包含 name 参数的请求。
Cookie Predicate Factory: 根据请求 Cookie 匹配路由。例如 cookie("name", "value") 可以匹配包含指定 Cookie 的请求。
RemoteAddr Predicate Factory: 根据请求源 IP 地址匹配路由。例如 remoteAddr("192.168.1.1/24") 可以匹配来自 192.168.1.0/24 网段的请求。
Weight Predicate Factory: 根据权重匹配路由。例如 weight("group1", 9) 可以将 90% 的请求路由到 "group1"。
After Predicate Factory: 根据请求时间匹配路由。例如 after("2020-01-20T00:00:00.000+08:00") 可以匹配在指定时间之后的请求。
Before Predicate Factory: 根据请求时间匹配路由。例如 before("2020-01-20T00:00:00.000+08:00") 可以匹配在指定时间之前的请求。
Between Predicate Factory: 根据请求时间匹配路由。例如 between("2020-01-20T00:00:00.000+08:00", "2020-01-21T00:00:00.000+08:00") 可以匹配在指定时间范围内的请求。
Path谓词示例:
spring:
cloud:
gateway:
routes:
- id: ruoyi-auth
uri: lb://ruoyi-auth
predicates:
- Path=/auth/**
filters:
- CacheRequestFilter
- ValidateCodeFilter
- StripPrefix=1
3、创建全局 Filter
"Pre"Filter 定义前置过滤器
定义前置过滤器需要实现GlobalFilter和Ordered接口并重写filter方法和getOrder方法,filter方法中为自定义过滤器的实现逻辑,getOrder中为控制自定义过滤器Filter 在 过滤器链Filter Chain 中的位置
@Component
public class AuthFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
// 排除过滤的 uri 地址,nacos自行添加
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Autowired
private RedisService redisService;
/**
* @param exchange 表示当前的 HTTP 请求和响应
* @param chain 表示过滤器链,可以用它来调用下一个过滤器
* @return Mono<Void>类型,表示一个异步的无返回值操作
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取当前的 HTTP 请求对象
ServerHttpRequest request = exchange.getRequest();
//创建一个 ServerHttpRequest.Builder 对象,用于修改请求对象
ServerHttpRequest.Builder mutate = request.mutate();
//获取当前请求的 URL 路径
String url = request.getURI().getPath();
// 跳过不需要验证的路径
if (StringUtils.matches(url, ignoreWhite.getWhites())) {
return chain.filter(exchange);
}
String token = getToken(request);
if (StringUtils.isEmpty(token)) {
return unauthorizedResponse(exchange, "令牌不能为空");
}
Claims claims = JwtUtils.parseToken(token);
if (claims == null) {
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
String userkey = JwtUtils.getUserKey(claims);
boolean islogin = redisService.hasKey(getTokenKey(userkey));
if (!islogin) {
return unauthorizedResponse(exchange, "登录状态已过期");
}
String userid = JwtUtils.getUserId(claims);
String username = JwtUtils.getUserName(claims);
if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {
return unauthorizedResponse(exchange, "令牌验证失败");
}
// 设置用户信息到请求
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
// 内部请求来源参数清除
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
//重新构建请求对象并执行之后的过滤器
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
if (value == null) {
return;
}
String valueStr = value.toString();
String valueEncode = ServletUtils.urlEncode(valueStr);
mutate.header(name, valueEncode);
}
private void removeHeader(ServerHttpRequest.Builder mutate, String name) {
mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
}
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {
log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
}
/**
* 获取缓存key
*/
private String getTokenKey(String token) {
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
/**
* 获取请求token
*/
private String getToken(ServerHttpRequest request) {
String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
// 如果前端设置了令牌前缀,则裁剪掉前缀
if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {
token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
}
return token;
}
/**
* 设置该过滤器的优先级
* 数值越大优先级越低
* @return
*/
@Override
public int getOrder() {
return -200;
}
}
"Post"Filter 定义后置过滤器
@Configuration
public class LoggingGlobalFiltersConfigurations {
final Logger logger =LoggerFactory.getLogger(LoggingGlobalFiltersConfigurations.class);
@Bean
public GlobalFilter postGlobalFilter() {
return (exchange, chain) -> {
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
logger.info("Global Post Filter executed");
}));
};
}
}
“Pre” 和 “Post” 逻辑结合到一个过滤器中
@Component
public class FirstPreLastPostGlobalFilter
implements GlobalFilter, Ordered {
final Logger logger =
LoggerFactory.getLogger(FirstPreLastPostGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
logger.info("First Pre Global Filter");
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
logger.info("Last Post Global Filter");
}));
}
@Override
public int getOrder() {
return -1;
}
}
4、创建局部 GatewayFilter
实现GatewayFilterFactory接口创建局部过滤器
实现GatewayFilterFactory接口定义了一个静态配置类CustomAddRequestHeaderConfig。实现shortcutFieldOrder方法定义配置文件中的字段,实现apply方法实现过滤器的业务逻辑,实现getConfigClass获取配置类的class,实现newConfig创建了一个新的配置类的 Class 对象。
/**
* @package com.ruoyi.gateway.filter
* @ClassName Test
* @Description 这个类实现了 GatewayFilterFactory 接口,并指定了它所使用的配置类为 CustomAddRequestHeaderConfig
* @Author zhanggh
* @Date 2024/4/18 9:05
* @Version 1.0
*/
@Component
public class CustomAddRequestHeaderGatewayFilterFactory implements GatewayFilterFactory<CustomAddRequestHeaderGatewayFilterFactory.CustomAddRequestHeaderConfig> {
//将配置类的 Class 对象保存在一个私有的成员变量中
private final Class<CustomAddRequestHeaderConfig> configClass = CustomAddRequestHeaderConfig.class;
/**
* 这个方法返回了一个字段名称列表,这些字段可以在 YAML 配置中以简写的形式指定
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
return new ArrayList<>(Arrays.asList("headerName", "headerValue"));
}
/**
* 这个方法是过滤器工厂的核心,它根据提供的配置对象 CustomAddRequestHeaderConfig 创建一个 GatewayFilter 实例
* 在这个例子中,它会在请求头中添加一个自定义的头部
* @param config
* @return
*/
@Override
public GatewayFilter apply(CustomAddRequestHeaderConfig config) {
return ((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate().headers(httpHeaders -> {
httpHeaders.set(config.getHeaderName(), config.getHeaderValue());
}).build();
return chain.filter(exchange.mutate().request(request).build());
});
}
/**
* 这个方法返回了配置类的 Class 对象
* @return
*/
@Override
public Class<CustomAddRequestHeaderConfig> getConfigClass() {
return configClass;
}
/**
* 这个方法创建了一个新的配置对象实例
* @return
*/
@Override
public CustomAddRequestHeaderConfig newConfig() {
return BeanUtils.instantiateClass(this.configClass);
}
/**
* 这个内部类定义了过滤器的配置,包括头部名称和头部值两个属性。这些属性可以在 YAML 配置中进行设置。
*/
public static class CustomAddRequestHeaderConfig {
private String headerName;
private String headerValue;
public String getHeaderName() {
return headerName;
}
public void setHeaderName(String headerName) {
this.headerName = headerName;
}
public String getHeaderValue() {
return headerValue;
}
public void setHeaderValue(String headerValue) {
this.headerValue = headerValue;
}
}
}
在YML文件中配置定义的过滤器并定义过滤器需要的两个参数
spring:
redis:
host: localhost
port: 6379
password:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
# 认证中心
# 指定该路由规则的ID为ruoyi-auth
- id: ruoyi-auth
#指定请求转发的目标服务为ruoyi-auth,采用负载均衡的方式进行转发。lb代表从服务注册发现组件(如Eureka、Consul、Nacos等)中获取服务列表
uri: lb://ruoyi-auth
#匹配请求路径为/auth/**的请求
predicates:
- Path=/auth/**
filters:
- CustomAddRequestHeader=customHeaderName,customHeaderValue
继承AbstractGatewayFilterFactory抽象类创建局部过滤器