同源策略学习

同源策略简介

如果两个页面拥有相同的协议(http、https)、相同的端口、相同的 host,那么就可以认为这两个页面是同源的。即,同源策略就是同协议、端口、host 的三元组。

下标给出相对 http://store.company.com/dir/page.html 同源检测的实例:

URL 结果 原因
http://store.company.com/dir2/other.html true 只有路径不同
http://store.company.com/dir/inner/another.html true 只有路径不同
https://store.company.com/secure.html false 不同协议 ( https和http )
http://store.company.com:81/dir/etc.html false 不同端口 ( http:// 80是默认的)
http://news.company.com/dir/other.html false 不同域名 ( news和store )

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

对于 about:blankjavascript: 这种特殊的 URL,他们的源应当是继承加载他们的页面的源,因为这些类型的 URLs 没有明确包含有关原始服务器的信息。

HTTP 请求控制

同源策略控制了不同源之间的交互,例如在使用XMLHttpRequest<img> 标签时则会受到同源策略的约束。这些交互通常分为三类:

  • Cross-origin write (跨域写操作)。例如链接(links),重定向以及表单提交。通常允许。
  • Cross-origin embedding(跨域资源嵌入)。通常允许。
  • Cross-origin reads (跨域读操作)。通常不允许,但是可以通过内嵌资源来进行访问。

其中 Cross-origin embedding(跨域资源嵌入)包括:

  • <script src="..."></script> 标签嵌入跨域脚本。语法错误只能在同源脚本中捕捉到
  • <link rel="stylesheet" herf="..."> 标签嵌入 CSS。由于 CSS 的松散的语法规则, CSS 跨域需要设置正确的 Content-Type 消息头,不同浏览器有不同的限制
  • <img> 嵌入图片,支持的图片格式包括PNG、JPEG、GIF、BMP、SVG...
  • <video><audio> 嵌入多媒体资源
  • @font-face 引入的字体,一些浏览器允许跨域字体(cross-origin fonts),一些需要同源字体(same-origin fonts)
  • <frame><iframe> 载入的任何资源。站点可以使用 X-Frame-Options 消息头来组织这种形式的跨域交互

值得注意的是,上述跨域资源嵌入允许的都是尖括号内的内容,而不是标签之间的内容,如:

<script src="http://www.baidu.com/a.js"></script>

# http://www.baidu.com/a.js 在一对尖括号(<>) 内,所以允许跨域
<script>
var url = "http://www.baidu.com/abc.html"
$.get(url,function(result){
        alert(result)
    });
</script>

# 这次访问是在标签之间,是不允许的访问

跨域资源共享 - CORS

CORS 全称是跨域资源共享(Cross-origin resource sharing), 跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

使用场景: 通常在 AJAX 的 XMLHttpRequest 中使用。

两种请求

浏览器将 CORS 请求分为两类:简单请求(simple request) 和非简单请求(not-simple request)。

只要同时满足一下两大条件,就属于简单请求:

  1. 请求是一下三种方法之一:
    • HEAD
    • GET
    • POST
  2. HTTP 的头信息不超出一下几种字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type: 只限于三个值 application/x-www-form-urlencode、multipart/form-data、text/plain

不同时满足上述两个请求,就属于非简单请求,浏览器对这两种请求的处理不一样。

简单请求

简单请求的基本流程

对于简单请求,浏览器直接发出 CORS 请求。具体来说就是在头信息之中,浏览器在请求头中自动增加一个 Origin 字段:

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0..

Origin 字段用来说明本这次请求来自哪个源(协议 + 域名 + 端口),服务器根据这个值,决定是否同意这次请求。

如果 Origin 字段指定的域名不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段,就知道出错了,从而抛出一个错误,被 XMLHttpRequestonerror 回调函数捕获。并且,这种错误无法通过状态码识别,因为HTTP回应的状态码可能是 200。

如果 Origin 指定的域名在许可范围内,服务器返回的响应会多出几个头信息字段:

Access-Control-Allow-Origin:http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html;charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以 Access-Control-开头。

服务端(api.alice.com)的代码是:

<?php

if($_SERVER['HTTP_ORIGIN'] == "http://api.bob.com")
{

    header('Access-Control-Allow-Origin: http://api.bob.com');
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Allow-Expose-Headers: FooBar');
    header('Content-type: text/html;charset=utf-8');
}
else
{    
header('Content-Type: text/html');
echo "<html>";
echo "<head>";
echo "   <title>Not Allowed</title>";
echo "</head>";
echo "<body>",
    "<p>Not Allowed</p>",
"</body>",
"</html>";
}
?>

服务器返回的响应多出几个头信息字段解释:

  • Access-Control-Allow-Origin 该字段是必须的。它的值要么是请求时 Origin 字段的值,要么是一个 * 表示接受任意域名的请求
  • Access-Control-Allow-Credentials 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
  • Access-Control-Expose-Headers 该字段可选。 CORS 请求时,XMLHttpRequest 对象的 getResponseHeaders() 方法只能获得6个基本字段: Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想要获得其他字段就必须在 Access-Control-Expose-Headers 里面指定。上面的例子指定 getResponseHeader('FooBar') 可以获得 FooBar字段的值

withCredentials 属性

CORS 请求默认不发送 Cookie 和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定 Access-Control-Allow-Credentials字段

Access-Control-Allow-Credentials: true

另一方面,开发者必须在 AJAX 请求中打开 withCredentials 属性:

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

否则,即使服务器同意接受 Cookie,浏览器也不会发送。或者,服务器要求设置 Cookie,浏览器也不会处理。

如果忽略 withCredentials 设置,有的浏览器还是会一起发送 Cookie。这时可以显示关闭:

xhr.withCredentials = false;

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

非简单请求

预检请求

非简单请求是那种对服务器有特殊要求的请求,如请求方法是 putdelete,或者 content-typeapplication/json

非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检” (preflight)。

浏览器先查询服务器,当前网页所在域名是否在服务器的许可名单中,以及可以使用哪些 HTTP 动词和信息字段,只有得到肯定答复,浏览器才会发送正式的 XMLHttpRequest请求, 否则就报错。

例:

<script>
var url = 'http://api.alcie.com/cors'
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
</script>

上述代码中,HTTP 请求的方法是 PUT,并且发送一个自定义的头信息 X-Custom-Header。 浏览器发现这是一个非简单请求,就自动发送一个预检请求(preflight),要求服务器确定允许这样的请求,预检请求的HTTP头信息如下:

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

预检的请求方法为 OPTIONS,表示这个请求是用来询问的。关键字 Origin 表示请求来自哪个源。

除了 Origin 字段,预检请求的头信息包括两个特殊字段:

  • Access-Control-Request-Method,该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些HTTP方法,上例是PUT
  • Access-Control-Request-Headers,该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是 X-Custom-Header

预检请求的回应

服务器收到预检请求后,检查 OriginAccess-Control-Request-MethodAccess-Control-Request-Headers 字段后,确认允许跨源请求就可以做出回应:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

其中关键的是 Access-Control-Allow-Origin 字段,表示 http://api.bob.com 可以跨域请求数据。该字段也可以设为星号,表示同意任意跨域请求:

Access-Control-Allow-Origin: *

如果浏览器否定了预检请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror 回调函数捕获。

服务器回应的其他CORS相关字段及其含义如下:

  • Access-Control-Allow-Methods,该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求
  • Access-Control-Allow-Headers,如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段
  • Access-Control-Allow-Credentials,该字段与简单请求时的含义相同
  • Access-Control-Max-Age,该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求

浏览器的正常请求和回应

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

预检"请求之后,浏览器的正常CORS请求:

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。

服务器正常的回应:

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。



参考链接: