同源策略
在浏览器中,如果我们直接使用 AJAX 发送一个对其他网站的请求(跨域请求),默认情况下是无法获取到响应的。
这是因为浏览器内置的 同源策略 对客户端脚本的限制。
默认情况下,同源策略 只允许脚本请求同源资源,而对于请求不同源的脚本在没有明确授权的情况下,无法读取对方资源。
注:同源 指的是:协议、域名 和 端口 三者都相同
同源策略是浏览器内置的一个最核心,也是最基础的安全功能,它保障了用户的上网安全。
但是,如果我们确信某个非同源网站是安全的,我们希望能够对其资源进行访问,那么,就需要通过相应的机制进行跨域请求。
最常见的前端跨域请求解决方案是 JSONP,它的原理是借助script
标签不受浏览器同源策略限制,允许跨域请求资源,因此可以通过script
标签的src
属性,进行跨域访问。如下代码所示:
// 1. 前端定义一个 回调函数 handleResponse 用来接收后端返回的数据
function handleResponse(data) {
console.log(data);
};
// 2. 动态创建一个 script 标签,并且告诉后端回调函数名叫 handleResponse
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.src = 'http://www.laixiangran.cn/json?callback=handleResponse';
body.appendChild(script);
// 3. 通过 script.src 请求 `http://www.laixiangran.cn/json?callback=handleResponse`,
// 4. 后端能够识别这样的 URL 格式并处理该请求,然后返回 handleResponse({"name": "laixiangran"}) 给浏览器
// 5. 浏览器在接收到 handleResponse({"name": "laixiangran"}) 之后立即执行 ,也就是执行 handleResponse 方法,获得后端返回的数据,这样就完成一次跨域请求了。
虽然 JSONP 可以完成跨域请求,但是它只支持GET
请求方式,限制非常大。
于是,为了更好地支持跨域资源请求,W3C 标准就发布了一套浏览器跨域资源共享标准:CORS(Cross-origin resource sharing,跨域资源共享)
CORS(跨域资源共享)
CORS 支持多种 HTTP 请求,它其实就是定义了一套跨域资源请求时,浏览器与服务器之间的交互方式。基本的原理就是通过自定义的 HTTP 请求头来传递信息,进行验证。
浏览器中,将 CORS 请求分为两种类型:
-
简单请求:同时满足以下两大条件的请求,即为简单请求:
- 请求的方法是
HEAD
、GET
或者是POST
三种之一 - 请求头不超出以下几种字段:
Accept
、Accept-Language
、Content-Language
、Last-Event-ID
、Content-Type
(其值为:application/x-www-form-urlencoded
、multipart/form-data
或text/plain
三者之一)
- 请求的方法是
非简单请求:不是简单请求的都属于非简单请求。
浏览器对于 简单请求 和 非简单请求 的 CORS 处理机制不一样,具体如下:
-
简单请求:对于简单请求的 CORS,浏览器的处理机制流程如下:
- 浏览器会在请求头添加一个额外的
Origin
头部,其值为当前请求页面的源信息(即:协议 + 域名 + 端口)。如下所示:
GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
- 服务器接收到请求后,查看到
Origin
头部指定的源信息,如果同意该请求,就会为下发的响应添加头部Access-Control-Allow-Origin
,其值为请求的源信息(或者是*
,表示允许任意源信息)。如下所示:
Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8
- 浏览器接收到响应后,会查看下是否有
Access-Control-Allow-Origin
头部信息,如果没有或者其值不匹配当前源信息,那么浏览器就会禁止响应该 CORS 请求,当前页面的 AJAX 请求的onerror
函数会得到回调。
反之,如果浏览器验证通过,则跨域请求成功。
注:CORS 请求默认不发送 Cookie 和 HTTP 认证信息,如果需要把 Cookie 发送给服务器,则 AJAX 和 服务器必须同时打开 Credentials 字段,如下所示:
- 服务器需设置:
Access-Control-Allow-Credentials: true
- AJAX 需设置:
new XMLHttpRequest().withCredentials = true;
注:如果 AJAX 发送了 Cookie,那么服务器的
Access-Control-Allow-Origin
则不能设置为*
,必须指定该明确的、与请求网页一致的域名。 - 浏览器会在请求头添加一个额外的
-
非简单请求:非简单请求是那种对服务器有特殊要求的请求,比如
PUT
或DELETE
请求,或者是Content-type: application/json
请求...
浏览器检测到非简单请求的 CORS 时,在正式发送请求前,会先进行一次探测请求(preflight),通过才会发送正式请求,具体过程如下:- 浏览器检测到非简单 CORS 请求,则先发送一个探测请求,请求方式为
OPTIONS
,如下所示:
OPTIONS /cors HTTP/1.1 Origin: http://api.bob.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
可以看到
OPTIONS
请求,除了携带Origin
请求头外,还额外携带了以下几个请求头:-
Access-Control-Request-Method
:该字段必须携带,表示 CORS 请求使用的 HTTP 请求方法 -
Access-Control-Request-Headers
:可选字段,表示 CORS 请求发送的自定义头部信息,多个头部以逗号进行分隔
- 服务器收到浏览器发送的探测请求后,检测
Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
都在自己的许可名单时,就会允许跨域请求,返回响应。如下所示:
HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Max-Age: 1728000 Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain
响应主要包含如下请求头信息:
-
Access-Control-Allow-Origin
:表示允许进行跨域请求的域 -
Access-Control-Allow-Methods
:必须字段,表示允许 CORS 请求的方法 -
Access-Control-Allow-Headers
:表示允许 CORS 请求的头部 -
Access-Control-Max-Age
:表示探测请求缓存时间(单位:秒)
- 一旦浏览器通过探测请求,以后每次进行 CORS 请求时,就重复简单请求步骤(直至探测请求缓存过期)。
而如果探测请求通不过(即响应没有任何 CORS 相关的头部信息字段),浏览器就知道服务器会拒绝该 CORS 请求,于是就直接触发一个错误,回调给 AJAX 请求的onerror
方法。
- 浏览器检测到非简单 CORS 请求,则先发送一个探测请求,请求方式为
Spring Boot 配置支持 CORS
一个很幸运的事情就是:浏览器会自动帮我们完成 CORS 相关操作,用户完全无感知。
对于开发者来说,前端代码无需修改,如果是 CORS 请求,浏览器会自动帮我们加上相应的请求头进行请求...
因此,实现 CORS 通信需要配置的就只是服务器端。
下面介绍下在 Spring Boot 中配置 CORS 通信,主要介绍几种常用的配置方法,如下所示:
-
@CrossOrigin
:该注解可用于方法和类上,注解在方法上,表示对该方法的请求进行 CORS 校验,注解在类上(即Controller
上),表示该类内的方法都遵循该 CORS 校验。如下所示:注:前端页面 AJAX 请求源码可查看 附录 内容。
@Slf4j @RestController @RequestMapping("cors") @CrossOrigin( value = "http://127.0.0.1:5500", maxAge = 1800, allowedHeaders = "*") public class CorsController { @PostMapping("/") public String add(@RequestParam("name") String name, @RequestHeader("Origin") String origin) { log.info("Request Header ==> Origin: " + origin); return "add successfully: " + name; } @DeleteMapping("/{id}") public String delete(@PathVariable("id") Long id) { return String.valueOf(id) + " deleted!"; } }
上述代码在
Controller
类上使用@CrossOrigin
进行注解配置 CORS,这样前端页面就可以进行 CORS 请求当前Controller
下的所有接口。其中,
@CrossOrigin
注解可选参数如下:方法 作用 value
表示支持的域,即 Access-Control-Allow-Origin
的值origins
表示支持的域数组 methods
表示支持的 CORS 请求方法,即 Access-Control-Allow-Methods
的值。
其默认值与绑定的控制器方法一致maxAge
表示探测请求缓存时间(单位:秒),即 Access-Control-Max-Age
的值。
其默认值为1800
,也即 30 分钟allowedHeaders
表示允许的请求头,即 Access-Control-Allow-Headers
的值
默认情况下,支持所有请求头exposedHeaders
表示下发其他响应头字段给浏览器,即 Access-Control-Expose-Headers
的值。
默认不下发暴露字段allowCredentials
表示是否支持浏览器发送认证信息(比如 Cookie),即 Access-Control-Allow-Credentials
的值。
默认不支持接收认证信息 -
全局配置:如果想全局配置 CORS 通信,只需添加一个配置类。如下所示:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") //设置允许跨域的路径 .allowedOrigins("*") .allowedMethods("*") .allowedHeaders("*") .maxAge(1800) .allowCredentials(true); } }
只需创建一个配置类实现接口
WebMvcConfigurer
,然后覆写方法addCorsMappings
即可。
addCorsMappings
方法中,registry.addMapping
用于设置可以进行跨域请求的路径,比如/cors/**
表示路径/cors/
下的所有路由都支持 CORS 请求。其他的设置与注解@CrossOrigin
一样,无需介绍。注:这里也可以直接通过注入一个
WebMvcConfigurer
的 Bean 实例,自定义跨域规则:@Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS"); } }; }
-
通过
Filter
配置:通过过滤器Filter
可以让我们手动控制响应,自然就能完成 CORS 配置。如下所示:@Component public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, HEAD"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "access-control-allow-origin, authority, content-type, version-info, X-Requested-With"); filterChain.doFilter(servletRequest, servletResponse); } }
附录
-
CORS 前端页面 AJAX 请求源码如下所示:
注:前端页面运行在本地:
http://127.0.0.1:5500
-
Spring Security 配置跨域:如果项目中使用了 Spring Security 框架,那么也可以直接配置 Spring Security 支持跨域即可:
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 允许跨域资源请求 // by default uses a Bean by the name of corsConfigurationSource http.cors(Customizer.withDefaults()); } @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("*")); configuration.setAllowedMethods(Arrays.asList("GET","POST","OPTIONS")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 所有 url 都使用 configuration 定制的跨域规则 source.registerCorsConfiguration("/**", configuration); return source; } }
参考
- 阮一峰 - 跨域资源共享 CORS 详解
- 浏览器同源策略及跨域的解决方法
- Spring Boot+Vue全栈开发实战
- SpringBoot配置CORS解决跨域访问的几种实现方式