一、背景
由于前后端域名差别,所以出现了跨域问题
二、什么是跨域
跨域问题是指在浏览器上运行的Web应用程序试图通过XMLHttpRequest或Fetch API等方式向不同源(域名、协议或端口)的服务器发送请求时,
浏览器会根据同源策略(Same-Origin Policy)阻止这种行为。
同源策略:developer.mozilla.org/zh-CN/docs/...
三、常见的跨域解决方案
1.CORS(Cross-Origin Resource Sharing,跨源资源共享):使用自定义的HTTP头部让浏览器与服务器进行沟通。
通常在服务器端设置 Access-Control-Allow-Origin 头部,指定允许的来源域名,即可实现跨域请求的许可。
2.JSONP(JSON with Padding):利用 script 标签的跨域特性,通过动态创建 script 标签并设置其 src 属性为跨域的 URL,服务器端返回的响应数据需要用特定的格式包裹起来,并通过回调函数返回给客户端。
只支持GET数据请求,不支持POET数据请求。
3.代理服务器(如nginx反向代理):在同源策略限制下,可以通过在同域名下的服务器上设置一个代理服务器,将客户端请求转发到目标服务器,再将相应的结果返回给客户端。
客户端只需要与代理服务器通信,而不是直接与目标服务器通信,间接实现了跨域请求。
四、实际问题处理过程
(1)get请求跨域篇:
1.在服务的controller层增加@CrossOrigin注解;后端要设置Access-Control-Allow-Origin为请求的源地址,不能是*,而且还要设置header('Access-Control-Allow-Credentials: true');
2.从其他项目中复制粘贴拦截器的方式
get请求成功解决!
后来因为传输数据增加,所以需要把get换成post请求,前端又反馈出现跨域问题了!
(2)post请求跨域篇:
1.经过查看发现是OPTIONS的请求返回了403 跨域非简单请求会触发预检请求
浏览器将CORS请求分成两类:简单请求(simple request)
和非简单请求(not-so-simple request
)
为什么要使用预检请求?
这些非简单请求有可能会在服务器进行比较大的运算,增加负载,如果此时cors不通过,就有可能增加了服务器没有必要的运算,如果此时有预检请求,如果不通过,则真实请求不会发出,在一定程度上减少了服务器无效的运算。
2.通过查看业务容器的access.log并没有发现OPTIONS请求,所以排除是拦截器的问题
3.公司的网关没办法查,不过大概率不会是这边的问题
4.查看网关的nginx的access.log,发现请求是打到后端网关
5.访问业务容器ip的预检请求正常返回204,所以定位到问题就出现在后端网关中!
ps:在验证过程还发现一个奇怪的现象,当请求header里面没有"Access-Control-Request-Method"的时候请求是可以正常打到业务容器的
6.首先尝试的是在后端网关增加拦截器处理,确实返回了204的状态码了,但是发现header头存在重复,只能把业务服务里的拦截器关掉
kotlin
@Slf4j
@Component
public class CorsFilter implements WebFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
response.getHeaders().set("Access-Control-Allow-Origin", origin);
response.getHeaders().set("Access-Control-Allow-Credentials", "true");
response.getHeaders().set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
response.getHeaders().set("Access-Control-Max-Age", "3600");
response.getHeaders().set("Access-Control-Allow-Headers", "*");
if (HttpMethod.OPTIONS.toString().equals(request.getMethodValue())) {
response.setRawStatusCode(HttpStatus.NO_CONTENT.value());
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -99;
}
}
弊端:由于后端网关连接的后端服务比较多,如果这么改,就要把所有的服务拦截器都干掉,不合适 7.继续深入查找问题
typescript
org.springframework.web.cors.reactive.DefaultCorsProcessor
@Override
public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders responseHeaders = response.getHeaders();
List<String> varyHeaders = responseHeaders.get(HttpHeaders.VARY);
if (varyHeaders == null) {
responseHeaders.addAll(HttpHeaders.VARY, VARY_HEADERS);
}
else {
for (String header : VARY_HEADERS) {
if (!varyHeaders.contains(header)) {
responseHeaders.add(HttpHeaders.VARY, header);
}
}
}
if (!CorsUtils.isCorsRequest(request)) {
return true;
}
if (responseHeaders.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request); //判断是否跨域
if (config == null) {
if (preFlightRequest) {
rejectRequest(response);
return false;
}
else {
return true;
}
}
return handleInternal(exchange, config, preFlightRequest);
}
/**
* Invoked when one of the CORS checks failed.
*/
protected void rejectRequest(ServerHttpResponse response) {
response.setStatusCode(HttpStatus.FORBIDDEN); //返回了403
}
java
package org.springframework.web.cors.reactive;
public abstract class CorsUtils {
public static boolean isPreFlightRequest(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
return (request.getMethod() == HttpMethod.OPTIONS
&& headers.containsKey(HttpHeaders.ORIGIN)
&& headers.containsKey(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
}
}
所以解答了没有"Access-Control-Request-Method"的时候会正常
kotlin
org.springframework.web.reactive.handler.AbstractHandlerMapping
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) { //找到两处调用set的地方
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;
}
}
@Override
public Mono getHandler(ServerWebExchange exchange) {
return getHandlerInternal(exchange).map(handler -> {
if (logger.isDebugEnabled()) {
logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
}
ServerHttpRequest request = exchange.getRequest();
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
config = (config != null ? config.combine(handlerConfig) : handlerConfig); //这边config是空的
if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
return REQUEST_HANDLED_HANDLER;
}
}
return handler;
});
}
typescript
//调用1,得知需要配置spring.cloud.gateway.globalcors
org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping
org.springframework.cloud.gateway.config.GlobalCorsProperties
@ConfigurationProperties("spring.cloud.gateway.globalcors")
public class GlobalCorsProperties {
private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
public Map<String, CorsConfiguration> getCorsConfigurations() {
return corsConfigurations;
}
}
typescript
//调用2,spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping由于默认值是false所以不会生效,需要设置为true
org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SimpleUrlHandlerMapping.class)
@ConditionalOnProperty(
name = "spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping",
matchIfMissing = false)
public class SimpleUrlHandlerMappingGlobalCorsAutoConfiguration {
@Autowired
private GlobalCorsProperties globalCorsProperties;
@Autowired
private SimpleUrlHandlerMapping simpleUrlHandlerMapping;
@PostConstruct
void config() {
simpleUrlHandlerMapping
.setCorsConfigurations(globalCorsProperties.getCorsConfigurations());
}
}
yaml
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
# 允许携带认证信息
allowCredentials: true
# 允许跨域的源(网站域名/ip),设置为全部
allowedOrigins: "*"
# 允许跨域的method, 默认为GET和OPTIONS,设置为全部
allowedMethods: "*"
# 允许跨域请求里的head字段,设置为全部
allowedHeaders: "*"
dart
//如果网关和业务都设置了跨域请求头,那么前端也会报错
//需要配置spring.cloud.gateway.default-filters:DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials Access-Control-Max-Age Access-Control-Allow-Headers,RETAIN_LAST
org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory
void dedupe(HttpHeaders headers, Config config) {
String names = config.getName();
Strategy strategy = config.getStrategy();
if (headers == null || names == null || strategy == null) {
return;
}
for (String name : names.split(" ")) {
dedupe(headers, name.trim(), strategy);
}
}
private void dedupe(HttpHeaders headers, String name, Strategy strategy) {
List<String> values = headers.get(name);
if (values == null || values.size() <= 1) {
return;
}
//DedupeResponseHeader 有三个策略:
switch (strategy) {
//仅保留第一个值(默认)
case RETAIN_FIRST:
headers.set(name, values.get(0));
break;
//仅保留最后一个值
case RETAIN_LAST:
headers.set(name, values.get(values.size() - 1));
break;
//按照第一次相遇的顺序保留所有唯一值
case RETAIN_UNIQUE:
headers.put(name, new ArrayList<>(new LinkedHashSet<>(values)));
break;
default:
break;
}
}
yaml
圆满解决,最终配置如下:
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials Access-Control-Max-Age Access-Control-Allow-Headers,RETAIN_LAST
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
# 允许携带认证信息
allowCredentials: true
# 允许跨域的源(网站域名/ip),设置为全部
allowedOrigins: "*"
# 允许跨域的method, 默认为GET和OPTIONS,设置为全部
allowedMethods: "*"
# 允许跨域请求里的head字段,设置为全部
allowedHeaders: "*"
五、总结
虽然跨域问题是老生常谈的问题,依赖过往经验和网上资料来尝试解决,但是这次深入了解后还是学习到很多新鲜知识点的。