HTTP访问控制
今天来继续学习MDN Web开发技术基础之HTTP的部分内容。在前端面试中,HTTP部分除了缓存,问的最多可能就是跨域问题,无非关注跨域问题产生的原因,如何解决跨域问题等。
同源策略与跨源访问
在了解访问控制之前,我们要了解为什么设置一个控制机制,在访问什么资源的时候要进行何种控制。同源策略是浏览器的一种基本安全策略,但是实际的需求往往还是需要跨源访问资源的,进行安全可靠的跨源资源访问,也是Web开发需要注意的内容。
浏览器的同源策略
首先我们了解一下Web安全的一个重要概念—同源策略。
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
简单来说就是访问a.com,返回的文档和脚本中,如果请求b.com的资源,将受到浏览器的策略管理,防止网络拦截修改了站点资源加载了恶意资源。
而“同源”指的是协议+主机+端口相同的资源。例如http://a.com和https://a.com,由于协议不同而不属于同源;再如http://a.com和http://b.com,由于主机不同而不属于同源;再如http://a.com和http://a.com:81,由于端口不同也不属于同源。而http://a.com/1/2.html和http://a.com/2/3.html满足“协议/主机/端口”三元组属于同源。
注意!IE同源限制不包含端口,且互信的域名不限制。
通过about:blank
打开的窗口与打开者算作一个源,因此其包含的代码算在打开者的作用范围,以此来讨论是否跨域。
注意!通过document.domain
修改当前页面的域时,只能修改为当前域或父域,然后后续的同源检测将以这个设置成功的域进行。比如http://b.a.com/1/2.html中使用脚本document.domain=“a.com”
,后续的代码将通过http://a.com/1/3.html的资源请求,认为是同源。但是2.html并不能执行document.domain=“b.com”
这样的代码。另外就是要注意document.domain=“a.com”
这样的操作会将端口号覆盖为null,要么指明端口,要么确保两个域端口都是null。修改域的操作一般用在让子域访问父域的资源。
跨源网络访问
为了安全起见,浏览器要求同源之间的资源安全访问,所以同源策略会限制所有的,尝试在资源中访问其他资源的行为。比如异步请求API XMLHttpRequest发送的请求和img标签所加载的资源。
- 通常同源策略允许跨源的写操作,比如表单提交。
- 也允许跨源资源嵌入。比如script标签嵌入跨域脚本,link标签嵌入跨域css(Content-Type头可能需要处理一下),img标签嵌入跨域图片资源,其他的还有video,audio,object,embed,applet等标签,css中的@font-face引入字体(这个特性不同浏览器会不同),另外frame标签和iframe标签在根站点没有X-Frame-Options的情况下可以加载跨域站点资源。
- 不允许跨源读操作。
!!!那么在同源策略的限制下,就完全不允许访问跨域资源了吗?那就要继续讨论我们的CORS。上面也说到了,同源策略下跨域访问也分允许和不允许,如果连那些允许的内容也想禁止掉该怎么办:
- 如果要拒绝跨域写操作,也就是拒绝跨域的表单提交等,可以使用传说中的CSRF token。这里简单提一下跨站请求攻击,通过一些方法让用户发起一个GET请求(通常),会携带用户浏览器的绘画信息,让用户在不知道这个请求作用的前提下执行了某项需要登陆权限的操作。这种攻击能够成功的原因,一方面是因为用户的凭证存在cookie,使用GET请求有时候不需要脚本(让用户点开一个攻击链接),所以要防御这种攻击,可以通过referer字段,保证请求是从制定源发起的,或者使用CSRF token。设置这个token是为了和cookie中的凭证区分开,往往是存在于资源的内部,所以攻击者无法提前知道。
- 如果要拒绝跨域读操作,要确保不能被嵌入。
- 如果要拒绝跨域嵌入,根据官方文档的描述,貌似没有办法。这里要说一下,浏览器在处理HTML文档中的script标签时,通常不会按照Content-Type来处理,而是直接当作JavaScript脚本来处理,所以如果你的script的src指向一个不是脚本文件的资源,浏览器也会按照脚本来解析。
跨源脚本API访问
在浏览器环境下,有一些API可能会引起不同源文档的关联,比如在某页面通过window.open来打开另一个源的文档,这个时候window和location对象的访问也会有响应的限制。由于打开者和被打开者不是同源资源,但是window.blur和window.close等操作是允许跨源使用,另外还可以注意window.postMessage也是可以的。而Location对象只能用replace,具体还要看浏览器。
跨域资源共享CORS
由于同源策略的限制,处于安全考虑往往不允许跨源资源的完全访问,而跨域资源共享机制,通过HTTP头部信息,在HTTP层面可信任的访问不同源的资源。或者说浏览器的同源策略,是建立在HTTP访问控制的基础上,放通某些操作,同时阻挡某些操作。注意!这里我读到一句话,某些不被允许的跨域请求,有可能是在发送层面被阻止,也有可能是在响应层面被阻止。
具体到跨源读操作,浏览器的同源策略,限制跨源HTTP请求,也就是XMLHttpRequest以及新的Fetch API,而限制的方式是:使用这些API请求资源的脚本等,这里统称为Web应用,仅限于从加载这个应用的域请求HTTP资源,除非响应能够正确包含CORS头。所以正确的描述跨域问题,应该是跨域请求收到跨域资源共享机制的控制。
哪些行为需要CORS
跨域资源共享让以下情况可以进行跨域HTTP请求:
- XMLHttpRequest和Fetch API发起的跨域HTTP请求,需要在CORS指导下进行
- Web字体也受CORS指导,网站的字体资源需要保护,让已授权网站进行使用
- WebGL题图
- drawImage方法绘制图片和视频资源时,这些资源的访问收到CORS指导,也是为了防止资源被滥用
CORS原理
跨域资源共享机制通过HTTP头,请求-响应过程中,就能让浏览器知道该资源是不是允许访问,同时浏览器也可以根据共享机制来保证资源的可访问性。
功能概述
跨域资源共享机制,或者说规范,对HTTP首部信息进行了一些扩充,为的是能够指明共享的条件,比如什么样的资源可共享,可被什么样的客户端共享之类的。同时CORS主要针对GET以外的,可能对服务器产生影响的HTTP方法有要求,比如常用的异步请求方法POST,或者文件上传PUT,浏览器会用OPTIONS请求进行检查,因为OPTIONS请求得到的响应也包含跨域控制的头字段,浏览器就可以根据这些字段来决定是否进行同源控制。当然,发送OPTIONS请求时,服务器也知道浏览器的信息。
要注意的是,CORS发起的检测请求也是会失败的,但是代码中使用API发起的请求并没有真的执行,所以产生错误可能无法在代码中被处理。
使用场景
CORS要求跨域请求要先进行检查,但是某些“简单请求”不会这样。如果一个请求,方法是GET,HEAD或POST中的一种,Content-Type是text/plain,multipart/form-data,application/x-www-form-urlencoded中的一种,以及一些其他要求(具体可以参见文档),这样的请求(同时满足这些条件)被认为是“简单请求”,不会进行OPTIONS请求检查。
虽然简单请求不会进行检查,但是对于服务器来说,如果不满足要求也是得不到响应的,所以并不是跨过了同源策略的限制。
下面将使用官网的一个例子分析学习以下:
// 加入http://foo.example页面中如下脚本
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/public-data/';
function callOtherDomain() {
if(invocation) {
invocation.open('GET', url, true);
invocation.onreadystatechange = handler;
invocation.send();
}
}
产生的请求报文和响应报文如下:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[XML Data]
请求报文就不难发现,Origin包含的主机名和Host指明的主机名不相同,是一个跨域请求。同时包含各种Accept-头。响应头的一个重要字段Access-Control-Allow-Origin: *
,也就是说服务器接受的跨域访问来源没有限制,浏览器用请求头中的Origin和比较,发现是包含关系,也就是允许,所以这个跨域请求OK。
当然这个*是由服务器决定的,如果服务器认为这个接口只能对某些站点开放,比如本例中的foo.example,那么进行这样的设置Access-Control-Allow-Origin: http://foo.example
,这个跨域请求同样是OK的,因为Origin: http://foo.example
和“http://foo.example
”比较是匹配的。
最简单的跨域资源访问控制,由请求头中的Origin和响应头中的Access-Control-Allow-Origin进行匹配,Access-Control-Allow-Origin的值则只能是*(所有)或者是Origin字段指明的域名,即泛匹配和严格匹配。
OPTIONS请求
HTTP方法中有一个比较特殊的,OPTIONS,专门用于检查请求,目的是为了知道服务器是否支持即将发送的真是请求,如果不进行检查,跨域请求可能会产生不可预见的影响,这是一个为了保证请求安全的措施之一。
简单来说,不是“简单请求”的各种请求都需要进行检查,之前的例子中,虽然是跨域请求,但是没有特别的请求头,也不是特殊的请求方法,所以作为一个简单请求并没有进行检查。那下面在看一个因为包含了特殊请求头的跨域请求,引起了检查请求,并如何作用:
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '<?xml version="1.0"?><person><name>Arun</name></person>';
function callOtherDomain(){
if(invocation)
{
invocation.open('POST', url, true);
invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/xml');
invocation.onreadystatechange = handler;
invocation.send(body);
}
}
这个简单的例子要向一个不同源的服务器发送一个XML文档,同时设定了一个自定义请求头“X-PINGOTHER”,Content-Type也设定成“application/xml”,虽然是POST请求,但是仍然会触发OPTIONS请求进行检查。从流程上来说,浏览器会先发送一个OPTIONS请求,包信息是这样的:
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
有Access-Control-Request-Method: POST
和Access-Control-Request-Headers: X-PINGOTHER, Content-Type
信息,这两个就是要检查的内容,询问服务器支不支持POST方法,同时询问相关的请求头。假设得到以下的响应:
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://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
OPTIONS请求的响应包含了Access-Control-*的内容,回答了OPTIONS要检查的内容,服务器支持POST,GET和OPTIONS请求,支持X-PINGOTHER和Content-Type头部。Access-Control-Max-Age: 86400
这个信息告诉浏览器,在24小时内不用再检查这个请求,可以减少不必要的OPTIONS请求。当然浏览器自身也有自己的限制,可能不会完全按照这个字段来。
身份凭证问题
之前也说到浏览器同源策略是为了资源访问的安全,所以进行跨域请求的时候,浏览器自然也会很谨慎,所以一般来说是不会携带Cookie的,也就是一般用作身份凭证的信息。但是CORS既然是为了方便资源共享,自然也会可控的携带身份凭证,这就是传说中的withCredentials
设置。相应的,服务器也要处理这种情况,浏览器通过设置withCredentials
携带Cookie过去,但是服务器没有Access-Control-Allow-Credentials:true
设置,响应也是没法完成的。
这里还要格外注意一个问题,如果服务器设置了Access-Control-Allow-Origin:true
即允许任何其他域跨域访问,这时跨域请求携带Cookie会导致请求失败。想要正确携带Cookie信息必须要严格设置Access-Control-Allow-Origin的值。这大概也是因为Cookie和域应当严格对应的关系。
详细介绍一下相关首部的含义和作用
前面有很多跨域请求的例子,请求头和响应头都有一些CORS标准下专用的信息,负责安全的进行跨域请求,有浏览器表明态度的信息,也有服务器的控制,下面来仔细看看。
Access-Control-Allow-Origin
响应首部
可能值:<origin>|*
这个字段指定了哪些外域可以访问该资源。特别说明:不需要携带身份凭证的资源,可以设置为“*”通配符,否则必须要指明外域。
另外,设置了具体的外域值,响应的Vary
字段(这个字段在HTTP缓存中有介绍)必须包含Origin,也就是说虽然资源接受外域的请求,但是第一次从a.com触发,返回了Vary字段包含Origin,使得第二次从b.com触发时,不会错误的使用缓存。
Access-Control-Expose-Headers
响应首部
可能值:各种响应头
以XMLHttpRequest实例来说,getResponseHeade()获取响应头信息,只包含一些基本的Cache-Control,Content-Type等,Access-Control-Expose-Headers响应头可以让脚本获取更多其他的头部信息。这个头信息是为了保证服务器自定义头部能正确不读取到。
Access-Control-Max-Age
响应头部
可能值:<秒数>
在OPTIONS请求的部分遇到过这个响应头,用于表达检查请求有效性。如果有这个字段,不用每次都进行检查。
Access-Control-Allow-Credentials
响应头部
可能值:true
浏览器通过设置withCredentials携带了Cookie,服务器则通过设置这个头部,让浏览器可否读响应。如果OPTIONS请求的响应包含该头部,表示实际请求时可以携带Cookie。如果GET简单请求携带了Cookie,但服务器响应没有这个字段,响应就会被忽略掉了,浏览器也不会把内容返回出来。
Access-Control-Allow-Methods
响应头部
可能值:<method>[,<method>]*
包含一个或多个支持的HTTP方法,用于OPTIONS请求的响应,告诉浏览器服务器支持何种类型的HTTP方法。
Access-Control-Allow-Headers
响应头部
可能值:<field-name>[,<field-name>]*
包含一个或多个允许的请求头部,用于OPTIONS请求的响应,告诉浏览器实际请求中,服务器允许携带的头部字段。
Origin
请求头部
可能值:<origin>
这个字段就是直接表明源的,内容是源站的URI,没有路径信息(因为不重要),表达源的概念依靠服务器名称或者叫主机名称就够了。
不过要注意的是这个字段可能为空,当且仅当源站是一个data URL。这个字段是基本的请求头,不管是不是跨域请求,总会携带这个字段表明来源。
Access-Control-Request-Method
请求头部
可能值:<method>
用于OPTIONS请求头部,告知服务器真正请求的方法
Access-Control-Request-Headers
请求头部
可能值:<field-name>[,<field-name>]*
用于OPTIONS请求头部,告知服务器真正请求会包含的头部信息。
总结
“跨域”本身描述的是一种情况,从一个源访问另一个源的资源,或者说从一个域要访问另一个域的资源,这种情况是跨域。所以跨域发生在具体什么情况:资源跳转型(表单提交,重定向)、嵌入型(script和img标签等)和异步请求(ajax相关)。同源策略则是针对跨域情形的限制-放通策略。如果非要进行跨域访问,嵌入hack—jsonp,或者利用一些同源策略下API的放通性—postMessage+window.open之类。标准一点的方式就是利用HTTP访问控制,在资源共享策略的帮助下进行发起安全的跨域请求,服务器也允许,自然就可以访问跨域资源。除此之外nginx代理,node转发算是在服务器上进行“域的扩大”,在客户端的表现上就看不出是跨域了,Origin和Host是一样的。而一个比较有争议的方式WebSocket协议,是否算是跨域,确实可以链接不同源的websocket服务并获取数据,但WebSocket协议是GET请求升级上来,理论上又算是简单请求,算是在CORS下的正常请求,所以这里也没有具体讨论。
本文并没有介绍和具体举例jsonp,或者iframe传值,毕竟这种方式对于服务器端的逻辑有特殊要求,客户端脚本也会有限制,比如jsonp制定的callback,在现在的开发模式中几乎没法使用。所以用比较标准,对客户端服务器端都友好的方式,是本文介绍的目的。
当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »