同源策略SOP与AJAX跨域

同源的定义

同源策略(Same Origin Policy)指出,如果协议、域名和端口对于两个页面是相同的,则两个页面是同源的。

如下和http://store.company.com/dir/page.html 比较,是否同源:

URL是否同源原因
http://store.company.com/dir2/other.html协议、端口、域名相同
http://store.company.com/dir/inner/another.html协议、端口、域名相同
https://store.company.com/secure.html协议不同
http://store.company.com:81/dir/etc.html端口不同
http://news.company.com/dir/other.html域名不同

同源的目的

同源策略指出从一个域上加载的脚本不允许访问另外一个域的文档属性,这是为了保证用户信息的安全,防止恶意的网站窃取数据。

假如用户访问了A网站,又去打开了B网站,如果没有同源的限制,B网站将能操控A网站的DOM,获取A网站的cookie信息,甚至模拟A站发送请求。

由此可见,”同源政策”是必需的,否则网站的资源将暴露,被非法网站利用。

跨域访问限制

跨域可以访问

  1. 页面中的链接重定向以及表单提交不会受到同源策略限制。
  2. 嵌入的资源,如JS <script src="..."></script>,CSS <link rel="stylesheet" href="..."><img><video><iframe>,也允许跨域嵌入。

跨域不能访问

  1. Cookie、LocalStorage 和 IndexedDB 无法读取。
  2. DOM 无法获得。
  3. AJAX 请求不能发送。

前俩点如何解决跨域问题不做过多说明,可以参考阮前辈的文章。http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html
这里主要讲些如何解决AJAX跨域问题。

AJAX跨域解决方案

JSONP

AJAX是基于XMLHttpRequest实现的,XMLHttpRequest遵从同源策略,也就不允许进行跨域请求。
为了解决跨域请求,Bob Ippolito在2005年提出JSONP的解决方案。

JSONP工作细节

JSONP(JSON with padding)是基于JS的动态嵌入实现的。上文也提到,当网页加载js资源时,是不受同源策略限制的,所以可以动态为网页添加js脚本,加载其他域的资源。

假如接口 http://localhost:7979/api/test
返回数据 It's so hot!

动态添加脚本:

1
2
3
4
script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'http://localhost:7979/api/test';
document.body.appendChild(script);

当js加载完成后:

1
2
3
<script>
It's so hot!
</script>

显然,这不是我们要的结果,我们希望调用方法render,将接口返回结果渲染到页面上,这时就轮到 callback 大显身手了。

动态添加脚本,将回调函数传到服务端:

1
2
3
script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'http://localhost:7979/api/test?callback=render';

当js加载完成后,服务端使用回调函数包裹结果:

1
2
3
<script>
render("It's so hot!");
</script>

这时回调函数自动执行,完成数据渲染。这就是JSONP的一次请求过程。

CORS(跨站资源共享)

CORS是w3c提出的标准,旨在解决 XMLHttpRequest的跨域问题,而不是像JSONP,绕过XMLHttpRequest,使用<script>

CORS工作原理

CORS标准新增了一组 HTTP 首部字段Access-Control-Allow-OriginAccess-Control-Allow-Methods等,用于控制哪些域名、哪些方法具有访问的权限。在发起真正的请求前,浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许跨域请求,在得到浏览器确认后,再发起真正的请求获取数据。

简单请求

然而,有些请求不会触发预检请求,我们称这样的请求为简单请求
满足下列条件的,视为简单请求。

使用下列方法之一:

  • GET
  • HEAD
  • POST

不得人为设置该集合之外的其他首部字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (的值属于下列之一)
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width

简单请求细节

simple

浏览器在发出请求时,会携带当前源信息Origin,服务器会根据Origin的值判断是否在许可范围内。

如果在许可范围内,会返回CORS特有的响应头信息。Access-Control-Allow-Origin即是CORS响应头信息的一员,该资源允许访问的外域。

简单请求成功示例(在线示例,需要翻墙)

如果不允许跨域请求或者不在许可范围内,响应状态码也可能是200,浏览器console会打印类似的错误信息:

1
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://Server-b.com' is therefore not allowed access.

简单请求失败示例

非简单请求细节

no-simple

非简单请求示例

非简单请求会触发预检请求。

使用OPTIONS方法发送预检请求时,将同时携带请求头Access-Control-Request-MethodAccess-Control-Request-Headers,分别告知服务器,实际的请求将会使用PUT方法,携带请求头信息authorization。
预检请求的响应头中,会返回Access-Control-Allow-MethodsAccess-Control-Allow-Headers,告知浏览器实际请求允许使用GET,POST等方法,允许携带authorization头信息。并通过Access-Control-Max-Age缓存预检请求,即在有效时间内,浏览器无须为同一请求再次发起预检请求。

只有在预检请求成功后,才会发送实际请求,实际请求仍会检查Origin

附带身份凭证的请求

对于跨域请求,默认浏览器不会发送身份凭证信息,即Cookie和HTTP认证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的特殊标志位withCredentialstrue

1
2
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

光设置XMLHttpRequest的xhr.withCredentials标志还不够,服务器响应头中需要返回Access-Control-Allow-Credentials: true告知浏览器,这是一个允许附带身份凭证的请求。

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

JSONP with CORS

JSONPCORS
请求方式只支持GET支持所有方式
同步异步只支持异步都支持
浏览器兼容所有浏览器现代浏览器,IE兼容到IE10,
IE8,9使用XDomainRequest勉强支持
安全性容易引发XSS攻击通过响应头保证安全

参考链接:

http://www.ruanyifeng.com/blog/2016/04/cors.html
https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
https://en.wikipedia.org/wiki/Cross-origin_resource_sharing