缘起
- 关于跨域问题,听得多,但一直未曾梳理。
- 最近因为跨站 POST 无法上传图片,初识 cors,折腾多次;因认识不深而存在非认证即可上传安全漏洞;
- 7月11-12日关于 ajax 处理 502 错误页面跳转问题(bug#692)又折腾了一次;
- 7月19日又发现 IE8&9 存在兼容问题,因为 IE 是通过 XDomainRequest object 来支持 cors 的,幸好 MoonScript 帮助实现了在 ie8&9 下和其他浏览器一样进行跨站访问;但是不幸的是,不能携带 cookies,也就是说和认证有关的都不行;Eric Law 的文章 《XDomainRequest – Restrictions, Limitations and Workarounds》 揭示了这一点;
- NoteCode 首先应用 cors,让我们解脱了 JSONP,同时在前后端代码上理清了很多问题。
什么是 CORS?
假设有两个站点,A 站是一个应用站点,S 站是一个服务站点(比如 API 站点);A 站的 js(ajax) 访问 S 站提供的服务或者资源,这样的请求就是 cors 请求(Cross-Origin Resource Sharing 跨站资源共享)。
请移步 HTTP access control (CORS) @ mozilla.org、 Use Cases and Design Decision FAQ @ w3 wiki 查看详细。
Origin 请求头是 cors 请求的一个简单标识
简单地说,cors 请求的一个明显标识就是在请求中含有 Origin:
请求头。
Origin:
请求头和 Access-Control-Allow-Origin:
响应头组成了 cors 的一个最简单用法;
- 实现了 CORS 协议的浏览器会自动发送
Origin:
请求头; - 实现了 CORS 协议的服务端如果允许
Origin:
指定的站点访问,则在回应请求头Access-Control-Allow-Origin:
中包含这个站点即可;
预请求(Preflight request)和 实际请求(Actual request)
这是两类请求,一般简单请求通常并不需要事先搞一个 Preflight 请求,只有使用了特殊的 Content-Type、有非标准的或者自定义的 request header 时,浏览器才会在提起实际请求前首先发送 Preflight 请求。
何时 preflight?
满足以上所列条件的跨站点请求就是简单请求,不需要事先 preflight- preflight 以 OPTIONS 方法发起请求
目的在于发起实际业务请求之前,向服务端事先了解对跨站点访问控制策略的实施情况; - preflight 请求头
- Origin: 应用站点,类似:http://www.example.com
- Access-Control-Request-Method: 列出实际请求希望使用的HTTP方法;
- Access-Control-Request-Headers:
Access-Control-Request-Headers:
列出自定义的 header;如果 Content-Type 值为自定义的,也需要将 Content-Type 列入; - preflight 请求头示例
Origin: http://www.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
- preflight 响应头示例
Access-Control-Allow-Origin: http://www.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400 指明对于 OPTIONS 请求的缓存时长;
实际请求(Actual Request)
- 请求头
Origin: http://www.example.com
Cookie: ...
Requests with credentials 时(XMLHttpRequest.withCredentials = true;),在请求头中会携带上 Cookie:
请求头;
这对于提供服务的 S 站来说,是一个重要且简捷的地方,S 站可以管理自己域下的 Cookie,就可以避免在主域名下种 cookie 而导致的种种问题。
应用站点和服务站点是同一个主域名时,这是一个简单做法。
比如:WWW 应用站点收集用户登录信息(简单如用户名/密码),通过 AJAX 到 API 服务站点进行认证后访问服务站点的资源,后续所有 API 操作都需要带上 Cookie 等凭证信息。
- 响应头
Access-Control-Allow-Origin: http://www.example.com
Access-Control-Allow-Credentials: true
-
错误提示
假如在 Preflight 时,`Access-Control-Allow-Headers` 没有包含实际请求所提交的相关请求头,则会报告错误提示
dropzone.js 的预请求问题
dropzone 是一个支持拖拽的文件上传 js 库,比如图片上传同源策略(Same Origin Policy)
同源策略是客户端脚本(javascript)的重要安全度量标准。所有浏览器都遵循这个标准。例如:XMLHttpRequest 请求就只能访问源站的资源,也就是说 A 站的 js 只能访问 A 站的资源;如果访问其他网站资源,浏览器会拒绝将返回的内容传递给 js,目的在于避免 A 站的恶意脚本窃取 B 站页面的敏感信息;
关于跨站请求的可能的误解
浏览器并不会阻止 AJAX 发起跨站请求,浏览器也会正常接收服务端的返回内容,只不过为了履行安全策略,浏览器并不会向上层调用者返回 AJAX 响应内容,也就是说返回结果被浏览器拦截了;
我们在处理 bug# 692 时就误以为 jQuery 有问题,Google 了半天,在 stackoverflow 上才有一个提示说可能是 CORS 问题;
使用 curl 调试 cors 请求(OPTIONS 请求示例)
curl -I --verbose -X OPTIONS \
-H "Origin: http://www.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: X-Requested-With" \
http://api.example.com/index.php?r=j
服务端 PHP 代码示例
if(isset($_SERVER['HTTP_ORIGIN']) && stripos($_SERVER['HTTP_ORIGIN'], APP_TOP_DOMAIN) !== false){
header('Access-Control-Allow-Origin: '.$_SERVER['HTTP_ORIGIN']);
header('Access-Control-Allow-Credentials: true');
if($_SERVER['REQUEST_METHOD']=='OPTIONS'){
header('Access-Control-Allow-Headers: Content-Type, Cache-Control, X-Requested-With');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Max-Age: 86400');
}
}
header('Access-Control-Expose-Headers: Date');
关于 cors 的缓存
Last-Modified:Wed, 17 Aug 2016 16:00:00 GMT
Cache-Control:public, max-age=86400
-
Vary: Origin 问题
未标识 `Vary: Origin` 导致的问题
fonts 需要授权使用
fonts 要么放到同一个域名下,要么放到 cdn,但 需要 enabling CORS;
nginx 片段示例:add_header Access-Control-Allow-Origin *
;
参考
- Server-Side Access Control
- TR cors @ w3.org:W3C 的技术报告(TR)
- cors @ whatwg.org:解释简洁;
- rfc6454: The Web Origin Concept;
- http://enable-cors.org/
- http://www.cnblogs.com/yuzhongwusan/p/3677955.html
- upgrade-insecure-requests:stackoverflow discussion;
bug# 692 备注
- 前端 js 无法直接捕获 API 50X 错误;
服务端接收到了 jQuery AJAX 请求,也正确地返回了,但是 jQuery 就是没有正确返回调用,本来以为这是 jQuery 问题,其实不是; - 前端 js 只好通过超时来推导服务端出现问题;
- 超时发生时,直接跳到 502 页面;
网友评论