spring配置CORS后未返回Access-Control-Allow-Origin的踩坑解决

一、配置方式

  在 Spring 框架下解决 CORS 问题,前面试了两种方法,发现在一种场景下,HTTP Response header 始终未应答 Access-Control-Allow-Origin:*

  (1)第一种方式,通过在 Controller 层增加 @CrossOrigin 注解。

@CrossOrigin
@RestController
@RequestMapping("/file")
public class FileController {
		...
}

  (2)第二种方式,利用 Spring 的 WebMvcConfigurer 中的 addCorsMappings


import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 支持跨域资源共享-CORS 配置
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        /**
         * addMapping: /** 表示所有路径及子路径下的 HTTP 应答都进行 Access-Control 标头包装
         * allowedOrigins: response header 中增加 Access-Control-Allow-Origin: * (表示允许所有 Origin 来源的跨域请求)
         * allowedMethods:response header 中增加 Access-Control-Allow-Methods: * (表示允许所有 HTTP Method)
         * allowedHeaders:response header 中增加 Access-Control-Allow-Headers: *
         * maxAge:response header 中增加 Access-Control-Max-Age: 1800 (表示建议浏览器缓存预检【Options请求】结果 1800s,可以降低服务端处理预检请求的压力)
         *
         * 配置解释参考:https://cloud.tencent.com/developer/article/1513418
         */
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("*")
                .allowedHeaders("*")
                .maxAge(1800);
    }
}

二、问题踩坑

  这两种方式测下来发现都有一个问题,就是如果 Origin 和 请求的 Url 地址是同源的( HTTP Method + host + port 完全一致则认为同源),则 Spring 框架并不会在 Response Header 中应答 Access-Control-Allow-Origin: *“同源访问时Spring不会返回Access-Control-Allow-Origin标头”,这个下的判断源码依据下面再谈,只是通过测试下的判断。Tips:翻看源码发现第一种加 @CrossOrigin 注解的方式跟第二种通过WebMvcConfigurer .addCorsMappings()配置, Spring 内部实现,其实是走的同一套,所以两种方式都会碰到同样的问题。

  然后正好我们有个场景,是内部的前端 Ajax 跨域调用到这个服务(假设叫 S1 服务,对应服务地址为 https://hello.com/file )上来,并且域名用的是一样的都是 hello.com(假设其他服务请求地址为:hello.com/server),但是这个域名支持 httpshttp两种方式访问。结果发现,用 httpsOrigin: https://hello.com/server的 Origin header 下访问https://hello.com/file 是会返回 Access-Control-Allow-Origin: *,但是换成 http 下去请求 https://hello.com/file就出问题了,并没有返回 Access-Control-Allow-Origin: *,导致前端Ajax请求被浏览器因 CORS 问题而挡掉。

三、原因分析

  按照刚才的判断: “同源访问时Spring不会返回Access-Control-Allow-Origin标头”,理论上Origin: https://hello.com/server 下请求 https://hello.com/file 是同源,不会出现 Access-Control-Allow-Origin: *,但偏偏刚好相反,origin = http & url = https 这个搭配出不来 Access-Controlorigin = https & url = https 这个搭配却出来了 Access-Control
  后来回想了下,也注意到了 Response 内容里的 Server: nginx/1.13.5,突然想起来 nginx 这类外部网关,会把外部进来的 https 请求解密解包,然后以 http 的方式转发给内网服务。
  这就解释通了上面的相反现象,origin = http & url = https 这种搭配下,因为 url 中的 https 经过 nginx 处理成 http 了,到了 Spring 层实际是个 http 请求,所以 Spring 判断其实是同源,于是没有应答 Access-Control-Allow-Origin: *;而origin = https & url = https 这种搭配,恰恰因为 nginx 处理成 http,到了 Spring 层判断时发现 origin 的是 https 地址,而 url 是 http,不同源,所以才有Access-Control
  当然,以上坑也只有在内部同域名下Ajax访问才有出现。提供给到外部服务时,一般两边域名就不一样,无论是 https / http 与否,Spring 肯定都会判断是 cross origin,所以都会有Access-Control

结合Spring CrossOrigin源码解析其跨域判断: (Spring源码版本:spring-web:5.2.15.RELEASE)
判断是否跨域源码:org.springframework.web.cors.CorsUtils.java isCorsRequest()


	/**
	 * Returns {@code true} if the request is a valid CORS one by checking {@code Origin}
	 * header presence and ensuring that origins are different.
	 */
	public static boolean isCorsRequest(HttpServletRequest request) {
		String origin = request.getHeader(HttpHeaders.ORIGIN);
		if (origin == null) {
			return false;
		}
		UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build();
		String scheme = request.getScheme();
		String host = request.getServerName();
		int port = request.getServerPort();
		return !(ObjectUtils.nullSafeEquals(scheme, originUrl.getScheme()) &&
				ObjectUtils.nullSafeEquals(host, originUrl.getHost()) &&
				getPort(scheme, port) == getPort(originUrl.getScheme(), originUrl.getPort()));

	}

可以发现,Spring @CrossOrigin 判断请求是否跨域,是根据 Header.Origin 域和 request.uri 判断的,Origin 就是前端送在 Header上的 Orgin,标识是从哪个源host过来的;而 request 那就是从Servlet容器(比如Tomcat)取出来的,也就是经过了 Nginx 等网关转发过的 request,所以一旦 Nginx 把 https 协议转成了 http,那 Spring @CrossOrigin 判断请求是否跨域就会出问题。
然后再查一下CorsUtils.isCorsRequest()的调用栈,可以在DefaultCorsProcessor.processRequest()发现一行:

if (!CorsUtils.isCorsRequest(request)) {
			return true;
}

DefaultCorsProcessor是 Spring 实现 CORS 的核心处理器,在processRequest()方法最后进行的handleInternal()操作就是执行了往response填充Access-Control-Allow-Origin等header信息,源码如下,所以这里判断不是 CorsRequest 就直接返回了,不会填充Access-Control-Allow-Origin,也就印证了上面我的判断:
DefaultCorsProcessor.handleInternal():


	/**
	 * Handle the given request.
	 */
	protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
			CorsConfiguration config, boolean preFlightRequest) throws IOException {

		String requestOrigin = request.getHeaders().getOrigin();
		String allowOrigin = checkOrigin(config, requestOrigin);
		HttpHeaders responseHeaders = response.getHeaders();

		if (allowOrigin == null) {
			logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
			rejectRequest(response);
			return false;
		}

		HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
		List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
		if (allowMethods == null) {
			logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
			rejectRequest(response);
			return false;
		}

		List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
		List<String> allowHeaders = checkHeaders(config, requestHeaders);
		if (preFlightRequest && allowHeaders == null) {
			logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
			rejectRequest(response);
			return false;
		}

		responseHeaders.setAccessControlAllowOrigin(allowOrigin);

		if (preFlightRequest) {
			responseHeaders.setAccessControlAllowMethods(allowMethods);
		}

		if (preFlightRequest && !allowHeaders.isEmpty()) {
			responseHeaders.setAccessControlAllowHeaders(allowHeaders);
		}

		if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
			responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
		}

		if (Boolean.TRUE.equals(config.getAllowCredentials())) {
			responseHeaders.setAccessControlAllowCredentials(true);
		}

		if (preFlightRequest && config.getMaxAge() != null) {
			responseHeaders.setAccessControlMaxAge(config.getMaxAge());
		}

		response.flush();
		return true;
	}

四、解决方案

  解决方案很简单,就是换一种方式,能够强制给 response header 加上Access-Control-Allow-Origin: *,不管 同源还是 Cross Origin 与否。
  可以自己在 Controller 方法上手工给 HttpServletResponse增加 header:

response.setHeader("Access-Control-Allow-Origin", "*");

这种办法很傻很原始,而且每个方法都得写一遍,更关键的是,针对 OPTIONS 预检请求(浏览器针对跨域请求在 POST 之前一般会先自动发 OPTIONS 预检请求,详细解释见 CORS跨域资源共享(一):模拟跨域请求以及结果分析,理解同源策略)也得加个方法处理 response,否则OPTIONS请求就会因为跨域而被 Block 掉。
所以最后采用的是针对所有请求都强制给 response 加上该 header,使用了Java web 三大组件之一的 filter-过滤器来实现:
*自定义一个CorsFilter *

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
@WebFilter(filterName = "CorsFilter")
@Configuration
public class CorsFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin","*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        chain.doFilter(req, res);
    }

此外别的方案就是,也可以配置Nginx可以实现不把HTTPS转成HTTP协议,参考文章:
Nginx SSL+tomcat集群,request.getScheme() 取到https正确的协议

参考文献:

  1. CORS跨域资源共享(一):模拟跨域请求以及结果分析,理解同源策略

  2. SpringBoot-实现CORS跨域原理及解决方案

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值