跨站资源共享CORS原理深度解析
我相信如果你写过前后端分离的web应用程序,或者写过一些ajax请求调用,你可能会遇到过CORS错误。
- CORS是什么?
- 它与安全性有关吗?
- 为什么要有CORS?它解决了什么目的?
- CORS是怎样运行的?
如果您有这些问题,那么这篇文章非常适合您。
一、什么是CORS?
要了解什么是CORS(Cross-Origin Resource Sharing:跨站资源共享),首先我们需要了解什么是同源策略Same Origin Policy
(SOP)。SOP是所有的现代浏览器都具备的安全措施,它不允许从一个加载的js脚本和资源的Origin域与另一个Origin域进行交互。换句话说,如果您的网站是www.example.com
,则您无法向www.test.com
发出XHR请求。
那么SOP有什么用?如果没有同源策略的限制,你想想会发生什么? 比如:您已经登录到微博,并且不小心打开了一个恶意网站。该网站可以向微博发出请求,并从您微博登录的会话中提取个人信息。这显然是巨大的安全问题,为了防止这种情况,在浏览器中实施同源策略的限制。实际上,服务器并没有意识到在浏览器端发生的这一切,您仍然可以使用curl或postman发出相同的请求,并且一切响应正常,因为这些工具上没有SOP。
如果说SOP是限制跨源访问的一种方式,那么CORS是一种绕过SOP限制并允许您的前端向服务器提出合法请求的方法。 如果您的服务端的确是存在跨域的情况(实际上对于现代分布式应用,这很常见),由于SOP限制您的客户端将无法向多节点跨域服务器发出xhr请求。救星就出现了,CORS使我们能够以安全且可管理的方式做到跨域请求,突破同源策略的限制。
二、同源策略的源(Same Origin Policy的Origin)
源由三部分组成:协议,hostip(域)和端口。例如
-
http://example.com/xxx/index.html
和http://example.com/yyy/index.html
是同源, -
http://example.com:80
和http://example.com
(对于http默认端口为80)是同源。 - 由于协议不同,
http://example.com/app1
和https://example.com/app2
是不同的源。 -
http://example.com
,http://www.example.com
由于域名不同,也是不同的源 - 非常要注意的是
http://localhost
和http://127.0.0.1
是不同的源
同源策略就是:不允许不同的ip、端口、协议的应用在浏览器内进行互相资源共享、请求调用。
三、CORS如何运作?
CORS规范允许服务器向浏览器返回一些HTTP Headers,浏览器可以基于这些HTTP Headers来决定是否突破SOP的限制。最主要的一个HTTP Headers是Access-Control-Allow-Origin。
//目标服务允许所有的网站对其进行跨域访问
Access-Control-Allow-Origin: *
//目标服务允许特定的网站对其进行跨域访问
Access-Control-Allow-Origin: https://example.com
CORS有两种类型的请求:“simple”简单请求和“preflight”预检请求,根据请求方法的不同由浏览器确定使用哪种请求。
simple简单请求:
如果符合以下所有条件,则API请求被视为简单请求:
- API方法是以下方法之一:GET,POST或HEAD。
-
Content-Type
请求头包含:application/x-www-form-urlencoded
,multipart/form-data
,text/plain
这两个条件将构成大多数简单请求的用例,但是可以在此处找到更详细的简单请求条件列表。
如果您的API请求被视为simple
简单请求,这个请求就可以直接被发送给服务器。服务器使用CORS HTTP Headers进行响应,浏览器将检查Access-Control-Allow-Origin
后决定这个请求是否可以突破同源策略的限制,进行下一步的处理。
preflight预检请求:
如果您的API请求不满足成为简单请求的标准(最常见不满足简单请求标准的Content-Type
值为application/json
),则浏览器将在发送实际请求之前发出预检请求。
举一个例子,我们尝试使用GET
请求https://example.com/status
,Content-Type
是application/json
,所以浏览器认为它不符合一个简单请求的标准,因此浏览器会在发出实际请求之前发出预检请求,这个预检请求是使用HTTP的 OPTIONS方法发出的:
curl --location --request OPTIONS 'http://example.com/status' \
--header 'Access-Control-Request-Method: GET' \
--header 'Access-Control-Request-Headers: Content-Type, Accept' \
--header 'Origin: http://test.com'
上面的curl就是模拟预检请求,实际作用是:浏览器希望告诉服务器,我的实际请求将使用HTTP GET
method进行调用,Content-Type
与Accept
作为HTTP headers,这个请求是从https://test.com
发起的。服务器响应此请求:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: OPTIONS, GET, HEAD, POST
Access-Control-Allow-Headers: Content-Type, Accept
-
Access-Control-Allow-Origin
:允许发出请求的源,或者*
可以从任何来源发出请求。(即允许跨域的源) -
Access-Control-Allow-Methods
:允许的以逗号分隔的HTTP方法列表。(即允许跨域的HTTP方法) -
Access-Control-Allow-Headers
:允许发送的HTTP headers列表。
浏览器收到服务端的预检请求响应之后,在我们的示例中服务器响应*
可以从任何来源发出请求,因此现在浏览器将再次访问https://example.com/status
,使用GET方法(不再是OPTIONS方法),浏览器将不再限制该请求的发出与响应数据的接收。
如果预检请求响应的Origin是特定的Access-Control-Allow-Origin: http://domain.com
,浏览器将出现Cross-Origin Request Blocked
错误。因为服务器端预检结果只允许http://domain.com
发出跨域请求,不允许其他应用向我发出跨域请求。
四、如何处理CORS错误
我们现在知道什么是CORS及其工作原理,后面的事情其实就简单了。从上面的内容我们需要注意的是,对CORS的完全控制权在服务器,即服务器可以允许或禁止源的跨域访问。所以说跨域问题的处理一般都在服务端进行,不同的服务端的处理HTTP 请求头的代码是不一样的,当然也可以不用写代码,比如:nginx、haproxy设置。但是万变不离其宗:最终都是对HTTP Headers进行重写。
我就简单的举几个例子:
比如Servlet处理跨域
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) resp;
response.setHeader("Access-Control-Allow-Origin", "*"); //解决跨域访问报错
response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
chain.doFilter(req, resp);
}
比如Spring MVC配置
@Configuration
public class GlobalCorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //添加映射路径,“/**”表示对所有的路径实行全局跨域访问权限的设置
.allowedOrigins("*") //开放哪些ip、端口、域名的访问权限
.allowCredentials(true) //是否允许发送Cookie信息
.allowedMethods("GET","POST", "PUT", "DELETE") //开放哪些Http方法,允许跨域访问
.allowedHeaders("*") //允许HTTP请求中的携带哪些Header信息
.exposedHeaders("*"); //暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
}
};
}
}
欢迎关注我的博客,里面有很多精品合集
- 本文转载注明出处(必须带连接,不能只转文字):字母哥博客。
觉得对您有帮助的话,帮我点赞、分享!您的支持是我不竭的创作动力! 。另外,笔者最近一段时间输出了如下的精品内容,期待您的关注。