Web 开发者必须要要知道的知识,怎么处理去处理跨域访问资源。很早之前就知道怎么去处理跨域,比如jsonp
,也知道今天准备梳理的知识:跨域资源共享(CORS
),但是还没有认真的去了解这种协议支持那些响应头,规则是怎么样的,只是知道Access-Control-Allow-Origin
这个头部。这次就来梳理一下。
CORS
先从概念入手,根据MDN
文档中,CORS
是 Cross-Origin Resource Sharing的缩写,也就是跨域资源共享。它是通过一些指定的 HTTP
头信息,来协调浏览器和服务器之间,怎么让一个允许在某个 origin(domain/域)下的 Web 应用,被允许访问不同源服务器上的资源的。
对于同源策略的规则可以通过文档来了解。根据 MDN 文档中的一个标注,可以注意到,译者标注 “并不一定是浏览器限制了发起跨域请求,也可能跨域请求可以正常发起,但是返回结果被浏览器拦截了。”
跨域资源共享( CORS )机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch )使用 CORS,以降低跨域 HTTP 请求所带来的风险。
通过上面最后一句话,说使用了CORS
是降低了跨域HTTP
请求的所带来的风险,就让我想到以前比价常用的jsonp
。
由于jsonp
是通过利用<script>
元素的这个开放策略,获取数据的形式如同加载了一个可执行的js
代码。于是这里就会存在代码注入的风险,加载的js
代码里面就可能会包含一些危险的脚本,比如获取cookie
得到用户的会话信息。所以一般来说,如果使用jsonp
的对象不是自己掌握的网站或者同公司的业务还是慎重。jsonp
还有一个缺点就是只能使用GET
,灵活性就少了点。
整个在网页中CORS
的过程都是由浏览器自己去完成的,当与到跨域请求的时候,浏览器会去携带相应的头部,所以一般来说CORS
一般技术实现的关注点在服务端。
简单请求和复杂请求
标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是
GET
以外的 HTTP 请求,或者搭配某些 MIME 类型的POST
请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。
这里说的简单请求和复杂请求,在规范里面是有明确规定的。如果一个请求被归为复杂请求那么它相对与简单请求会多一次预检请求。也就是如上所述,会先使用 OPTIONS 方法发起一个请求,服务器会返回支持的请求和一些其他信息。
看一下这两种请求的定义,是怎么区分的。
简单请求
对于使用 XMLHttpRqeuest
对象来请求,如下场景被认为是简单请求:
- 请求方法为其中几种:
- GET
- HEAD
- POST
- 请求头部字段 Content-Type 的值仅限于下列三者之一:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
还有两个需要注意的规定,不是头部的值,而是对象属性的设置:
- 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
- 请求中没有使用 ReadableStream 对象。
现在还有一个比较流行,用来做请求的对象fetch
有自己的额外要求:
- Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为:
- Accept
- Accept-Language
- Content-Language
- Content-Type (需要注意额外的限制)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
以上这些请求都会被认为是简单请求,也就是不会先使用 OPTIONS 方法去检查服务器是否支持。排除了以上的请求那就是 复杂请求 了,怪不得有时候会看到 OPTIONS 请求,当时看不懂,不知道为什么。现在知道来源了。
MDN
上有这样的一个例子:
假如站点 foo.example 的网页应用想要访问 bar.other 的资源。foo.example 的网页中可能包含类似于下面的 JavaScript 代码:
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
,来表明发起请求的网站来源哪里。(我去查了一下,Origin
这个字段浏览器是拒绝修改的,直接设置这个请求头是不生效的。)服务端的响应头部会有一个Access-Control-Allow-Origin
字段,来表明资源允许哪些网站访问,这里为*
则表明任何网站都可以。
预检测
当我们发送不是简单请求的时候,通过 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);
}
}
复制代码
来看一下请求头和响应头:
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
...
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
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
...
复制代码
OPTIONS 主要是把请求端准备需要使用到的方法等,发送给服务端检查,是否支持。服务端如果支持,这把相应支持的值全部列出来,注意这里是全部列出来,是因为避免需要多次检查。这里可以注意到服务端还有一个字段 Access-Control-Max-Age
,它的值是整数代表多少毫秒,它的作用就是告诉请求者,我返回给你的检查信息需要多久重新更新一次,也就是重新请求一下。
当预检查请求有返回对应的字段和值,那么浏览器就会马上将真实的请求发送出去。如果没有对应的值,浏览器会在控制台打印相应的错误信息,回调 XMLHttpRequest
对象的 onerror
函数,但是需要注意一点,这时候的状态码仍然可能是 200, 所以我们不能通过状态码来判断跨域请求是否成功。
cookie 和 身份认证
一般而言,对于跨域 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的某个特殊标志位。这个特殊的标志位 XMLHttpRequest 和 fetch 有点不一样。XMLHttpRequest 中是字段 withCredentials
,值位 boolean
型,fetch 里面在请求的时候传入参数 { credentials: 'include', ...}
。有些浏览器可能默认是带 cookie
或者认证信息的,针对这种情况,如果不需要带的话,可以将这个字段手动设置为 false
。
想要能带上这些认证信息,除了请求的时候需要设置 credentials ,服务端也需要在响应头上设置Access-Control-Allow-Credentials: true
,也就是允许携带认证信息。如果没有这值,浏览器将不会把响应内容返回给请求的发送者。注意,这里是数据已经返回但是浏览器给拦截了。
有了上诉两点,还有一个需要规避的情况(真的是条件多。。。安全问题一大难题),如果服务端将Access-Control-Allow-Origin
设置为*
(通配符),请求仍然会失败,也会被拦截并且抛出异常。
响应字段 和 请求字段
通过上面的描述,大致理清楚了规定和用法,下面列举一下CORS包含哪些响应字段和请求字段。
响应字段
-
Access-Control-Allow-Origin 语法是遮掩的:
Access-Control-Allow-Origin: <origin> | *
,可以设置多个 origin ,以逗号隔开就行。 根据语义上理解,就是允许那些 origin 能够访问资源。 -
Access-Control-Expose-Headers 跨域访问中,请求者一般只能拿到(XMLHttpRequest对象的getResponseHeader()方法)最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果想要拿到其他响应头就可以要服务端设置该头部。
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
复制代码
-
Access-Control-Max-Age 这个字段之前说过,来表明 预检查 过期时间,当超过这个时间后,浏览器会在复杂请求时会再次使用 OPTIONS 方法获取支持信息,否则还是使用上次请求获取的信息。
-
Access-Control-Allow-Credentials 指定请求者是否能携带 cookie,其实就是告诉浏览器如果请求者带了 cookie 但是我不需要,浏览器你就给它抛一个异常吧,并且当前和你的通信就不要返回给它了。
Access-Control-Allow-Credentials: true
复制代码
- Access-Control-Allow-Methods 用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。
Access-Control-Allow-Methods: <method>[, <method>]*
复制代码
- Access-Control-Allow-Headers 用于预检请求的响应。其指明了实际请求中允许携带的首部字段。
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
复制代码
请求头部
这些字段无需开发者自己设置,全部有浏览器自动带上。
-
Origin 表明请求的来源,有时候这值可能是空字符串,文档中说为
data URL
的时候为空,我查了一下,data URL
是类似于如下请求地址:data:,Hello%2C%20World!
简单的 text/plain 类型数据;data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D
上一条示例的 base64 编码版本; 我们常见的比如图片的 base64 的表现形式。 -
Access-Control-Request-Method
-
Access-Control-Request-Headers 这两个字段和响应字段是一样的意思,表示想要支持什么样的方法和头部信息。
小结
梳理了一下 CORS 相关的知识,终于知道有时候发请求的时候一些额外的字段信息是什么了。我任务 CORS 中这些头部信息一定需要知道,再以后有一些跨域的情况,就可以让服务端好好的配合了。