问题背景
有个服务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;@Beanpublic 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 {@BeanCorsWebFilter 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, 跨域配置如下
@Beanpublic WebMvcConfigurer webMvcConfigurer(){return new WebMvcConfigurer(){@Overridepublic 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}
}