SpringBoot—CORS跨域问题详解和解决方案
关注微信公众号: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请求同时满足以下三个条件时就使用简单请求
,否则即为非简单请求。
- 请求方法是下列之一:
请求方法 |
---|
GET |
HEAD |
POST |
- 请求头中的Content-Type请求头的值是下列之一:
Content-Type请求头值 |
---|
application/x-www-form-urlencoded |
multipart/form-data |
text/plain |
- 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
头信息介绍
- Access-Control-Allow-Origin
必选字段,要么是浏览器请求时传过来的Origin
值,表示接受该域名请求;要么是*
,表示接受任意域名请求。 - Access-Control-Allow-Credentials
可选字段,布尔值,代表是否允许发送cookie。默认cookie不包含在CORS请求中,所以需要设置为true,这样服务器允许浏览器将cookie包含在请求中发送给服务器(需要注意的是若要发送cookie,Access-Control-Allow-Origin
不可以为*
,必须指定为浏览器请求时传来的Origin值。);否则,设置为false或删除该字段,服务器不允许浏览器发送cookie。 - 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
或者DELETE
,Content-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
值表示源。
头信息介绍
-
Access-Control-Request-Method
必选字段,该字段指列出的是浏览器跨域请求的方法是哪些,如PUT/DELETE -
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
头信息介绍
-
Access-Control-Allow-Origin
表示服务器允许http://www.test.com
下的请求;若设为*
,表示服务器接受任意域名请求。 -
Access-Control-Allow-Methods
必选字段,值为逗号隔开的字符串,值为服务器所支持的所有跨域请求方法,不限于浏览器在预检请求中的方法。 -
Access-Control-Allow-Headers
是否必选,需根据浏览器请求看,若浏览器请求中包含该字段,则预检响应必选。值为逗号隔开的字符串,值表示服务器所支持的所有头信息字段,不限于浏览器在预检请求中的字段。 -
Access-Control-Allow-Credentials
可选字段,布尔值,代表是否允许发送cookie。同上述的简单请求解释。 -
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中解决跨域用方法二和方法三,即为粗粒度,全局性配置。如果有特殊的细粒度控制到某个方法接受某域的请求,可以使用方法一。