问题背景
有个服务A ,自身对外提供服务,几个系统的前端页面也在调用,使用springboot 2.6.8开发的,自身因为有前端直接调用已经配置了跨域。
现在有网关服务,一部分前端通过网关访问服务A(因为之前没有网关服务,这里不解释为何以前没有,总之大家在工作中会遇到各种各项的系统设计 或者部署 或者使用的hack, 老旧系统又不能随便改动,主要涉及的使用方较多,很难协调他们统一改动)。 网关使用的SpringCloudGateway ( 实际的gateway版本是3.1.3 webflux 2.7.8), 网络服务也配置跨域,但是一些系统的前端通过网关调用A的时候包跨域有问题。
前端浏览器中错误信息, 这里省略其他无关域名和ip内容,只保留错误的主要信息
Access-Control-Allow-Origin’ header contains multiple values’, but only one is allowed
简要分析
两个服务都已经配置了跨域,直接访问网关自身的一些API (例如health check的api)都正常,但是通过网关访问服务A就有问题, 一看是因为两个都配置了跨域,直接取消服务A的跨域配置,错误会消息,但是因为一部分前端是直接调用A服务的,因为A服务自身的跨域设置不能取消。 只能在网关处更改。 最终就是将网关处从A返回的response header进行去重,满足需求。
解决办法:
在gateway配置的RouteLocator修改如下, 备注:也可以通过配置文件实现,本人采用代码的方式。 这是可以工作的示例代码,其中svcaBaseUrl,serverPort需要再配置文件中对应的值,这两个也是A服务的域名, 以及网关服务的端口。
备注:其他人也遇到同样问题, 参看:https://stackoverflow.com/questions/70983244/use-spring-gateway-and-getting-error-access-control-allow-origin-header-conta。 本文方法仅供参考
@Component
public class MyRouteLocator
{
@Value("${svca.baseUrl}")
String svcaBaseUrl;
@Value("${server.port}")
int serverPort;
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder)
{
return builder.routes()
.route(p -> p
.path("/proxy/version/query-by-id", "/proxy/version/status", "/health",
"/proxy/version/add",)
.filters(f -> f.stripPrefix(1).dedupeResponseHeader("Access-Control-Allow-Origin", RETAIN_UNIQUE.name()))
.uri(svcaBaseUrl))
.route(p -> p
.path("/proxy/version/*","/proxy/log/*")
.filters(f -> f.stripPrefix(1))
.uri("http://127.0.0.1:%d/notSupport".formatted(serverPort)))
.build();
}
}
网关跨域配置 MyConfig.java, 备注:这只是全局配置跨域一种方式,可以在api单独配置。此处不在赘述。可以参考其他网络如何给springboot程序配置跨域设置。
package x.y.proxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
public class MyConfig {
@Bean
CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
服务A是springboot 2.6.8, 跨域配置如下
@Bean
public WebMvcConfigurer webMvcConfigurer()
{
return new WebMvcConfigurer()
{
@Override
public void addCorsMappings(CorsRegistry registry)
{
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedHeaders("*")
.allowedMethods("*")
.allowCredentials(true)
.maxAge(TimeUnit.DAYS.toSeconds(1));
}
};
}
本地浏览器中验证跨域问题
跨域问题是前端发现的,后端开发可以在chrome浏览器中直接测试验证,不需要前端同学配置,可以加速开发过程
// 测试post方法使用 a.b.c是需要替换的域名 或者ip
:var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://a.b.c/version/add?Id=2&label=test1');
xhr.setRequestHeader("Content-type","application/json;charset=UTF-8");
xhr.setRequestHeader("jwt-token","JhbGciOiJSUzI1N");
xhr.setRequestHeader("x-access-token","aaa");
xhr.send('{"version": "1.0","desc": "string"}');
xhr.onload = function(e) {
var xhr = e.target;
console.log(xhr.responseText);
}
// 测试get方法使用 a.b.c是需要替换的域名 或者ip
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://a.b.c/common/health');
xhr.setRequestHeader("x-access-token","aaa");
xhr.setRequestHeader("Access-Control-Allow-Origin","bbb");
xhr.setRequestHeader("Content-type","application/json;charset=UTF-8");
xhr.send(null);
xhr.onload = function(e) {
var xhr = e.target;
console.log(xhr.responseText);
}
执行console测试示意图
备注:这里附录上网关工程的一些gradle信息,如果你使用的gateway版本与我不一样,请以对应版本的官方文档为准
例如我使用gateway 3.1.3 cors配置文件如下:
https://docs.spring.io/spring-cloud-gateway/docs/3.1.3/reference/html/#cors-configuration
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.8'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'com.x.y'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2021.0.3")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springdoc:springdoc-openapi-ui:1.6.11'
implementation 'io.netty:netty-all:4.1.75.Final'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}
bootJar {
archiveBaseName = 'app'
def excludes = [~"junit.*\\.jar", ~"hamcrest.*\\.jar"]
exclude {
for (exclude in excludes) {
if (it.name ==~ exclude) {
return true
}
}
return false
}
}