系列文章:前端
在前一篇文章结尾, 咱们发现使用 CORS 方式实现跨域, 有时候会发送两个请求
一个 OPTIONS 一个正常请求
, 这个 OPTIONS 是个什么鬼呢?node
下面贴一段 MDN 的解释 git
众所周知, 后端 API 设计比较流行的范式就是 restful(到 2018 年 12 月 8 日). 在 restful 中分别用不一样的 HTTP METHOD 标识后端的 CURD, 对于使用这些可能会更新后端数据的 HTTP METHOD 发出的跨域请求, 浏览器要首先和服务器商定一下当前的域名是否是有执行对应的 CURD 的权限. 因而这个 OPTIONS 类型的 预检请求
就诞生了. 那么问题来了 可能对服务器数据产生反作用的 HTTP 请求方法
是有那些咧? 不知道么有关系, TIM 队长为咱们探探路 😄程序员
在 CORS 机制中, 把请求分为了 简单请求
和 复杂请求
, 一个 HTTP 请求若想要让本身成为一个简单请求就要知足如下条件:github
GET POST HEAD
三者中的一个Accept Accept-Language Content-Language Content-Type DPR Downlink Save-Data Viewport-Width Width
.Content-Type
的时候, 其值必须为 text/plain multipart/form-data application/x-www-form-urlencoded(这个是 form 提交默认的 Content-Type)
三者中的一个.综上, 只要前端发出的请求知足以上三个条件, 你发出的请求就是简单请求. 那么什么事复杂请求呢? 答: 只要不是简单请求就是复杂请求. 本来觉得很难理清的概念, 竟然只有三个条件搞定 ^_^.web
再告诉你们一个秘密, 全部的简单请求跨域访问都是不会触发预检请求的哟. 那是复杂请求的专利...ajax
对于复杂请求发生跨域访问前, 老是要经过预检请求进行鉴权. 那么鉴权的过程究竟是啥么样子的呢? 这一步咱们一块儿来研究一下.json
node ./be/cors/index.js
live-server ./fe/cors
启动后端服务和前端的 web 容器.response.setHeader('Access-Control-Allow-Origin', '*');
response.setHeader('Access-Control-Allow-Methods', 'PUT');
response.setHeader('Access-Control-Allow-Headers', 'token');
response.setHeader('Access-Control-Max-Age', 5);
复制代码
一一对应, 绝非偶然 😄. 那么请求头中标注的两个又是什么意思呢?后端
浏览器在接受到咱们发送的跨域请求的指令时, 会自动判断咱们的请求是否属于跨域请求, 若是是的话便会发出预检请求, 预检请求的请求头信息也是浏览器根据咱们的请求信息自动添加的. 示例项目中, 由于咱们的请求是 PUT
类型的, 因此在预检请求的时候会添加 Access-Control-Allow-Methods: PUT
来咨询服务器本身是否能够向它发送这种类型的请求. 同理, 因为咱们的请求中有自定义请求头 token
因此, 在预检请求中, 浏览器要和服务器作是否能够添加自定义请求头的协商. 只有当浏览器和服务器之间的预检请求协商经过了, 浏览器才会继续发送真正的 AJAX
请求.api
在工做中, 老板每每是不懂技术的. 能看控制台的老板通常是高手了. 面对这种一个 api 发两次请求的状况可能一个程序员笑笑也就过去了, 可是老板就不这么认为了, 一个接口他就要一次请求. 你要把 圈圈的圈
跨域文章推荐给老板, 让小哥哥也了解下? 估计你会被 fire 掉. 那肿么办呢?
面对这种状况, 有两种解决方案.
Access-Control-Max-Age
就派上用场了. 这个响应头的意思是预检请求的有效期. 在指定时间内再次跨域访问接口, 是不须要预检请求的, 单位是 秒
. 若是咱们把有效时间写的很是的长, 那么四不四看上去就像删除了预检请求了呢 ^_^.PS: 使用 Access-Control-Max-Age
机制和缓存相似, 因此给老板演示的时候千万不要清理缓存. 不要勾选 Network 下的 disable cache
. 不说啦, 都是泪...
经过 Access-Control-Allow-Origin
, 能够在后端设置能够跨域访问咱们的域名列表, *
表明全部的域名均可以跨域访问咱们的后端, 这样实际上是有隐患的. 为了安全起见, 咱们把能够跨域访问的域名限制为咱们已知的域名. 老规矩.
后端代码
// 修改一行代码, 必定要添加协议哟
response.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8080');
复制代码
修改之后浏览器访问 http://127.0.0.1:8080
若是想要开放多个域名的跨域访问咋办咧?
若是咱们有多个业务域名须要跨域访问同一个服务器, 能够把容许的域名列表保存到一个数组里. 接到请求以后先判断当前请求域名是否在咱们容许的域名列表里, 若是在的话直接添加到响应头 Access-Control-Allow-Origin
下.
后端代码
const http = require('http');
const PORT = 8888;
// 协议名必填, 若是同时存在 http 和 https 就写两条s
const allowOrigin = ['http://127.0.0.1:8080', 'https://www.baidu.com'];
// 建立一个 http 服务
const server = http.createServer((request, response) => {
const { headers: { origin } } = request;
if (allowOrigin.includes(origin)) {
response.setHeader('Access-Control-Allow-Origin', origin);
}
response.setHeader('Access-Control-Allow-Methods', 'PUT');
response.setHeader('Access-Control-Allow-Headers', 'token');
response.setHeader('Access-Control-Max-Age', 5);
response.end("{name: 'quanquan', friend: 'guiling'}");
});
// 启动服务, 监听端口
server.listen(PORT, () => {
console.log('服务启动成功, 正在监听: ', PORT);
});
复制代码
此时代码, 首先访问http://127.0.0.1:8080
其次访问 www.baidu.com, 打开控制台, 执行
xhr = new XMLHttpRequest()
xhr.open('GET', 'http://localhost:8888')
xhr.onreadystatechange = () => {
xhr.status === 200 && xhr.readyState === 4 && console.log(xhr.responseText)
}
xhr.send()
复制代码
没有任何报错, 返回结果成功打印. 成功...
通常状况下, 前端发出的跨域的 ajax OR fetch 请求是不会携带 Cookie 的. 可是, 后端小哥哥还要. 咋弄咧? 加上呗.
前端代码:
// 在 xhr.send 以前添加这一行
xhr.withCredentials = true;
复制代码
添加完之后, 刷新浏览器.
对于这个报错, 不知道你有没有啥好说的, 反正我是没啥话了...
后端代码:
const http = require('http');
const PORT = 8888;
// 协议名必填, 若是同时存在 http 和 https 就写两条s
const allowOrigin = ['http://127.0.0.1:8080', 'http://localhost:8080', 'https://www.baidu.com'];
// 建立一个 http 服务
const server = http.createServer((request, response) => {
const { method, headers: { origin, cookie } } = request;
if (allowOrigin.includes(origin)) {
response.setHeader('Access-Control-Allow-Origin', origin);
}
response.setHeader('Access-Control-Allow-Methods', 'PUT');
// 容许前端请求携带 Cookie
response.setHeader('Access-Control-Allow-Credentials', true);
response.setHeader('Access-Control-Allow-Headers', 'token');
if (method === 'OPTIONS') {
console.log('预检请求');
} else if (!cookie) {
// 若是不存在 Cookie 就设置 Cookie
response.setHeader('Set-Cookie', 'quanquan=fe');
}
response.end("{name: 'quanquan', friend: 'guiling'}");
});
// 启动服务, 监听端口
server.listen(PORT, () => {
console.log('服务启动成功, 正在监听: ', PORT);
});
复制代码
此时代码, 再次到浏览器看一下.
Cookie 中多了一条
请求中携带了 Cookie
经过下边的动图能够看出, 咱们先后端 Cookie 传递很是的通畅.
工做中经常遇到后端把一些标识放在响应头上返回给前端的 case, 好比用户登陆, 后端返回用户的惟一标识放在响应头上. 须要前端获取, 后续的请求都须要把这个标识放在请求头, 用于验证用户的身份.
咱们首先修改后端代码:
// 在 response.end() 前添加这一行
response.setHeader('token', 'quanquan');
复制代码
修改前端代码:
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText)
// 打印响应数据时同时打印全部响应头
console.log(xhr.getAllResponseHeaders())
}
}
复制代码
修改完成后代码, 浏览器看一下.
console.log 打印出了空行
可是在 Network Tab 下后端确实返回了响应头 token 字段. 懵逼了...
原来, Access-Control-
系列还有一个响应头 Access-Control-Expose-Headers
, 咱们在后端代码 response.end(...)
以前加上 response.setHeader('Access-Control-Expose-Headers', 'token');
再次会浏览器查看
成功了 😄.
咱们的响应结果原本应该是在正式的请求中才须要返回的, 可是咱们看下预检请求的返回详情发现
预检请求只是浏览器层面的解析, 前端代码根本拿不到. 这里的内容仅仅是浪费带宽和用户的流量. 因此咱们改造一下.预检请求再也不返回内容.
后端代码:
const http = require('http');
const PORT = 8888;
// 协议名必填, 若是同时存在 http 和 https 就写两条s
const allowOrigin = ['http://127.0.0.1:8080', 'http://localhost:8080', 'https://www.baidu.com'];
// 建立一个 http 服务
const server = http.createServer((request, response) => {
const { method, headers: { origin, cookie } } = request;
if (allowOrigin.includes(origin)) {
response.setHeader('Access-Control-Allow-Origin', origin);
}
response.setHeader('Access-Control-Allow-Methods', 'PUT');
response.setHeader('Access-Control-Allow-Credentials', true);
response.setHeader('Access-Control-Allow-Headers', 'token');
response.setHeader('Access-Control-Expose-Headers', 'token');
response.setHeader('token', 'quanquan');
if (method === 'OPTIONS') {
response.writeHead(204);
response.end('');
} else if (!cookie) {
response.setHeader('Set-Cookie', 'quanquan=fe');
}
response.end("{name: 'quanquan', friend: 'guiling'}");
});
// 启动服务, 监听端口
server.listen(PORT, () => {
console.log('服务启动成功, 正在监听: ', PORT);
});
复制代码
此时代码, 验证, 就不验证了吧. 好使 😄
下基预告: 前两种跨域方案就算是讲完了, 很多小伙伴吐槽, jsonp 太老, cors 太麻烦.... 那么下一节咱们尝试一下 反向代理
, See you