SpringBoot—CORS跨域问题详解和解决方案 引言 同源策略 跨域 SpringBoot解决跨域

SpringBoot—CORS跨域问题详解和解决方案
引言
同源策略
跨域
SpringBoot解决跨域
关注微信公众号:CodingTechWork,一起学习进步。

  在前后端开发过程中,遇到过一种错误,类似于报错:

Access to XMLHttpRequest at 'http://127.0.0.1:8080/' from origin 'null' has been blocked by CORS policy! No 'Access-Control-Allow-Origin' header is present on the requested resource.


亦或是

XMLHttpRequest cannot load http://127.0.0.1:8080/xxx/yy/list. Response to preflight request doesn't paas access control check: No 'Access-Control-Allow-Origin' header is present on the requestesd resource. Oirgin 'http://127.0.0.1:8010' is therefore not allowed access. The response bad HTTP status code 404.

  对于上述的报错,似曾相识,这就是跨域报错,那什么是跨域?在了解跨域问题之前,我们还得了解一下什么是同源策略?什么是CORS?SpringBoot中如何解决跨域问题?

同源策略

非同源隐患

  用户登录网站A后,又通过网站A访问了网站B,若网站B可以读取网站A的cookie(包含了网站A的用户登录信息、状态等隐私数据),网站B就可以冒充用户进行网站A的数据窃取、提交表单等操作。

同源概述

  同源策略是Netscape公司提出的一种安全策略,基本上所有支持JavaScript的浏览器都使用该测了。只有同源网页中可以共享cookie,是为了保证用户的信息安全,防止恶意的网站过来窃取用户数据。所谓同源策略就是规定浏览器中的两个URL地址的协议、域名、端口相同。

同源示例

网址:http://www.test.com/dir1/test.html
其中,协议是http://,域名是www.test.com,端口是8080(默认省略)。
同源情况如下

网址 同源情况
http://www.test.com/dir2/test.html 同源(协议、域名和端口相同)
https://www.test.com/dir1/test.html 不同源(协议不同)
http://test.com/dir1/test.html 不同源(域名不同)
http://www.test.com:8081/dir1/test.html 不同源(端口不同)

跨域

跨域概述

  由于浏览器的同源策略限制,要求url的协议、域名和端口都需要相同,而浏览器访问中同页面下会遇到跨域操作,即请求的url协议、域名和端口中有和当前网页url不同的情形。
  比如一个资源在与该资源本身所在服务器的不同域、端口中请求一个资源时,资源就会发起一个跨域请求,比如http://www.baidu.com的某个html页面中通过访问图片的http://www.google.com/image1.jpg,这就会发出一个跨域http请求。由于同源限制,浏览器限制从页面脚本内发起跨域请求,只能加载本域下的资源。如何解决?这个时候就需要使用CORS机制。

CORS概述

  跨域资源共享(CORS, Cross-Origin Resource Sharing)是一个W3C标准,允许浏览器向跨源服务器发出请求,它是使用额外的HTTP头部告知浏览器,允许web应用从不同源服务器*问指定资源,从而突破AJAX的同源策略限制。
  CORS需要浏览器和服务器都支持,而浏览器会自动完成CORS通信,重点是服务器实现CORS接口,这样才能保证CORS跨域通信。

CORS分类

  CORS请求分为两类:简单请求和非简单请求(预先请求)
  当CORS请求同时满足以下三个条件时就使用简单请求,否则即为非简单请求。

  1. 请求方法是下列之一:
请求方法
GET
HEAD
POST
  1. 请求头中的Content-Type请求头的值是下列之一:
Content-Type请求头值
application/x-www-form-urlencoded
multipart/form-data
text/plain
  1. Fetch规范定义CORS安全头的集合(跨域请求中自定义的头属于安全头的集合)
Fetch规范的安全头
Accept
Accept-Language
Content-Language
Content-Type
DPR
Downlink
Save-Data
Viewport-Width
Width

简单请求

简单请求的请求示例

  简单请求即为浏览器直接发出CORS请求,就是在头信息中自动添加一个Origin字段。如下举例:

GET /cors HTTP/1.1
Host: www.user.com
Origin: http://www.test.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0..

  其中,Origin说明了源(协议、域名、端口),传送到服务器后,由服务器根据Origin值来决定是否允许这次请求。

简单请求的响应示例

  当收到简单请求后,若服务器允许访问Origin指定的源,服务器则会多几个头信息字段用来标识(如下举例),否则,服务器会返回一个正常的HTTP响应。

Access-Control-Allow-Origin: http://www.test.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Test01
Content-Type: text/html; charset=utf-8

  头信息介绍

  1. Access-Control-Allow-Origin
    必选字段,要么是浏览器请求时传过来的Origin值,表示接受该域名请求;要么是*,表示接受任意域名请求。
  2. Access-Control-Allow-Credentials
    可选字段,布尔值,代表是否允许发送cookie。默认cookie不包含在CORS请求中,所以需要设置为true,这样服务器允许浏览器将cookie包含在请求中发送给服务器(需要注意的是若要发送cookie,Access-Control-Allow-Origin不可以为*,必须指定为浏览器请求时传来的Origin值。);否则,设置为false或删除该字段,服务器不允许浏览器发送cookie。
  3. Access-Control-Expose-Headers
    可选字段,CORS请求时,XMLHttpResquest对象的getResponseHeader()方法只可以取到6个基本字段(Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma),若需要取除此以外的其他字段,需通过该字段进行指定,如上述Access-Control-Expose-Headers: Test01将取出Test01的字段值。

非简单请求

  非简单请求即为对服务器有特殊要求的请求,即在浏览器页面发出的不是简单请求的请求,是不是有点绕?
  那我们就对比一开始说的简单请求同时需要满足的三个条件,挑出不是那三个条件的任意,即为非简单请求,比如请求方法是PUT或者DELETEContent-Type请求头的值为application/json的。
  非简单请求发出后,并不会立即执行对应的请求代码,在双方正式通信之前会触发预先请求模式,预先请求模式会发出预先验证的请求(预检请求),执行一次正常的HTTP查询操作,是一个OPETIONS请求,用于查询要被跨域访问的服务器是否允许当前域名下的页面发送跨域请求,可以使用那些HTTP动词、头信息字段等,当得到服务器授权确认后,浏览器方可发送真正的XMLHttpRequest请求。

预检请求的请求示例

OPTIONS /cors HTTP/1.1
Host: www.user.com
Origin: http://www.test.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header,content-type
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

  非简单请求发出的预先验证请求是OPETIONS请求,用于询问服务器是否允许本次跨域;Origin值表示源。
  头信息介绍

  1. Access-Control-Request-Method
    必选字段,该字段指列出的是浏览器跨域请求的方法是哪些,如PUT/DELETE

  2. Access-Control-Request-Headers
    该字段是逗号分隔的字符串,指定浏览器CORS请求额外发送的头信息字段。

预检请求的响应示例

通过预检请求

HTTP/1.1 200 OK
Date: Aug, 01 Dec 2020 20:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://www.test.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header,content-type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Content-Length: 100
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

  头信息介绍

  1. Access-Control-Allow-Origin
    表示服务器允许http://www.test.com下的请求;若设为*,表示服务器接受任意域名请求。

  2. Access-Control-Allow-Methods
    必选字段,值为逗号隔开的字符串,值为服务器所支持的所有跨域请求方法,不限于浏览器在预检请求中的方法。

  3. Access-Control-Allow-Headers
    是否必选,需根据浏览器请求看,若浏览器请求中包含该字段,则预检响应必选。值为逗号隔开的字符串,值表示服务器所支持的所有头信息字段,不限于浏览器在预检请求中的字段。

  4. Access-Control-Allow-Credentials
    可选字段,布尔值,代表是否允许发送cookie。同上述的简单请求解释。

  5. Access-Control-Max-Age
    可选字段,用于指定预检请求的有效期,单位为秒。上述86400s表示有效期为10天,在此期间,不需要发送额外的预检请求,会缓存该请求。

否定预检请求

HTTP/1.1 403 Forbidden
Date: Aug, 01 Dec 2020 20:15:39 GMT
Content-Type: application/json; charset=utf-8

服务器否定了预检请求,会返回一个正常的HTTP响应,浏览器收到该响应后会触发错误,被XMLHttpRequest对象的onerror回调函数捕获,如下示例报错信息。

XMLHttpRequest cannot load http://www.test.com.
Origin http://www.test.com is not allowed by Access-Control-Allow-Origin.

正常请求的请求示例

服务器通过预检请求后,在有效期内,浏览器都无需再发送预检请求,都直接发送正常的CORS请求,同简单请求一样。

PUT /cors HTTP/1.1
Host: www.user.com
Origin: http://www.test.com
Accept-Language: en-US
X-Custom-Header: xxx
Connection: keep-alive
User-Agent: Mozilla/5.0...

其中,Origin: http://www.test.com是浏览器自动添加。

正常请求的响应示例

Access-Control-Allow-Origin: http://www.test.com
Content-Type: application/json; charset=utf-8

SpringBoot解决跨域

方法一:基于@CrossOrigin配置

@CrossOrigin注解源码

package org.springframework.web.bind.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
    /** @deprecated */
    @Deprecated
    String[] DEFAULT_ORIGINS = new String[]{"*"};
    /** @deprecated */
    @Deprecated
    String[] DEFAULT_ALLOWED_HEADERS = new String[]{"*"};
    /** @deprecated */
    @Deprecated
    boolean DEFAULT_ALLOW_CREDENTIALS = true;
    /** @deprecated */
    @Deprecated
    long DEFAULT_MAX_AGE = 1800L;

    @AliasFor("origins")
    String[] value() default {};

    @AliasFor("value")
    String[] origins() default {};

    String[] allowedHeaders() default {};

    String[] exposedHeaders() default {};

    RequestMethod[] methods() default {};

    String allowCredentials() default "";

    long maxAge() default -1L;
}

使用示例

@RestController
public class HiController {
	@CrossOrigin(value = "http://localhost:8080")
    @RequestMapping(value = "/hi", method = RequestMethod.GET)
    public String callHi() {
        return "hi";
    }
}

在Controller层在某个方法上通过配置@CrossOrigin注解配置接受http://localhost:8080的请求,这种有局限性,且每个方法都得配置该注解。

方法二:基于CorsFilter过滤器

@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsFilter() {
    	//new一个CorsConfiguration对象用于CORS配置信息
        CorsConfiguration corsConfiguration = new CorsConfiguration();
          //允许所有域的请求
          corsConfiguration.addAllowedOrigin("*");
          //允许请求携带认证信息(cookies)
          corsConfiguration.setAllowCredentials(true);
          //允许所有的请求方法
          corsConfiguration.addAllowedMethod("*");
          //允许所有的请求头
          corsConfiguration.addAllowedHeader("*");
          //允许暴露所有头部信息
          corsConfiguration.addExposedHeader("*");

		//添加映射路径
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);

		//返回新的CorsFilter对象
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }
}

或写成

@ConfigurationProperties("cors-config")
public class CorsConfig {
    private CorsConfiguration buildCorsConfiguration() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildCorsConfiguration());
        return new CorsFilter(source);
    }
}

方法三:基于WebMvcConfigurerAdapter全局配置

在启动类加:

public class Application extends WebMvcConfigurerAdapter {  

    @Override  
    public void addCorsMappings(CorsRegistry registry) {  
        registry.addMapping("/**")  
                .allowCredentials(true)  
                .allowedHeaders("*")  
                .allowedOrigins("*")  
                .allowedMethods("*");  
    }  
}  

或配置文件形式

@Configuration
public class CorsConfig extends WebMvcConfigurerAdapter {
	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
				.allowedOrigins("*")
				.allowedMethods("GET", "HEAD", "POST","PUT", "DELETE", "OPTIONS")
				.allowCredentials(true)
				.maxAge(3600);
	}
}

总结

  一般SpringBoot中解决跨域用方法二和方法三,即为粗粒度,全局性配置。如果有特殊的细粒度控制到某个方法接受某域的请求,可以使用方法一。

参考
阿里云API网关文档