跨域请求那些事
何为跨域请求(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
- 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检测和拦截在前,应用层设置跨域允许在后,所以结论是不可行。