Spring配置CORS后未返回Access-Control-Allow-Origin的解决方案

1,366次阅读
没有评论

一、配置方式
  在 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),但是这个域名支持 https和 http两种方式访问。结果发现,用 https在 Origin: 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-Control,origin = 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”, “*”);
1
这种办法很傻很原始,而且每个方法都得写一遍,更关键的是,针对 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正确的协议

参考文献:

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

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

正文完
可以使用微信扫码关注公众号(ID:xzluomor)
post-qrcode
 0
评论(没有评论)

文心AIGC

2024 年 9 月
 1
2345678
9101112131415
16171819202122
23242526272829
30  
文心AIGC
文心AIGC
人工智能ChatGPT,AIGC指利用人工智能技术来生成内容,其中包括文字、语音、代码、图像、视频、机器人动作等等。被认为是继PGC、UGC之后的新型内容创作方式。AIGC作为元宇宙的新方向,近几年迭代速度呈现指数级爆发,谷歌、Meta、百度等平台型巨头持续布局
文章搜索
热门文章
潞晨尤洋:日常办公没必要上私有模型,这三类企业才需要 | MEET2026

潞晨尤洋:日常办公没必要上私有模型,这三类企业才需要 | MEET2026

潞晨尤洋:日常办公没必要上私有模型,这三类企业才需要 | MEET2026 Jay 2025-12-22 09...
反超Nano Banana!OpenAI旗舰图像生成模型上线

反超Nano Banana!OpenAI旗舰图像生成模型上线

反超Nano Banana!OpenAI旗舰图像生成模型上线 Jay 2025-12-17 10:25:43 ...
“昆山杯”第二十七届清华大学创业大赛决赛举行

“昆山杯”第二十七届清华大学创业大赛决赛举行

“昆山杯”第二十七届清华大学创业大赛决赛举行 一水 2025-12-22 17:04:24 来源:量子位 本届...
人车家全生态持续破圈,小米宣布对开发者开放小米MiMo大模型、CarIoT硬件生态

人车家全生态持续破圈,小米宣布对开发者开放小米MiMo大模型、CarIoT硬件生态

人车家全生态持续破圈,小米宣布对开发者开放小米MiMo大模型、CarIoT硬件生态 量子位的朋友们 2025-...
最新评论
ufabet ufabet มีเกมให้เลือกเล่นมากมาย: เกมเดิมพันหลากหลาย ครบทุกค่ายดัง
tornado crypto mixer tornado crypto mixer Discover the power of privacy with TornadoCash! Learn how this decentralized mixer ensures your transactions remain confidential.
ดูบอลสด ดูบอลสด Very well presented. Every quote was awesome and thanks for sharing the content. Keep sharing and keep motivating others.
ดูบอลสด ดูบอลสด Pretty! This has been a really wonderful post. Many thanks for providing these details.
ดูบอลสด ดูบอลสด Pretty! This has been a really wonderful post. Many thanks for providing these details.
ดูบอลสด ดูบอลสด Hi there to all, for the reason that I am genuinely keen of reading this website’s post to be updated on a regular basis. It carries pleasant stuff.
Obrazy Sztuka Nowoczesna Obrazy Sztuka Nowoczesna Thank you for this wonderful contribution to the topic. Your ability to explain complex ideas simply is admirable.
ufabet ufabet Hi there to all, for the reason that I am genuinely keen of reading this website’s post to be updated on a regular basis. It carries pleasant stuff.
ufabet ufabet You’re so awesome! I don’t believe I have read a single thing like that before. So great to find someone with some original thoughts on this topic. Really.. thank you for starting this up. This website is something that is needed on the internet, someone with a little originality!
ufabet ufabet Very well presented. Every quote was awesome and thanks for sharing the content. Keep sharing and keep motivating others.
热评文章
反超Nano Banana!OpenAI旗舰图像生成模型上线

反超Nano Banana!OpenAI旗舰图像生成模型上线

反超Nano Banana!OpenAI旗舰图像生成模型上线 Jay 2025-12-17 10:25:43 ...
英伟达护城河又宽了!低调收购开源算力调度王牌工具,全球过半顶级超算在用,Thinking Machines也离不开它

英伟达护城河又宽了!低调收购开源算力调度王牌工具,全球过半顶级超算在用,Thinking Machines也离不开它

英伟达护城河又宽了!低调收购开源算力调度王牌工具,全球过半顶级超算在用,Thinking Machines也离...
英伟达护城河又宽了!低调收购开源算力调度王牌工具,全球过半顶级超算在用,Thinking Machines也离不开它

英伟达护城河又宽了!低调收购开源算力调度王牌工具,全球过半顶级超算在用,Thinking Machines也离不开它

英伟达护城河又宽了!低调收购开源算力调度王牌工具,全球过半顶级超算在用,Thinking Machines也离...
是个公司都在用AI Agent,但大家真的用明白了吗| MEET2026圆桌论坛

是个公司都在用AI Agent,但大家真的用明白了吗| MEET2026圆桌论坛

是个公司都在用AI Agent,但大家真的用明白了吗| MEET2026圆桌论坛 一水 2025-12-17 ...
人车家全生态持续破圈,小米宣布对开发者开放小米MiMo大模型、CarIoT硬件生态

人车家全生态持续破圈,小米宣布对开发者开放小米MiMo大模型、CarIoT硬件生态

人车家全生态持续破圈,小米宣布对开发者开放小米MiMo大模型、CarIoT硬件生态 量子位的朋友们 2025-...