predicate(断言):
判断uri是否符合规则 • 最常用的的就是PathPredicate,以下列子就是只有url中有user前缀的才能被gateway识别,否则它不会进行路由转发
routes:
- id: ***
# uri: lb://starry-sky-upms
uri: http://localhost:9003/
predicates:
- Path=/user/**
filters:
- StripPrefix=1
filter(过滤器) :
上面的例子为例,StripPrefix就是说gateway进行转发的时候要去掉user前缀,所以在后端服务的api层,是不需要加user前缀的。
• 全局过滤器 • 自定义全局过滤器,即实现GlobalFilter,就可以实现全局拦截,执行自己的业务逻辑。权限鉴定就是这一步做的。
• 动态路由(要使新加入的route生效)
• 新增Listener,监听nacos配置改变(RefreshRoutesEvent),当发现route有增加的时候,就调用actuator的refresh方法,刷新到内存汇总即可。
以上4个部分都可以自定义符合自己业务需求的扩展。 在实际业务中,可能用gateway来做登陆校验,以及访问权的判断,还可以配置白名单实现哪些路径可以忽略。
1. 登陆校验:不同平台的登陆校验,校验token是否有效
2. 访问权限校验:比如有没有功能权限码之类的,系统里面会为不同的人配置不同的功能权限
3. 配置忽略路径:有些路径可能不需要上述2步校验,所以直接可以通过 可以写自定义一个GlobalFilter,重写invoke方法。
1. 先处理ignore路径,用正则表达式匹配,如果符合匹配,则直接放行。
2. 如果不是白名单,则先进行登陆校验,再进行权限校验。
动态路由的实现:
private void initListener() {
try {
nacosConfigManager.getConfigService()
.addListener(dynamicRouteDataId, nacosGroup,
new AbstractListener() {
@Override
public void receiveConfigInfo(String configInfo) {
log.info("Init NacosListener: data info{}" + configInfo);
try {
YAMLMapper mapper = new YAMLMapper();
List<RouteDefinition> list = mapper.readValue(configInfo, new TypeReference<>() {});
list.forEach(definition -> {
Result result = dynamicRouteService.update(definition);
log.info("update route,{},{}", definition.getId(), JsonUtils.toJson(result));
});
} catch (Exception e) {
log.error("update route error:{}, configInfo:{}", e.getMessage(), e, configInfo);
exceptionCounterHandler.increment();
}
}
});
} catch (Exception e) {
log.error("Gateway addListener error:{}", e.getMessage(), e);
exceptionCounterHandler.increment();
}
}
通过nacos 的NacosConfigManager获取NacosConfigService,通过addListener方法,其实是像nacos的WorkClient类里面添加一个监听器。WorkClient类是nacos管理配置的一个类,它里面启动了一个长连接定时任务(延迟队列)去不断的check配置的改变(checkConfigInfo),一旦有配置改变,就通知监听器
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
// Initialize the timeout parameter
init(properties);
....
executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
checkConfigInfo();
} catch (Throwable e) {
LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
继续跟下去,就可以看到LongPollingRunnable的run方法中,会比较MD5是否一致,不一致就会发消息
void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
safeNotifyListener(dataId, group, content, md5, wrap);
}
}
}
在面试时可能会问,gateway的接入流程:
1. spring boot启动过程不用说了,接入gateway的starter jar包,然后自动装配各种gateway的bean 2. 服务启动成功后,输入url,gateway如何拦截请求? 首先Netty 的 HTTP 服务通过HttpServerInitializer启动,最终请求会传递到 Spring WebFlux 的DispatcherHandler:
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (this.handlerMappings == null) {
return createNotFoundError();
}
return Flux.fromIterable(this.handlerMappings)
//mapping.getHandler 会调RoutePredicateHandlerMapping
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
// invokeHandler会去处理gateway的Filter
.flatMap(handler -> invokeHandler(exchange, handler))
.flatMap(result -> handleResult(exchange, result));
gateway注入的类入口是:RoutePredicateHandlerMapping,它实现了spring webflux的接口HandlerMapping,HandlerMapping 我们都很熟悉,spring mvc的常客,处理http请求的。所以spring在处理过程中就会调用这个类的方法:getHandlerInternal; FilteringWebHandler实现WebHandler,所以在上面Handler处理的时候就会找到它,调用它的handle方法
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
List<GatewayFilter> gatewayFilters = route.getFilters();
List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
combined.addAll(gatewayFilters);
// TODO: needed or cached?
AnnotationAwareOrderComparator.sort(combined);
if (logger.isDebugEnabled()) {
logger.debug("Sorted gatewayFilterFactories: " + combined);
}
return new DefaultGatewayFilterChain(combined).filter(exchange);
}
项目里面如何用的gateway?如何用它做的权限校验?
- 首先把gateway的starter引入,然后把gateway所需要个bean交给spring容器管理
- request请求访问来了后,通过netty的webflux把请求转发给HandlerMapping,它在执行handle方法的时候,会去查所有实现GlobalFilter的类,并且调用其filter方法。还会去查所有的FilterFactory,并且调filter方法
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
//这里是获取所有***FilterFactory(比如:gateway自带的***FilterFactory)
List<GatewayFilter> gatewayFilters = route.getFilters();
//这里是globalFilters
List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
combined.addAll(gatewayFilters);
// TODO: needed or cached?
AnnotationAwareOrderComparator.sort(combined);
if (logger.isDebugEnabled()) {
logger.debug("Sorted gatewayFilterFactories: " + combined);
}
return new DefaultGatewayFilterChain(combined).filter(exchange);
}
如何用它做的权限校验?
在实际业务中,可能用gateway来做登陆校验,以及访问权的判断,还可以配置白名单实现哪些路径可以忽略。
登陆校验:不同平台的登陆校验,校验token是否有效
访问权限校验:比如有没有功能权限码之类的,系统里面会为不同的人配置不同的功能权限
配置忽略路径:有些路径可能不需要上述2步校验,所以直接可以通过
可以写自定义一个GlobalFilter,重写invoke方法。
先处理ignore路径,用正则表达式匹配,如果符合匹配,则直接放行。
2. 如果不是白名单,则先进行登陆校验,再进行权限校验。
如何用gateway做限流(gateway自带一个令牌桶的限流器)
• 首先要配置route • 其次当请求命中了配置的uri路径后RequestRateLimiterGatewayFilterFactory的apply方法就会被执行
spring:
cloud:
gateway:
routes:
- id: rate_limiter_route
uri: http://backend-service:8080 # 目标服务地址
predicates:
- Path=/api/** # 匹配的请求路径
filters:
- name: RequestRateLimiter # 使用限流过滤器
args:
# 1. 指定 Key 解析器(需注册为 Spring Bean)
key-resolver: "#{@ipKeyResolver}"
# 2. Redis 限流器配置
redis-rate-limiter:
replenishRate: 10 # 每秒填充的令牌数
burstCapacity: 20 # 令牌桶容量
requestedTokens: 1 # 每次请求消耗的令牌数
如何动态路由?
自定义一个监听器,处理的event为refreshScopeEvent,nacos的配置发生改变后(会通知所有的监听器),拿到消息后,可以通过gateway提供的refresh方法去刷新r内存中的route。
自定义实现全局过滤器:
SignUtils:
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
/**
* 签名的工具类
*/
public class SignUtils {
public static List<String> splitString(String input, int chunkSize) {
List<String> chunks = new ArrayList<>();
for (int i = 0; i < input.length(); i += chunkSize) {
int end = Math.min(input.length(), i + chunkSize);
chunks.add(input.substring(i, end));
}
return chunks;
}
public static String md5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 algorithm not found", e);
}
}
public static String aesEncrypt(String input, String key) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(input.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
}
SignGlobalFilter:
import org.gateway.example.utils.SignUtils;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Component
public class SignGlobalFilter implements GlobalFilter, Ordered {
private static final String AES_KEY = "mysecretkey12345!"; // 16字节 AES 密钥
private static final List<String> WHITE_LIST = Arrays.asList(
"/usp/out-api/auth/whiteApi", // 示例白名单路径
"/public"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
// 1. 白名单路径直接放行
if (isWhiteListed(path)) {
return chain.filter(exchange);
}
// 2. 装饰响应以处理 JSON 内容
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
// 3. 仅处理 JSON 响应
if (isJsonResponse(this)) {
return Flux.from(body)
.collectList()
.flatMap(list -> {
// 4. 合并响应体内容
DataBuffer composite = bufferFactory.join(list);
byte[] content = new byte[composite.readableByteCount()];
composite.read(content);
DataBufferUtils.release(composite);
String responseBody = new String(content, StandardCharsets.UTF_8);
// 5. 生成签名逻辑
try {
// 参数3:响应体 MD5
String dataMd5 = SignUtils.md5(responseBody);
List<String> param3 = SignUtils.splitString(dataMd5, 6);
// 参数1:请求头中的签名
String param1 = request.getHeaders().getFirst("X-Sign");
if (param1 == null) {
return Mono.error(new IllegalArgumentException("Missing X-Sign header"));
}
List<String> param1List = SignUtils.splitString(param1, 10);
// 参数2:请求路径 MD5
String urlMd5 = SignUtils.md5(path);
List<String> param2 = SignUtils.splitString(urlMd5, 8);
// 合并、排序、生成最终签名
List<String> combined = new ArrayList<>();
combined.addAll(param1List);
combined.addAll(param2);
combined.addAll(param3);
combined.sort((a, b) -> b.compareTo(a)); // 降序排列
String combinedMd5 = SignUtils.md5(String.join("", combined));
String signature = SignUtils.aesEncrypt(combinedMd5, AES_KEY);
// 6. 添加签名到响应头
getHeaders().add("X-Signature", signature);
} catch (Exception e) {
return Mono.error(e);
}
// 7. 重新写入响应体
return super.writeWith(Flux.just(bufferFactory.wrap(content)));
})
.onErrorResume(e -> {
originalResponse.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return originalResponse.writeWith(
Flux.just(bufferFactory.wrap(e.getMessage().getBytes()))
);
});
} else {
// 非 JSON 响应直接传递
return super.writeWith(body);
}
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
// 判断是否为白名单路径
private boolean isWhiteListed(String path) {
return WHITE_LIST.stream().anyMatch(path::startsWith);
}
// 判断是否为 JSON 响应
private boolean isJsonResponse(ServerHttpResponse response) {
MediaType contentType = response.getHeaders().getContentType();
return contentType != null && contentType.includes(MediaType.APPLICATION_JSON);
}
@Override
public int getOrder() {
return -1; // 高优先级
}
}
maven配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud</groupId>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.0</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--添加配置跳过测试-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>