Spring Boot 跨域问题的四种解决方案

2,470 阅读7分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

开篇注:“跨域”确切来讲应该说成“跨源”,对应于英文的 cross-origin,但是前者的使用频次明显更高,考虑到语言的约定俗称性,可以认为“跨域”和“跨源”两个词是可以等价替换的,所以本文将不加区分地使用两者。

同源策略

浏览器为了保证用户信息安全,应用了一系列安全策略,其中之一就是同源策略 same-origin policy,缩写是 SOP

所谓同源,指的是以下三者相同:

  • 协议相同
  • 域名相同
  • 端口号相同

同源策略用于约束跨域交互,跨域交互通常分为三类:

  • 跨域写 cross-origin writes:比如链接 <a>、重定向以及表单提交。
  • 跨域资源嵌入 cross-origin embedding:比如使用 <script> 嵌入脚本,使用 <img> 标签展示图片,使用 <link rel="stylesheet"> 引入 CSS 样式等等。
  • 跨域读 cross-origin reads:比如 XMLHttpRequest 请求、读取 DOM 对象、读取 JS 对象等等。

并非所有的跨域交互都被禁止:

  • 一般允许跨域写,特定少数的 HTTP 请求需要添加 preflight
  • 一般允许跨域资源嵌入。
  • 一般禁止跨域读。

跨域问题

跨域问题是指在同源策略的约束下,某些跨域交互受到限制而引起的问题。比如在接口联调时,前端发送跨域 AJAX 请求,如果没有处理跨域问题,就会在浏览器控制台报错:

Access to XMLHttpRequest at 'aaa' from origin 'bbb' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

总体来说,同源策略限制了以下三种跨域行为,它们都属于跨域读操作:

  • CookieLocalStorageIndexedDB 无法读取。
  • DOM 无法获得。禁止对非同源页面的 DOM 进行操作,比如不同域名的 <iframe> 不能互相访问。
  • AJAX 请求(XMLHttpRequest)不能发送。

CORS 跨源资源共享

CORS 简介

CORScross-origin resource sharing 的缩写,中文是跨源资源共享,也常被称作跨域资源共享。它是一种基于 HTTP 首部字段的机制,从服务器端(后端)解决了跨域问题。

简单请求与预检请求

浏览器把请求分为两类,简单请求 simple requests预检请求 preflighted requests,对于这两类请求的处理流程是不一样的。

简单请求指的是满足以下所有条件的请求:

  • 请求方法是 GETPOSTHEAD 其中之一
  • 除去用户代理自动设置的首部字段,首部字段仅包括以下几种:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type:仅限于 text/plainmultipart/form-dataapplication/x-www-form-urlencoded
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器。
  • 请求中没有使用 ReadableStream 对象。

所有不满足上述条件的请求就是预检请求。简单请求和预检请求的主要区别在于简单请求没有预检这一步骤,所以简单请求也可以理解为是不会触发 CORS 预检请求的请求

简单请求的工作流程:

  • 浏览器判定某次请求为简单请求,自动在首部字段中添加 Origin 字段,然后向服务器发送请求。
  • 服务器查看 Origin 中的源是否被允许,如果允许,则返回带有 Access-Control-Allow-Origin 首部字段的响应。如果不允许,则返回一般响应,该响应没有 Access-Control-Allow-OriginAccess-Control-Allow-Origin 的值一般为 * 或者请求中 Origin 指定的域。
  • 浏览器查看响应,如果存在 Access-Control-Allow-Origin 首部字段,则正常处理响应,如果没有该字段,说明请求违反同源策略,抛出异常。

预检请求的工作流程:

  • 在正式请求服务器之前,浏览器会发出一次预检请求,请求方法为 OPTIONS。其中需要包含 Origin 首部字段,以指定请求来自哪个源,除此之外还要包括 Access-Control-Request-MethodAccess-Control-Request-Headers 这两个首部字段。
  • 服务器收到请求,如果允许该跨域访问,返回带有 Access-Control-Allow-Origin 首部字段的响应,需要指定为 * 或者请求中 Origin 字段的值。
  • 如果服务器不允许该跨域请求,那么返回的响应不带有 Access-Control-Allow-Origin,浏览器将抛出异常。
  • 预检请求成功之后开始发送实际请求,工作流程类似于简单请求,浏览器发送的请求需要携带 Origin 首部字段,而服务器返回的响应需要携带 Access-Control-Allow-Origin 字段。
  • 对于预检请求,不用每次都先发一次 OPTION 请求,在一定的有效时间之内,可以不用重复发送。在服务器响应 OPTION 预检请求时,可以通过 Access-Control-Max-Age 来设置该响应的有效时间。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。

CORS 请求首部字段

  • Origin:指定预检请求或者实际请求的源。Origin 参数的值为源站 URI。它不包含任何路径信息,只是服务器名称。在所有访问控制请求(Access control request)中,Origin 首部字段总是被发送。
  • Access-Control-Request-Method:预检请求专用,用于将实际请求所使用的 HTTP 方法告诉服务器。
  • Access-Control-Request-Headers:预检请求专用,将实际请求所携带的首部字段告诉服务器。多个字段用逗号分开。

CORS 响应首部字段

  • Access-Control-Allow-Origin:指定了允许访问该资源的 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。如果服务端指定了具体的域名而非 *,那么响应首部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容。
  • Access-Control-Expose-Headers:在跨源访问时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到一些最基本的响应头,Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma,如果要访问其他头,则需要服务器设置本响应头。比如 Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
  • Access-Control-Max-Age:设置了预检请求的结果能够被缓存多久,单位为秒。
  • Access-Control-Allow-Credentials:为 true 时,表示服务器允许浏览器发送 cookie。该值只能为 true,如果服务器不需要浏览器发送 cookie,直接不写该字段即可。如果响应携带了该字段,Access-Control-Allow-Origin 不能为 *,必须明确指定为请求发送方的域名。
  • Access-Control-Allow-Methods:用于响应预检请求,规定实际请求所允许使用的 HTTP 方法。
  • Access-Control-Allow-Headers:用于响应预检请求,指明了实际请求中允许携带的首部字段。

Spring Boot 解决跨域问题

Spring Boot 中,有多种方案可以解决跨域问题,这里提到的四种方案,按照作用范围可以分为两类:

  • 局部跨域
  • 全局跨域

局部跨域

  1. 使用 @CrossOrigin 注解
  • 应用在 Controller 类上面,可以允许该类下面的所有接口接收跨域请求
@RestController
@CrossOrigin
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
	    return "hello";
	}
}
  • 应用在 Controller 类的某个方法上,可以允许该方法接收跨域请求
@RestController
public class HelloController {
    @GetMapping("/hello")
	@CrossOrigin
    public String hello() {
	    return "hello";
	}
}
  1. 使用 HttpServletResponse,手动为响应头添加字段 Access-Controll-Allow-Origin
@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(HttpServletResponse response) {
	    response.addHeader("Access-Control-Allow-Origin", "http://localhost:8080");
	    return "hello";
	}
}

全局跨域

  1. 定义 CorsConfig.java 配置类,实现 WebMvcConfigurer 接口,重写 addCorsMappings 方法。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
        .allowedOrigins("http://localhost:3000")
		.allowedHeaders("*")
        .allowedMethods("GET", "POST", "PUT", "DELETE")
		.allowCredentials(true)
		.maxAge(86400);
    }
}
  1. 使用过滤器
@Configuration
public class WebConfig  {
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();

        config.addAllowedOrigin("https://localhost:3000");
        config.setAllowCredentials(true);
        config.addAllowedMethod(CorsConfiguration.ALL);
        config.addAllowedHeader(CorsConfiguration.ALL);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        return new CorsFilter(source);
    }
}

参考资料