系列文章:
在前一篇文章中, 我们一起学习了第一种跨域处理方案
JSONP
. 这种方法相对比较原始, 优点是兼容性好, 就连现代前端没怎么听说过的IE 6
上跑起来都是妥妥的. 然鹅, 它也就这一点优点了. 其缺点有: 只支持 GET 请求, 配置繁琐(前后端都需要调整代码), 在 window 上注册各种回调函数, 开发体验差....
CORS
由于 JSONP 的方案存在诸多缺点且老旧, 这里我们一起学习一种比较现代的跨域问题解决方案---CORS
兼容性
从 mdn 官网粘来的兼容性列表如下:
ie 10 都可以跑, 足以满足现代前端开发者的需求了.
概念 😳
概念性的东西在这儿 MDN 偶尔需要梯子, 自备哈.
搭建跨域的环境
我们先创建一个跨域的环境, 代码基于我们 jsonp 时候的示例项目 cross-domain, 首先, 在 fe 和 be 目录下创建 cors 目录. 其次, 分别添加 index.html
和 index.js
. 修改以后的项目目录如下图.
编写前端代码 fe/cors/index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>CORS 实现跨域</title>
</head>
<body>
<h3>CORS 实现跨域</h3>
<script>
var xhr = new XMLHttpRequest()
xhr.open('GET', 'http://localhost:8888')
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText)
}
}
xhr.send()
</script>
</body>
</html>
复制代码
一个灰常简单的 ajax 请求, 有木有.
后端代码 be/cors/index.js
const http = require('http');
const PORT = 8888;
// 创建一个 http 服务
const server = http.createServer((request, response) => {
response.end("{name: 'quanquan', friend: 'guiling'}");
});
// 启动服务, 监听端口
server.listen(PORT, () => {
console.log('服务启动成功, 正在监听: ', PORT);
});
复制代码
此时的项目代码
找点苗头
代码环境准备完成后
- 首先启动后端代码
node ./be/cors/index.js
- 其次启动前端 web 容器
live-server ./fe/cors
- 打开浏览器, 访问 http://localhost:8080/
- 打开控制台, 切换到 Console tab
- 刷新浏览器
'Access-Control-Allow-Origin' header is present on the requested resource.
, 这么明显的暗示, 难道我们就不试试???
响应头添加 Access-Control-Allow-Origin
针对浏览器的报错, 我们分析出他是要我们在响应头上添加Access-Control-Allow-Origin
这个字段,
那么我们修改我们的后端代码.
const http = require('http');
const PORT = 8888;
// 创建一个 http 服务
const server = http.createServer((request, response) => {
response.setHeader('Access-Control-Allow-Origin', '*');
response.end("{name: 'quanquan', friend: 'guiling'}");
});
// 启动服务, 监听端口
server.listen(PORT, () => {
console.log('服务启动成功, 正在监听: ', PORT);
});
复制代码
改动后的代码.
浏览器刷新一下, 我了个乖乖. 好了 😄
开心过后, 我们想一下, jsonp 的缺点是只能支持 GET 请求, 作为现代
的跨域请求方式. cors 能不能支持其他的请求方式呢?
其他请求方式的支持
作为现代
的跨域问题解决方案, 应该是能解决多种请求方式的. 光说不练假把式. 咱们试试 😄
改动前端代码
// 改动前
xhr.open('GET', 'http://localhost:8888')
// 改动后
xhr.open('POST', 'http://localhost:8888')
复制代码
修改后代码来浏览器上看一下?
木有任何问题, 返回的数据顺利的打印. 没有任何的报错.
趁着兴头试试PUT
请求
再次改动前端代码
// 改动前
xhr.open('POST', 'http://localhost:8888')
// 改动后
xhr.open('PUT', 'http://localhost:8888')
复制代码
修改后代码来浏览器上看一下?
哎呀我滴妈? 很眼熟的错误, 但是不要认错人哈, 这次的报错和之前的报错长的很像, 但是关键词不一样了. 根据之前的经验, 后端添加 Access-Control-Allow-Methods
响应头应该好使.
后端代码
const http = require('http');
const PORT = 8888;
// 创建一个 http 服务
const server = http.createServer((request, response) => {
response.setHeader('Access-Control-Allow-Origin', '*');
response.setHeader('Access-Control-Allow-Methods', 'PUT');
response.end("{name: 'quanquan', friend: 'guiling'}");
});
// 启动服务, 监听端口
server.listen(PORT, () => {
console.log('服务启动成功, 正在监听: ', PORT);
});
复制代码
修改后代码来浏览器上看一下?
成功了 😄
其他的 http 方法和 PUT
方法处理的方式是一样的. 举一反三即可.
后端说, 你的请求要加一个 token 呀
既然是现代的开发, 那么会话的管理一般是会用 jwt(后续可能会写相关的文章), jwt 一个闪耀的标志就是请求头添加了 jwt token. 明人不说暗话.
修改前端代码:
// 添加了一行
xhr.setRequestHeader('token', 'quanquanbunengshuo')
复制代码
修改后代码来浏览器上看一下?
相信大家已经摸清了我的套路, 闲话不扯.
后端代码
// 添加了一行
response.setHeader('Access-Control-Allow-Headers', 'token');
复制代码
修改后代码来浏览器上看一下?
目前为止, 跨域请求成功了, 请求方式兼容了, 自定义请求头好使了. 是不是大吉大利, 可以吃鸡了呢?
致, 被打入冷宫的 Network tab
我们自始至终都在查看浏览器的 Console tab, 作为一个通信性质的文章, 不看一下 Network 明显有点说不过去辣.
既然存在, 那肯定是要看看的, 我们把 tab 切换到 Network.
哎呦喂? 两个请求, 一个 OPTIONS 一个 PUT, 这是什么鬼?
下集预告: 刚刚看到了 Network 就出现了血案. 当然如果仅仅是停留在会用 CORS 实现跨域上, 到目前为止已经没有什么问题了, 用来面试也是杠杠滴. 下一步, 我们一起探讨 CORS 条件下, 预检请求
和 Cookie
携带那些事儿. 周五码字感觉好累...... 约小姐姐去辣 😄
系列文章:
预检请求的诞生
下面贴一段 MDN 的解释
众所周知, 后端 API 设计比较流行的范式就是 restful(到 2018 年 12 月 8 日). 在 restful 中分别用不同的 HTTP METHOD 标识后端的 CURD, 对于使用这些可能会更新后端数据的 HTTP METHOD 发出的跨域请求, 浏览器要首先和服务器商定一下当前的域名是不是有执行对应的 CURD 的权限. 于是这个 OPTIONS 类型的
预检请求
就诞生了. 那么问题来了可能对服务器数据产生副作用的 HTTP 请求方法
是有那些咧? 不知道么有关系, TIM 队长为我们探探路 😄简单请求 VS 复杂请求
在 CORS 机制中, 把请求分为了
简单请求
和复杂请求
, 一个 HTTP 请求若想要让自己成为一个简单请求就要满足以下条件: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)
三者中的一个.综上, 只要前端发出的请求满足以上三个条件, 你发出的请求就是简单请求. 那么什么事复杂请求呢? 答: 只要不是简单请求就是复杂请求. 原本以为很难理清的概念, 居然只有三个条件搞定 ^_^.
再告诉大家一个秘密, 所有的简单请求跨域访问都是不会触发预检请求的哟. 那是复杂请求的专利...
预检请求都干了啥 😳
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 发两次请求的情况可能一个程序员笑笑也就过去了, 但是老板就不这么认为了, 一个接口他就要一次请求. 你要把
圈圈的圈
跨域文章推荐给老板, 让小哥哥也了解下? 估计你会被 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() 复制代码
没有任何报错, 返回结果成功打印. 成功...
你的请求怎么没有携带 Cookie
一般情况下, 前端发出的跨域的 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 传递非常的通畅.
我在响应头上给你返回了 Token, 你取出来放在请求头上
工作中常常遇到后端把一些标识放在响应头上返回给前端的 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