SpringCloud Gateway 学习笔记
作者:王珂
文章目录
前言
本文主要记录总结在使用SpringCloud组件Gateway时需要注意的点,方便在日后使用查询参考。
SpringBoot版本:2.7.15
SpringCloud版本:2021.0.5
SpringCloudAlibaba版本:2021.0.5.0
一、Gateway的基础组成
1.1 路由定义Route
一个Route模块由以下几个模块组成
- id
一个route定义一个为一个的id
- uri
route的目标地址
- predicate
使用predicate匹配http请求的任何内容
-
predicate
-
filter
示例:
spring:
cloud:
gateway:
routes: // 所有的route都定义在routes下
- id: item // route的id,唯一标识
uri: lb://item-service // 路由目标服务,lb代表负载均衡
predicates: // 所有predicate都定义在predicates下
- Path=/items/** 以请求路径做为判断
- id: xxx
uri: lb://xxx
predicates:
xxx
网关路由对应的Java类型是RouteDefinition,其常见的属性有:
-
id 路由唯一标识
-
uri 路由目标地址
-
predicates 路由断言,判断请求是否符合当前路由
-
filters 路由过滤器,对请求或响应做特殊处理
1.2 路由断言Predicate
Spring提供了12种基本的RoutePredicateFactory实现:
名称 | 说明 | 示例 |
---|---|---|
After | 某个时间点之后的请求 | - After=2017-01-20T17:42:47.789-07:00[America/Denver] |
Before | 某个时间点之前的请求 | - Before=2017-01-20T17:42:47.789-07:00[America/Denver] |
Between | 某两个时间点之间的请求 | - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] |
Cookies | 请求必须包含某些Cookies | - 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/{segment} |
Query | 请求参数必须包含指定的参数 | - Query=green |
RemoteAddr | 请求的IP必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
Weight | 权重处理 | - Weight=group1, 2 |
XForwarded Remote Addr | 基于请求的来源IP做判断 | - XForwardedRemoteAddr=192.168.1.1/24 |
1.3 路由过滤器Filter
网关过滤器有两种,分别是
-
GatewayFilter: 路由过滤器,作用于任意指定的的路由,默认不生效,需要配置到路由后生效。
这种过滤器也可以配置在default-filters下当作全局过滤器。
网关提供了33种这种过滤器,如下:
名称 | 说明 | 示例 |
---|---|---|
AddRequestHeader | 给当前请求添加一个请求头 | AddRequestHeader=headerName,headerValue |
RemoveRequestHeader | 移除请求种的一个请求头 | RemoveRequestHeader=headerName |
AddResponseHeader | 给响应结构中添加一个响应头 | AddResponseHeader=headerName,headerValue |
RemoveResponseHeader | 移除响应结果中一个响应头 | RemoveResponseHeader=headerName |
RewritePath | 请求路径重新 | RewritePath=/red/?(?.*),/${segment} |
StripPrefix | 去除请求路径中N段前缀 | StripPrefix=1 // 则路径/a/b转发时只保留/b |
spring:
cloud:
gateway:
routes:
id: xxx
uri: xxx
predicates:
xxx
filters:
- AddRequestHeader=xxx # GatewayFilter类型的过滤器
- GlobalFilter: 全局过滤器,作用范围是所有路由,声明后自动生效
默认过滤器 default-filters,对所有的过滤器都生效
spring:
cloud:
gateway:
routes:
...
default-filters: # 默认过滤器,对所有过滤器都生效
- AddRequestHeader=name, wangke
二、过滤器的使用
2.1 登录校验过滤器
网关登录校验需要关注的几个问题
1)如何在网关转发之前做登录校验?
2)网关如何将用户信息传递给微服务?
3)微服务之间如何传递用户信息?
@Component
public class TokenGlobalFilter implements GlobalFilter, Ordered {
/**
*
* @param exchange 网关内部的上下文,保存网关内部的共享数据(如:request, response)
* @param chain 过滤器链
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 判断token有效性 ...
// 放行过滤器(使用了责任链模式)
return chain.filter(exchange);
}
/**
* 设置过滤器的优先级(值越大优先级月低)
* @return -1
*/
@Override
public int getOrder() {
return -1;
}
}
解析Token
…
网关传递用户信息
在网关登录校验过滤器中,从Token中解析出正确的用户信息,将其保存在Http请求头中向下传递。
要修改转发的微服务请求,需要用到ServerWebExchange类提供的API
// 传递用户信息
String userInfo = "省略...";
ServerWebExchange serverWebExchange = exchange.mutate() // mutate就是对下游请求做修改
.request(builder -> builder.header("userInfo", userInfo))
.build();
用户拦截器
因为在每个微服务中都需要获取登录的用户信息,因此可以直接在common模块中定义需要的拦截器,其它微服务引用即可。
UserContext工具类
/**
* 用户上下文
*
* @author Ke Wang
* @since 2024-06-25
*/
public class UserContext {
private static final ThreadLocal<String> tl = new ThreadLocal<>();
/**
* 保存当前用户信息到ThreadLocal
* @param userId
*/
public static void setUser(String userId) {
tl.set(userId);
}
/**
* 获取当前登录的用户信息
* @return userId
*/
public static String getUser() {
return tl.get();
}
/**
* 移除当前登录的用户信息
*/
public static void removeUser() {
tl.remove();
}
}
拦截器类
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 1.获取登录的用户信息
String userInfo = request.getHeader("userInfo");
// 2.如果获取到了用户,则存入ThreadLocal
if (!StringUtils.isEmpty(userInfo)) {
UserContext.setUser(userInfo);
}
// 3. 放行
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
// 当请求处理完成之后清理用户信息
UserContext.removeUser();
}
}
拦截器配置类
@Configuration
// 对于SpringMVC该配置类才生效(Gateway是基于Webflux的,对其不会起作用)
@ConditionalOnClass(DispatcherServlet.class)
public class CommonWebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
OpenFeign传递用户信息
微服务之间的调用是通过OpenFeign来实现的。假如微服务A调用微服务B,微服务A中的用户信息是从网关的请求头中获取的,当调用微服务B时,也是需要将用户信息通过请求头传递给微服务B的。
OpenFeign中提供了一个拦截器接口,所有由OpenFeign发起的请求都会先调用拦截器处理请求。
public interface RequestInterceptor {
/**
* 在每次发起请求前调用,template可以对请求头进行修改
*/
void apply(RequestTemplate template);
}
示例:
@Component
public class TokenFeignClientInterceptor implements RequestInterceptor {
/**
* OpenFeigin拦截器,再每次OpenFeign发起请求前进行拦截
* @param requestTemplate 请求模板
*/
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 添加token
requestTemplate.header("Authorization", request.getHeader("Authorization"));
// 添加userinfo
requestTemplate.header("userInfo", UserContext.getUser());
}
}
2.2 自定义GatewayFilter
自定义GatewayFilter不是直接实现GatewayFilter,而是继承工厂类AbstractGatewayFilterFactory,由工厂根据不同的config生产出自定义的GatewayFilter。
无参的自定义GatewayFilter
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
/*
* 返回匿名配置new GatewayFilter(),但匿名内部类无法在实现Ordered接口,因此无法在确定过滤器的顺序
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("Print Andy is running ...");
return chain.filter(exchange);
}
};
*/
/**
* 提供了装饰类 OrderedGatewayFilter(), 增加了Ordered接口的实现
*/
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("Print Any is running ...");
return chain.filter(exchange);
}
}, 0);
}
}
注意:自定义过滤器工厂类的名字是XxxGatewayFilterFactory(后缀是GatewayFilterFactory),在配置文件中配置过滤器的前缀Xxx。
配置文件中的配置
spring:
cloud:
gateway:
default-filters:
- PrintAny
有参的自定义GatewayFilter
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
/*
* 返回匿名配置new GatewayFilter(),但匿名内部类无法在实现Ordered接口,因此无法在确定过滤器的顺序
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("Print Andy is running ...");
return chain.filter(exchange);
}
};
*/
/**
* 提供了装饰类 OrderedGatewayFilter(), 增加了Ordered接口的实现
*/
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String a = config.getA();
String b = config.getB();
String c = config.getC();
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println("Print Any is running ...");
return chain.filter(exchange);
}
}, 0);
}
/**
* 自定义配置属性
* 成员变量名很重要,下面会用到
*/
@Data
public static class Config {
private String a;
private String b;
private String c;
}
/**
* 将变量名以此返回,顺序很重要,将来读取参数时需要按顺序获取
*/
@Override
public List<String> shortcutFieldOrder() {
ArrayList<String> params = new ArrayList<>();
params.add("a");
params.add("b");
params.add("c");
return params;
}
public PrintAnyGatewayFilterFactory() {
super(Config.class);
}
}
三、动态网关
spring-cloud-gateway-server包提供了RouteDefinitionWriter接口,可以动态修改路由。
动态路由的实现可以通过配置中心或数据库来实现。
3.1 基于配置中心的动态路由
实现动态路由的原理是:
将路由信息配置在配置中心,对配置信息进行监控,将路由变化信息通过RouteDefinitionWriter更新路由表。
1)在nacos中配置路由信息
dataId名称:gateway-routes.json
group: DEFAULT_GROUP
[
{
"id": "service-user",
"uri": "lb://demo-springcloud-alibaba-service-user",
"predicates": [{
"name": "Path",
"args": {
"_genkey_0": "/sys/**",
"_genkey_1": "/user/**"
}
}],
"filters": []
}
]
2)编写动态路由组件DynamicRoute
@Component
@RequiredArgsConstructor
public class DynamicRoute {
private final NacosConfigManager nacosConfigManager;
private final RouteDefinitionWriter routeDefinitionWriter;
/**
* 路由Id集合
*/
private static Set<String> routeIds = new HashSet<>();
private String dataId = "gateway-routes.json";
private String group = "DEFAULT_GROUP";
private Long timeout = 3000L;
/**
* 初始化路由信息
* @PostConstruct 表示在组件初始化完成之后执行
* @throws NacosException
*/
@PostConstruct
private void initRoutes() throws NacosException {
// routeInfo是从配置中心拉取到的配置信息
String routeInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, timeout, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
/**
* 当配置发送变化时,监听到了变化
* @param s
*/
@Override
public void receiveConfigInfo(String configInfo) {
// 监听到了配置变化,需要更新路由表
updateConfigInfo(configInfo);
}
});
// 当类初始化完成后加载路由表
updateConfigInfo(routeInfo);
}
/**
* 更新路由表
* @param configInfo 路由表JSON
*/
private void updateConfigInfo(String configInfo) {
ObjectMapper objectMapper = new ObjectMapper();
List<RouteDefinition> routeDefinitions = null;
try {
routeDefinitions = objectMapper.readValue(configInfo, new TypeReference<List<RouteDefinition>>() {});
} catch (JsonProcessingException e) {
throw new RuntimeException("路由信息配置有无");
}
if (routeDefinitions == null || routeDefinitions.size() == 0) {
return;
}
// 当路由有变化时,先删除所有路由
if (routeIds != null && !routeIds.isEmpty()) {
for (String routeId: routeIds) {
routeDefinitionWriter.delete(Mono.just(routeId)).subscribe();
}
}
routeIds.clear();
// 再重新添加路由
for (RouteDefinition routeDefinition: routeDefinitions) {
routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
routeIds.add(routeDefinition.getId());
}
}
}
3.2 基于数据库的动态路由
将路由信息配置在数据库中,当通过接口修改路由数据后,再调用RouteDefinitionWriter更新路由表。
1)GatewayService
定义一个GatewayService,用于从数据库加载所有路由,并且提供修改路由的接口
public interface GatewayService {
/**
* 加载所有路由
* @return 路由数据
*/
String loadAllRoutes();
/**
* 加载(更新)一条路由
* @param gatewayEntity
* @return
*/
String loadRoute(GatewayEntity gatewayEntity);
}
接口实现类 GatewayServiceImpl
@Service("gatewayService")
public class GatewayServiceImpl implements GatewayService, ApplicationEventPublisherAware {
private ApplicationEventPublisher publisher;
@Resource
private GatewayDAO gatewayDAO;
@Resource
private RouteDefinitionWriter routeDefinitionWriter;
/**
* 加载所有路由
* @return true/false
*/
@Override
public String loadAllRoutes() {
List<GatewayEntity> allRoutes = gatewayDAO.findAll();
if (CollectionUtils.isEmpty(allRoutes)) {
return "failed";
}
allRoutes.stream().forEach(gatewayEntity -> {
loadRoute(gatewayEntity);
});
return "success";
}
/**
* 加载一条路由
* @param gatewayEntity
* @return
*/
@Override
public String loadRoute(GatewayEntity gatewayEntity) {
// 申明一个路由定义
RouteDefinition routeDefinition = new RouteDefinition();
// 定义的路由唯一的id
routeDefinition.setId(gatewayEntity.getRouteId());
// 定义uri
URI uri = null;
if ("0".equals(gatewayEntity.getRouteType())) {
// 如果配置路由type为0的话 则从注册中心根据服务名获取转发地址
uri = UriComponentsBuilder.fromUriString("lb://" + gatewayEntity.getRouteUrl() + "/").build().toUri();
} else {
// 如果配置路由type为1的话,则数据库存储的是即要转发的地址
uri = UriComponentsBuilder.fromHttpUrl(gatewayEntity.getRouteUrl()).build().toUri();
}
routeDefinition.setUri(uri);
// 定义Predicate
PredicateDefinition predicate = new PredicateDefinition();
// 路由断言采用路由匹配(除了路径匹配还有Header, Host, Method, Query, RemoteAddr)
predicate.setName("Path");
// 路由转发地址
Map<String, String> predicateParams = new HashMap<>(8);
predicateParams.put("pattern", gatewayEntity.getRoutePattern());
predicate.setArgs(predicateParams);
routeDefinition.setPredicates(Arrays.asList(predicate));
// 定义Filter
FilterDefinition filterDefinition = new FilterDefinition();
// 路径去前缀
// filterDefinition.setName("StripPrefix");
// Map<String, String> filterParams = new HashMap<>(8);
// filterParams.put("_genkey_0", "1");
// filterDefinition.setArgs(filterParams);
// 转发带域名
Map<String, String> preserveHostFilterParams = new HashMap<>();
filterDefinition.setName("PreserveHostHeader");
preserveHostFilterParams.put("_genkey_0", "true");
filterDefinition.setArgs(preserveHostFilterParams);
routeDefinition.setFilters(Arrays.asList(filterDefinition));
// 保存
routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
// 发布刷新路由事件
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
}
2)Spring Boot容器启动之后调用网关服务Gateway初始化路由
@Component
@Slf4j
public class GatewayRunner implements ApplicationRunner {
@Resource
private GatewayService gatewayService;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("执行从数据库加载所有路由");
gatewayService.loadAllRoutes();
}
}