Spring Cloud Gateway从数据库读取并更新Cors配置
由于运维特殊性,我们没有使用配置中心,仅仅只是使用了Nacos作为注册中心。目前项目gateway网关有个小需求,需要从数据库读取Cors跨域配置,刷新到应用中。
分析源码
Spring Cloud Gateway启动时,会通过GatewayAutoConfiguration配置需求创建的bean.在创建的RoutePredicateHandlerMapping bean时,在构造方法里,通过调用父类的setCorsConfigurations()方法更新或初始化Cors跨域配置。
org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#RoutePredicateHandlerMapping
public RoutePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator,
GlobalCorsProperties globalCorsProperties, Environment environment) {
this.webHandler = webHandler;
this.routeLocator = routeLocator;
this.managementPort = getPortProperty(environment, "management.server.");
this.managementPortType = getManagementPortType(environment);
setOrder(environment.getProperty(GatewayProperties.PREFIX + ".handler-mapping.order", Integer.class, 1));
setCorsConfigurations(globalCorsProperties.getCorsConfigurations());
}
AbstractHandlerMapping是RoutePredicateHandlerMapping的抽象父类
org.springframework.web.reactive.handler.AbstractHandlerMapping#setCorsConfigurations
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
Assert.notNull(corsConfigurations, "corsConfigurations must not be null");
if (!corsConfigurations.isEmpty()) {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(this.patternParser);
source.setCorsConfigurations(corsConfigurations);
this.corsConfigurationSource = source;
}
else {
this.corsConfigurationSource = null;
}
}
所以想更新Gateway网关的Cors跨域配置,可能通过引用RoutePredicateHandlerMapping bean,调用setCorsConfigurations刷新网关服务的cors跨域配置。
实现步骤
- 从数据库从读取Cors配置
- 更新Spring 上下文中 GlobalCorsProperties实例bean的值
- 通过RoutePredicateHandlerMapping提供的setCorsConfigurations()方法刷新Cors跨域配置
数据库设计
根据 org.springframework.web.cors.CorsConfiguration实体类考虑需求处理的字段:configPath路径、allowedOrigins、allowedMethods、allowedHeaders、exposedHeaders、allowCredentials、maxAge。
考虑到GlobalCorsProperties使用Map是LinkedHashMap,应该是有序的,添加了corsOrder字段,作为排序,添加Cors配置的顺序。
CREATE TABLE `t_cors_config` (
`config_id` varchar(100) NOT NULL,
`config_name` varchar(100) DEFAULT NULL COMMENT '配置名称',
`config_path` varchar(500) NOT NULL COMMENT '配置路径',
`allowed_origins` text COMMENT '允许哪些网站的跨域请求',
`allowed_methods` text COMMENT '允许的跨域请求方式',
`allowed_headers` varchar(100) DEFAULT NULL COMMENT '允许在请求中携带的头信息',
`exposed_headers` varchar(100) DEFAULT NULL COMMENT '需要向客户端公开的请求头',
`allow_credentials` tinyint(1) DEFAULT NULL COMMENT '是否允许携带cookie',
`max_age` bigint(20) DEFAULT NULL COMMENT '这次跨域检测的有效期(单位秒)',
`cors_order` int(11) DEFAULT NULL COMMENT 'cors配置顺序',
`sys_create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`sys_update_time` timestamp NULL DEFAULT NULL,
`sys_status` int(11) DEFAULT NULL COMMENT '数据标识',
`sys_remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`config_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='跨域配置';
把数据库的cors配置转为CorsConfiguration
我在数据库的实体类,直接写了转换方法,根据当前实体实例转成CorsConfiguration的实例,具体转换如下
public CorsConfiguration getCorsConfiguration() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
//允许哪些网站的跨域请求
corsConfiguration.setAllowedOrigins(this.convert(allowedOrigins));
//允许的跨域请求方式
corsConfiguration.setAllowedMethods(this.convert(allowedMethods));
//允许在请求中携带的头信息
corsConfiguration.setAllowedHeaders(this.convert(allowedHeaders));
//需要向客户端公开的请求头
corsConfiguration.setExposedHeaders(this.convert(exposedHeaders));
corsConfiguration.setAllowCredentials(allowCredentials);
corsConfiguration.setMaxAge(maxAge);
return corsConfiguration;
}
private List<String> convert(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
if (CorsConfiguration.ALL.equals(text)) {
List<String> list = new ArrayList<>();
list.add(text);
return list;
}
try {
Yaml yaml = new Yaml();
return yaml.loadAs(text, List.class);
} catch (Exception e) {
log.error("cors配置格式转换错误 configId={} configPath={} text={}", configId, configPath, text);
log.error(e.getMessage(), e);
throw new RuntimeException("cors配置格式转换错误 configId=" + configId);
}
}
更新cors配置到实际处理逻辑
通过RoutePredicateHandlerMapping提供的setCorsConfigurations()方法刷新Cors跨域配置
整体刷新入口代码如下
GlobalCorsProperties、RoutePredicateHandlerMapping直接通过注入即可,这2个bean已经在GatewayAutoConfiguration初始化,直接使用就可以了。
至于采用什么时候刷新配置,大家可以根据自己的情况设计。我是通过一个版本表,当检查到数据库的版本号大于应用中的配置版本号时,刷新配置。
/**
* 刷新cors跨域配置
*/
private void refreshCorsConfig() {
//从数据库中读取Cors配置
List<CorsConfig> corsConfigs = corsConfigRepository.getCorsConfigs();
//更新cors跨域配置bean
corsProperties.getCorsConfigurations().clear();
//排序后处理,数值小在前,null值在后
corsConfigs.stream().sorted(Comparator.comparing(CorsConfig::getCorsOrder, Comparator.nullsLast(Integer::compareTo)))
.forEach(o -> {
//CorsConfig转成CorsConfiguration更新到GlobalCorsProperties中,key是path,value是CorsConfiguration
corsProperties.getCorsConfigurations().put(o.getConfigPath(), o.getCorsConfiguration());
});
handlerMapping.setCorsConfigurations(corsProperties.getCorsConfigurations());
log.info("完成刷新网关cors跨域配置 总数 {}", corsProperties.getCorsConfigurations().size());
}
其他说明
1、在查看源码的时候,引入的版本是Spring Cloud Gateway 2.X,后续在引入3.X、4.X版本发现,CorsConfiguration多了个allowedOriginPatterns字段,如果需求这个字段的设置,自行补上就可以了。(跟SpringBoot版本有关)
2、在查资料的时候,听网友说配置中心不支持刷新Cors配置,我在4.X Gateway发现多了CorsGatewayFilterApplicationListener,也在GatewayAutoConfiguration中初始化了,它实现了ApplicationListener,接收RefreshRoutesEvent事件,在更新路由的配置的同时,也会更新Cors跨域配置。
所以说,在4.X版本的Gateway(Spring Cloud 2022.0.0-RC2后的版本),是支持配置中心刷新Cors跨域配置的。
org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener#onApplicationEvent
@Override
public void onApplicationEvent(RefreshRoutesEvent event) {
routeDefinitionLocator.getRouteDefinitions().collectList().subscribe(routeDefinitions -> {
// pre-populate with pre-existing global cors configurations to combine with.
var corsConfigurations = new HashMap<>(globalCorsProperties.getCorsConfigurations());
routeDefinitions.forEach(routeDefinition -> {
var corsConfiguration = getCorsConfiguration(routeDefinition);
corsConfiguration.ifPresent(configuration -> {
var pathPredicate = getPathPredicate(routeDefinition);
corsConfigurations.put(pathPredicate, configuration);
});
});
routePredicateHandlerMapping.setCorsConfigurations(corsConfigurations);
});
}