前言
对于跨域, 随着 w3c 的 CORS 的出现, 相比较于有些年头的 JSONP,CORS 以其简单安全, 支持 post 的优势越来越收到大家的欢迎. 具体如何 CORS 的原理和实现, 直接推荐阮老师的文章 http://www.ruanyifeng.com/blog/2016/04/cors.html , 十分详细. 本文主要关注 CORS 实现过程中的几个疑惑点.
预检请求
背景
浏览器将 CORS 请求分成两类: 简单请求 (simple request) 和非简单请求(not-so-simple request).
简单请求
同时满足一下条件的即是简单请求:
请求方法是以下三种方法之一:
HEAD,GET,POST
HTTP 的头信息不超出以下几种字段
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
Content-Type: 只限于三个值 application/x-www-form,multipart/form-data,text/plain
非简单请求
显然, 不同时满足则为非简单请求(可以认为是复杂请求). 两者的差别在于复杂请求在与服务端交互时多了一次 options 的预检请求, 毕竟复杂请求一般就是 HTTP 请求头信息超出限制或者 method 为 put,delete 等操作行为, 处于安全考虑, 需要服务端先行验证来决定是否给予相关权限.
如下所示(示例来自阮老师文章):
- var url = 'http://api.alice.com/cors';
- var xhr = new XMLHttpRequest();
- // PUT method 为复杂请求, 要预检
- xhr.open('PUT', url, true);
- xhr.setRequestHeader('X-Custom-Header', 'value');
- xhr.send();
非简单请求, 浏览器自动发送 otpios 的预检请求, 请求头如下:
- OPTIONS /cors HTTP/1.1
- // 请求源
- Origin: http://api.bob.com
- // 必须字段, 指明正式 cors 请求将会使用那些 method
- 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...
对于预检信息, 服务端一般做了如下操作:
1, 检查 origin,Access-Control-Request-Method 和 Access-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
- // 允许额外 header
- 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
如果不允许跨域, 依然响应该请求, 不过不携带 CORS 相关的信息. 浏览器则会认为服务器不允许跨域, 触发错误.
- // 常见的跨域错误
- XMLHttpRequest cannot load http://api.alice.com.
- Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
到这里一个流程结束, 不过我们要关注的是 options 预检请求之后 code 返回的问题
options 成功之后, 返回 code 200 还是 204
常规预检的就是对于 options 的请求直接返回 code 200 的响应, 表示校验通过.
但是前两天发现有的返回为 code204. 两者之间的差别具体在哪呢.
常见用法
1, 针对特定接口支持 CORS 时, 在代码里加判断对于 options 返回 200
- // 随便找了段 java 代码
- if (req.getMethod().equals("OPTIONS")) {
- res.setStatus(200);
- }
2, 如果整个域名都支持 CORS, 可以再 nginx 侧直接配置, 此时常见的是返回 204.
- if ($request_method = 'OPTIONS') {
- add_header Access-Control-Allow-Origin *;
- add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
- #**** 省略...
- return 204;
- }
总结
两者之间的差别, 首先可以参考下 204 和 200 对应的含义(下面内容摘自 MDN).
200
请求成功, 成功的具体含义依据 http method 的不同而有所差别.:
GET: 资源已经被提取并在消息中文中传递
POST: 描述动作结果的资源在消息体中传输
204
服务器成功处理了请求, 但不需要返回任何实体内容, 并且希望返回更新了的元信息.
客户端是浏览器的 haul, 用户浏览器应保留发送了该请求的页面, 而不产生任何文档视图上的变化. 由于 204 响应被禁止包含任何消息体, 因此它始终以消息头后的第一个空行结尾.
简单总结, 204 返回表示请求成功, 并且无消息体, 优势在于节省网络请求.
具体到 options 请求, 选用哪一个.
贴切的来说, 应该像其他 options 请求一样为预检 optiosn 请求返回相同的 code 状态码, 相关规范不要求或者推荐其他内容.
fecth 请求
例如对于 Fetch 规范 https://fetch.spec.whatwg.org/ 要求 CORS 协议的 status 可以为 200-209 里面的任意值.
- If a CORS check for request and response returns success
- and response's status is an ok status,
- run these substeps.
如果 response 为一个 okstatus 就可以继续执行
An ok status is any status in the range 200 to 299, inclusive.
并不要求具体哪一个值.
所以从 fetch 来看, 两者均可选择.
HTTP 1.1
对于 http/1.1 规范来说, 有一章节专门定义了各种响应 code. 对于 2 开头的 2-XXcode https://tools.ietf.org/html/rfc7231#section-6.3 , 分别描述如下:
200
请求成功, 成功的具体含义依据 http method 的不同而有所差别.
GET: 资源已经被提取并在消息中文中传递
POST: 描述动作结果的资源在消息体中传输
OPTIONS: communications options 成功的表示
由上可知, 对于 options 预检请求的响应, 需要包含下面两种情况:
1, 表明请求成功
2, 描述通信选项(这里包括, Access-Control-Allow-Methods 和 Access-Control-Allow-Headers 这些响应头)
看起来, 上面就是 200 在 http 定义中的含义, 显然满足, 但是如果继续看 204 的含义, 好像也可以满足需求.
204
服务器成功处理了请求, 但不需要返回任何实体内容, 并且希望返回更新了的元信息.
客户端是浏览器的话, 用户浏览器应保留发送了该请求的页面, 而不产生任何文档视图上的变化. 由于 204 响应被禁止包含任何消息体, 因此它始终以消息头后的第一个空行结尾.
结论
首先两者都可以使用, 对于 200, 从定义而言更符合场景和定义. 但是 204 无消息体, 优势在于节省网络请求.
至于用哪个, 大家自行做下判断.
跨域 读取 cookie
作为常见的场景, cookie 一般会存放一些, 鉴权会话等信息. 对于 CORS 跨域, 默认的是不包含 cookie 的.
A cross-origin request by default does not bring any credentials (cookies or HTTP authentication)
如果要操作 cookie 需要分别从服务端和客户端两个场景来看.
客户端 request 携带 cookie
request 如果要携带 cookie, 需要特定参数指明. 可能看到过这个参数为 credentials 或者 withCredentials, 什么时候用两者呢. 主要跟请求的实现有关:
Fetch 使用 credentials
直接使用原生 Fetch 的话, 需要设置 credentials.
credentials 是 Request 接口的只读属性, 用于表示用户代理是否应该在跨域请求的情况下从其他域发送 cookies. 这与 XHR 的 withCredentials 标志相似, 不同的是有三个可选值(后者是两个):
omit: 从不发送 cookies.
same-origin: 只有当 URL 与响应脚本同源才发送 cookies, HTTP Basic authentication 等验证信息.(浏览器默认值, 在旧版本浏览器, 例如 Safari 11 依旧是 omit,Safari 12 已更改)
include: 不论是不是跨域的请求, 总是发送请求资源域在本地的 cookies, HTTP Basic authentication 等验证信息.
CORS 跨域的时候, 只需要如下设置:
- fetch('http://another.com', {
- credentials: "include"
- });
XHR 使用 withCredentials
基于 XMLHttpRequest 实现的请求使用 withCredentials 来允许携带 cookie.
该属性为 boolean 类型, 所以只有 true/false 两个取值, 默认为 false.
这样也很好理解, 默认不携带是处于安全考虑.
使用如下
- var xhr = new XMLHttpRequest();
- xhr.open('GET', 'http://example.com/', true);
- xhr.withCredentials = true;
- xhr.send(null);
适用框架: jQuery 的 Ajax,axios 等.
服务端 Access-Control-Allow-Credentials
当客户端设置了允许携带 cookie 之后, 并不能完成该操作, 毕竟是跨域, 服务端也需要做响应设置, 否则浏览器拿不到正确响应.
Access-Control-Allow-Credentials:true
看 MDN 的解释:
The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to frontend JavaScript code when the request's credentials mode (Request.credentials) is"include".
当 credentials 为 include 的时候, 通知浏览器是否将响应暴露给前端 jscode, 如果为 false,JS 不能读取响应自然请求报错.
只有 Access-Control-Allow-Credentials 为 true 时, 才会将响应暴露给客户端.
当作为预检请求响应头时, 表明该实际请求 (即后面的真正请求) 是否可以使用 credentials.
不过对于简单请求, 因为没有预检, 如果服务端没有正确响应, 浏览器会忽略该属性, 并不会直接报错.
需要与 XMLHttpRequest.withCredentials 属性或者 Fetch 的 credentials 配合使用.
注意
如果要发送 Cookie,Access-Control-Allow-Origin 就不能设为星号, 必须指定明确的, 与请求网页一致的域名.
同时, Cookie 依然遵循同源政策, 只有用服务器域名设置的 Cookie 才会上传, 其他域名的 Cookie 并不会上传.
且 (跨源) 原网页代码中的 document.cookie 也无法读取服务器域名下的 Cookie.
毕竟 cookie 是有 path 来保证封闭性的, 如果可以随便读取不管从安全还是性能上都是一种隐患.
多域名跨域
对于多域名跨域, 方法比较多.
1,Access-Control-Allow-Origin:*
允许任意域名跨域, 显然支持多域名. 不过从安全性和 cookie 的使用的角度来看并不推荐.
2, 动态匹配域名
这种实现方式比较多, 原理就是声明允许的多域名配置, 可以是数组或者是正则, 根据当前请求的域名, 来判断是否在适用返回内, 在的话则设置 Access-Control-Allow-Origin 为当前域名.
具体实现这里就不写了.
结束语
参考文章
- http://www.ruanyifeng.com/blog/2016/04/cors.html
- http://www.yunweipai.com/archives/9381.html
以上是在工作中偶然发现的几点疑惑, 解决之后深究了下具体原理. 希望能对其他同学有所帮助, 抛砖引玉, 一起努力.
来源: https://www.cnblogs.com/pqjwyn/p/11117458.html