1. 跨源资源共享(CORS)

1.1 CORS 定义

跨源资源共享Cross-Origin Resource Sharing,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的CORS 预检请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。

跨源 HTTP 请求的一个例子:运行在 https://domain-a.com 的 JavaScript 代码使用 XMLHttpRequest 来发起一个到 https://domain-b.com/data.json 的请求。

同源和跨源相反,同源策略直接共享。

如果两个 URL 的 protocolport (en-US) (如果有指定的话) 和 host 都相同的话,则这两个 URL 是同源

出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求。例如,XMLHttpRequestFetch API 遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。

CORS 机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequestFetch)使用 CORS,以降低跨源 HTTP 请求所带来的风险。

CORS 机制的图表表示

图片来源参考文档[1]

1.2 功能概述

跨源资源共享标准新增了一组 HTTP 标头字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(例如 CookieHTTP 认证相关数据)。

CORS 请求失败会产生错误,但是为了安全,在 JavaScript 代码层面无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。

1.3 使用场景

2. 预检请求 Preflight request

一个 CORS 预检请求是用于检查服务器是否支持 CORS 即跨域资源共享。

它一般是用了以下几个 HTTP 请求首部的 OPTIONS 请求:Access-Control-Request-MethodAccess-Control-Request-Headers,以及一个 Origin 首部。

当有必要的时候,浏览器会自动发出一个预检请求;所以在正常情况下,前端开发者不需要自己去发这样的请求。

举个例子,一个客户端可能会在实际发送一个 DELETE 请求之前,先向服务器发起一个预检请求,用于询问是否可以向服务器发起一个 DELETE 请求:

OPTIONS /resource/foo
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org

如果服务器允许,那么服务器就会响应这个预检请求。并且其响应首部 Access-Control-Allow-Methods 会将 DELETE 包含在其中:

HTTP/1.1 200 OK
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

(4)Access-Control-Max-Age

The Access-Control-Max-Age 这个响应头表示 preflight request (预检请求)的返回结果(即 Access-Control-Allow-MethodsAccess-Control-Allow-Headers 提供的信息)可以被缓存多久(秒)

返回结果可以被缓存的最长时间(秒)。 在 Firefox 中,上限是 24 小时 (即 86400 秒)。 在 Chromium v76 之前, 上限是 10 分钟(即 600 秒)。 从 Chromium v76 开始,上限是 2 小时(即 7200 秒)。 Chromium 同时规定了一个默认值 5 秒。 如果值为 -1,表示禁用缓存,则每次请求前都需要使用 OPTIONS 预检请求。

4. 跨域报错形式

跨域检测是浏览器行为,所以F12中可以直接看到。

Chrome浏览器中typepreflight.

5. SpringBoot中处理跨域的方式

5.1 Webconfig 增加配置

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 允许全部请求跨域
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods(
                        HttpMethod.GET.name(),
                        HttpMethod.HEAD.name(),
                        HttpMethod.POST.name(),
                        HttpMethod.PUT.name(),
                        HttpMethod.DELETE.name());
    }
}

5.1 增加Filter

import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(filterName = "OptionFilter",urlPatterns = {"/*"})
public class OptionFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        logger.info("message coming");
        // 允许所有请求跨域。这些配置最好放在nginx中
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, private-token");
        response.setHeader("Access-Control-Max-Age", "3600");
        filterChain.doFilter(request, response);
    }
}

5.2 注解(精细化模式)

Controller或者具体的方法都可以。

示例:

public class GoodsController {
	@CrossOrigin(origins = "http://localhost:4000")
	@GetMapping("goods-url")
	public Response queryGoodsWithGoodsUrl(@RequestParam String goodsUrl) throws Exception {}
}  

@Crossorigin注解:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {}

6. Nginx 处理跨域

    add_header Access-Control-Allow-Origin *;

    location / {
            if ($request_method = 'OPTIONS') {
               add_header Access-Control-Allow-Origin *;
               add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
                    return 204;
            }
             
            ... ...
    }

7. 复健体操

  1. CORS
    • 定义
    • 同源定义
    • 功能概述
    • 使用场景
  2. 预检请求
    • 定义
    • 四个header
    • 使用方式
  3. 跨域处理方式
    • Springboot 两种处理方式
    • Nginx

8. 参考文档

[1] 跨源资源共享(CORS)

[2] Spring Boot 解决跨域问题的 3 种方案!

[3] 跨域资源共享CORS详解-阮一峰

[4] nginx 指定多个域名跨域请求配置

[5] Nginx配置跨域请求 Access-Control-Allow-Origin *