何为跨域请求(Cross-origin resource sharing)

在网页浏览器中,当属于站点A的网页试图访问位于站点B上的某一资源(如图片、CSS、JS文件、指定URL的接口数据),这个请求就属于跨域请求了。

通常情况下,站点A上的网页可以访问位于站点B上的图片、视频、CSS、JS等资源,但当站点A的网页向位于站点B的HTTP接口发送Ajax请求时,浏览器默认会检查该请求是否合法,意即:只有当站点B主动返回允许站点A访问该HTTP接口的许可信息时,站点A本次的Ajax请求才会被实际发送。从这一点可以看出,跨域请求的安全限制,实际是由浏览器保证并执行的。

浏览器对跨域的Ajax请求的处理流程图如下(资源来自维基百科 Flowchart_showing_Simple_and_Preflight_XHR.svg )

从上图中可以看出,一个Ajax请求无论是GET形式还是POST形式,只要包含了特定的内容数据,都会触发浏览器的安全校验,所谓的安全校验是指:浏览器在发起本次请求之前,会对同一URL优先发起一次OPTIONS请求,通过OPTION请求返回的Header信息判定本次的跨域Ajax请求是否合法,如果合法则请求被发出;否则请求被中止。

下方信息是从 hx.test.com 向 www.test.com/api/upload_img.html 发起Ajax POST请求时抓取到的、浏览器优先发起OPTIONS请求的关键信息

OPTIONS请求Header关键信息 说明
状态行 OPTIONS /api/upload_img.html HTTP/1.1   
Host www.test.com 站点B的域名
Origin  http://hx.test.com/ 站点A的网址,跨域请求需携带
Access-Control-Request-Method POST 跨域请求需携带的Header
Access-Control-Request-Headers

X-Requested-With

跨域请求需携带的Header
OPTIONS响应Header关键信息  
状态行 HTTP/1.1 200 OK  
Access-Control-Allow-Origin http://hx.test.com/ 站点B允许站点A访问本资源
若站点B设置该Header为 * ,则表示允许任意站点访问该资源
Access-Control-Allow-Headers Origin, X-Requested-With, Content-Type 站点A请求站点B时可携带的Headers
Access-Control-Allow-Methods POST, OPTIONS 站点A请求站点B时可使用的Methods

Access-Control-Allow-Credentials

true 站点A请求站点B时,可携带站点B的Cookies
若站点B未返回该Header,或将其设置为false,则站点A请求站点B不会携带站点B的Cookies)

综上可知,浏览器实现了W3C定义的安全约定,在站点跨域请求资源时,如果是Ajax请求并且请求的Header或Body中包含了触发安全校验的内容,则浏览器会优先发起(pre-flight)OPTIONS请求向目标站点询问:是否接受站点A的Ajax请求,以及可接受的请求内容(包括限定的Methods、Headers等)。最后再由浏览器检查核对后决定,允许并发起请求,或者,中止本次请求。

关于触发浏览器安全校验(pre-flight)的条件详情,请参考火狐开发者文档:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests

如何界定请求跨域了

假如站点A的网页为:http://www.test.com ,请看下方界定请求跨域的例子

站点B列表 是否跨域 原因
http://www.test.com/dir/page2.html 未跨域 Same protocol, host and port
http://www.test.com/dir2/other.html 未跨域 Same protocol, host and port
http://username:password@www.test.com/dir2/other.html 未跨域 Same protocol, host and port
http://www.test.com:81/dir/other.html 已跨域 Same protocol and host but different port
https://www.test.com/dir/other.html 已跨域 Different protocol
http://en.test.com/dir/other.html 已跨域 Different host
http://test.com/dir/other.html 已跨域 Different host (exact match required)
http://v2.www.test.com/dir/other.html 已跨域 Different host (exact match required)
http://www.test.com:80/dir/other.html 看浏览器实现 Port explicit. Depends on implementation in browser

PHP CodeIgniter框架对于跨域请求的注意点

随着业务线日益壮大,我也在日常的开发中遇到过几次跨域、跨站点请求(实则都为公司的子业务系统)失败、被阻的情况。

当遇到跨域请求失败的情况时,只需在后端代码中添加跨域允许的相关Header键值,即可通过浏览器的检查。

除此之外,PHP的CodeIgniter框架还对跨站请求伪造(CSRF)进行了防御,下面针对这种情况做一下说明。

CSRF的实现逻辑是:

  • 1)可信用户A登录了test.com;
  • 2)可信用户A访问危险网页或危险邮件,网页或邮件中隐藏着向test.com提交的表单内容(表单内容为普通form,由攻击发起者预先写死格式和内容);
  • 3)可信用户A被诱导点击了危险网页或邮件中的隐藏的form表单,导致表单内容被用户A提交至test.com,并且表单提交时携带的是可信用户A的可信Cookies
CodeIgniter对此做出的防御逻辑是:
  • 1)每一次POST请求之前,客户端应首先发起GET请求拉取可信签名(csrf_defence_sign)并将签名放入Cookie中返回给客户端
  • 2)POST数据时,需将可信签名作为表单的一部分同时提交,并由后端校验表单中的可信签名是否有效(具体的校验逻辑为:检查请求Cookie中的可信签名与POST中的可信签名一致,且可信签名有效)

在公司做新的站点业务(testB.com)的时候,它共用了test.com的几乎所有代码,二者只是在域名上有区分,后端代码处理逻辑一模一样。由于需要从testB.com向test.com提交表单信息,此时CodeIgniter后端服务便误以为这些提交是CSRF攻击,因此直接中止了请求。

那么疑问来了:如果想从testB.com向test.com发送ajax跨域提交,是否可行?看下方流程图可知:由于CodeIgniter框架的CrossSiteRequestForgery检测和拦截在前,应用层设置跨域允许在后,所以结论是不可行。