本文主要涉及三个关键词:
- 同源策略(Same-origin policy,简称 SOP)
- 跨站请求伪造(Cross-site request forgery,简称 CSRF)
- 跨域资源共享(Cross-Origin Resource Sharing,简称 CORS)
同源策略 SOP
同源
先解释何为同源:协议、域名、端口都一样,就是同源。
url | 同源 |
---|---|
https://niconico.com | 基准 |
https://niconico.com/spirit | o |
https://sub.niconico.com/spirit | x |
http://niconico.com/spirit | x |
https://niconico.com:8080/spirit | x |
限制
你之所以会遇到 跨域问题,正是因为 SOP 的各种限制。但是具体来说限制了什么呢?
如果你说 SOP 就是“限制非同源资源的获取”,这不对,最简单的例子是引用图片、css、js 文件等资源的时候就允许跨域。
如果你说 SOP 就是“禁止跨域请求”,这也不对,本质上 SOP 并不是禁止跨域请求,而是在请求后拦截了请求的回应。
其实表面上 SOP 分两种情况:
- 可以正常引用 iframe、图片等各种资源,但是限制对其内容进行操作
- 直接限制 ajax 请求,准确来说是限制操作 ajax 响应结果,这会引起后面说到的 CSRF
但是,本质上这两条是一样的:总之,对于非同源的资源,浏览器可以“直接使用”,但是程序员和用户不可以对这些数据进行操作,杜绝某些居心不良的行为。这就是现代安全浏览器对用户的保护之一。
下面是 3 个在实际应用中会遇到的例子:
- 使用 ajax 请求其他跨域 API,最常见的情况,前端新手噩梦
- iframe 与父页面交流(如 DOM 或变量的获取),出现率比较低,而且解决方法也好懂
- 对跨域图片(例如来源于
<img>
)进行操作,在 canvas 操作图片的时候会遇到这个问题
如果没有了 SOP:
- iframe 里的机密信息被肆意读取
- 更加肆意地进行 CSRF
- 接口被第三方滥用
绕过跨域
SOP 虽然让用户更安全,同时也会对程序员带来一定程度的麻烦,因为有时候业务上就是有跨域的需求。绕过跨域的方案由于篇幅所限,并且网上也很多相关文章,所以不在这里展开解决跨域的方案,只给出几个关键词:
对于 ajax
- 使用 JSONP
- 后端进行 CORS 配置
- 后端反向代理
- 使用 WebSocket
对于 iframe
- 使用 location.hash 或 window.name 进行信息交流
- 使用 postMessage
跨站请求伪造 CSRF
简述
CSRF(Cross-site request forgery)跨站请求伪造,是一种常见的攻击方式。是指 A 网站正常登陆后,cookie 正常保存登录信息,其他网站 B 通过某种方式调用 A 网站接口进行操作,A 的接口会在请求时会自动带上 cookie。
上面说了,SOP 可以通过 html 标签加载资源,而且 SOP 不阻止接口请求而是拦截请求结果,CSRF 恰恰占了这两个便宜。
对于 GET 请求,直接放到 <img>
就能神不知鬼不觉地请求跨域接口。
对于 POST 请求,很多例子都使用 form 提交:
代码语言:javascript复制<form action="<nowiki>http://bank.com/transfer.do</nowiki>" method="POST">
<input type="hidden" name="acct" value="MARIA" />
<input type="hidden" name="amount" value="100000" />
<input type="submit" value="View my pictures" />
</form>
所以 SOP 不能作为防范 CSRF 的方法。
回顾 SOP 的限制,这两个例子都是直接用 html 标签发起请求,而浏览器允许这么做,归根到底就是因为你无法用 js 直接操作获得的结果。
SOP 与 ajax
对于 ajax 请求,在获得数据之后你能肆意进行 js 操作。这时候虽然同源策略会阻止响应,但依然会发出请求。因为执行响应拦截的是浏览器而不是后端程序。事实上你的请求已经发到服务器并返回了结果,但是迫于安全策略,浏览器不允许你继续进行 js 操作,所以报出你熟悉的 blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
。
所以再强调一次,同源策略不能作为防范 CSRF 的方法。
不过可以防范 CSRF 的例外还是有的,浏览器并不是让所有请求都发送成功,上述情况仅限于简单请求,相关知识会在下面 CORS 一节详细解释。
CSRF 对策
SOP 被 CSRF 占了便宜,那真的是一无是处吗?
不是!是否记得 SOP 限制了 cookie 的命名区域,虽然请求会自动带上 cookies,但是攻击者无论如何还是无法直接获取 cookie 的内容本身。
所以应对 CSRF 有这样的思路:就是前后端分离常用的鉴权方法,使用 token 鉴权,并且不把 token 存放在 cookie,请求时在请求头上手动加上。
另一个方法是:cookie 里的东西,在发起请求时通过 query、body 或者 header 带上。请求到达服务器,那就不核对 cookie 传送的信息,只看自定义字段就好,如果正确,那一定是能看到 cookie 的本域发送的请求,CSRF 则做不到这一点。(这个方法用于前后端分离,后端渲染则可以直接写入到 dom 中)
示例代码如下:
代码语言:javascript复制var csrftoken = Cookies.get('csrfToken')
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return /^(GET|HEAD|OPTIONS|TRACE)$/.test(method)
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader('x-csrf-token', csrftoken)
}
},
})
跨域资源共享 CORS
跨域是浏览器限制,跨域资源共享(Cross-origin resource sharing)也是服务器与浏览器协调的结果。
如果服务器设置了 CORS 相关配置,在返回浏览器的请求头会加上 Access-Control-Allow-Origin
,浏览器看到这个字段的值与当前的源匹配,就会解锁跨域限制。
HTTP/1.1 200 OK
Date: Sun, 24 Apr 2016 12:43:39 GMT
Server: Apache
Access-Control-Allow-Origin: http://www.acceptmeplease.com
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: application/xml
Content-Length: 423
对于 CORS,请求分两种。
简单请求
- 请求方法使用 GET、POST 或 HEAD
- Content-Type 设为 application/x-www-form-urlencoded、multipart/form-data 或 text/plain
符合上面两个条件的都为 CORS 简单请求。简单请求都会直接发到服务器,会造成 CSRF。
预检请求
不符合简单请求要求的请求都需要先发送预检请求(Preflight Request)。浏览器会在真正请求前发送 OPTION 方法的请求向服务器询问当前源是否符合 CORS 目标,验证通过后才会发送正式请求。
例如使用 application/json 传参的 POST 请求就是非简单请求,会在预检中被拦截。
再例如使用 PUT 方法请求,也会发送预检请求。
上面提到的可以防范 CSRF 的例外,就是指预检请求。即使跨域成功请求预检,但真正请求并不能发出去,这就保证了 CSRF 无法成功。
CORS 与 cookie
与同域不同,用于跨域的 CORS 请求默认不发送 Cookie 和 HTTP 认证信息,前后端都要在配置中设定请求时带上 cookie。
这就是为什么在进行 CORS 请求时 axios 需要设置 withCredentials: true
。
下面是 node.js 的后台 koa 框架的 CORS 设置:
代码语言:javascript复制/**
* CORS middleware
*
* @param {Object} [options]
* - {String|Function(ctx)} origin `Access-Control-Allow-Origin`, default is request Origin header
* - {String|Array} allowMethods `Access-Control-Allow-Methods`, default is 'GET,HEAD,PUT,POST,DELETE,PATCH'
* - {String|Array} exposeHeaders `Access-Control-Expose-Headers`
* - {String|Array} allowHeaders `Access-Control-Allow-Headers`
* - {String|Number} maxAge `Access-Control-Max-Age` in seconds
* - {Boolean} credentials `Access-Control-Allow-Credentials`
* - {Boolean} keepHeadersOnError Add set headers to `err.header` if an error is thrown
* @return {Function} cors middleware
* @api public
*/
顺带一提,Access-Control-Allow-Credentials
设为 true
时,Access-Control-Allow-Origin
强制不能设为 *
,为了安全,也是挺麻烦。