实际业务跨域问题处理与gateway源码深入理解

一、背景

由于前后端域名差别,所以出现了跨域问题

二、什么是跨域

跨域问题是指在浏览器上运行的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: "*"

五、总结

虽然跨域问题是老生常谈的问题,依赖过往经验和网上资料来尝试解决,但是这次深入了解后还是学习到很多新鲜知识点的。

相关推荐
Yaml441 分钟前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠2 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries2 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_2 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平3 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码4 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞5 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod5 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。6 小时前
Spring Boot 配置文件
java·spring boot·后端