libin1991 / libin_Blog

爬虫-博客大全
https://libin.netlify.com/
124 stars 17 forks source link

CORS跨域 #677

Open libin1991 opened 5 years ago

libin1991 commented 5 years ago

系列文章:

预检请求的诞生

前一篇文章结尾, 我们发现使用 CORS 方式实现跨域, 有时候会发送两个请求 一个 OPTIONS 一个正常请求, 这个 OPTIONS 是个什么鬼呢?

下面贴一段 MDN 的解释

2018-12-08-09-17-13

众所周知, 后端 API 设计比较流行的范式就是 restful(到 2018 年 12 月 8 日). 在 restful 中分别用不同的 HTTP METHOD 标识后端的 CURD, 对于使用这些可能会更新后端数据的 HTTP METHOD 发出的跨域请求, 浏览器要首先和服务器商定一下当前的域名是不是有执行对应的 CURD 的权限. 于是这个 OPTIONS 类型的 预检请求 就诞生了. 那么问题来了 可能对服务器数据产生副作用的 HTTP 请求方法 是有那些咧? 不知道么有关系, TIM 队长为我们探探路 😄

2018-12-08-09-32-33

简单请求 VS 复杂请求

在 CORS 机制中, 把请求分为了 简单请求复杂请求, 一个 HTTP 请求若想要让自己成为一个简单请求就要满足以下条件:

  • 首先, 请求方式的限制: 请求方式(method) 只能是 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) 三者中的一个.

综上, 只要前端发出的请求满足以上三个条件, 你发出的请求就是简单请求. 那么什么事复杂请求呢? 答: 只要不是简单请求就是复杂请求. 原本以为很难理清的概念, 居然只有三个条件搞定 ^_^.

2018-12-08-09-58-34

再告诉大家一个秘密, 所有的简单请求跨域访问都是不会触发预检请求的哟. 那是复杂请求的专利...

预检请求都干了啥 😳

对于复杂请求发生跨域访问前, 总是要通过预检请求进行鉴权. 那么鉴权的过程到底是啥么样子的呢? 这一步我们一起来研究一下.

  • 首先, 打开上一节的代码
  • 分别执行 node ./be/cors/index.js live-server ./fe/cors 启动后端服务和前端的 web 容器.
  • 浏览器自动打开后打开控制台, 切换到 Network tab 并刷新浏览器. 不出意外的话, 看到的是这个样子的
    2018-12-08-10-09-38
  • 点击一下第一个 localhost 请求并查看详情
    2018-12-08-10-22-25
    不难发现, 响应头里标注的几个字段, 就是我们的后端项目里边写的几个.
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 掉. 那肿么办呢?

2018-12-08-10-42-18

面对这种情况, 有两种解决方案.

  • 第一种, 可以和后端小哥哥商量一下. 把接口改成简单请求, 预检请求的问题就迎刃而解了.
  • 然而, 有时候写好的代码谁都不愿意去改. 后端小哥哥不听话. 这种情况下 Access-Control-Max-Age 就派上用场了. 这个响应头的意思是预检请求的有效期. 在指定时间内再次跨域访问接口, 是不需要预检请求的, 单位是 . 如果我们把有效时间写的非常的长, 那么四不四看上去就像删除了预检请求了呢 ^_^.
  • 附加情况, 你老板不懂技术瞎 J8 指挥. 小爷我不干了. 当然这种处理方案比较不推荐.

PS: 使用 Access-Control-Max-Age 机制和缓存类似, 所以给老板演示的时候千万不要清理缓存. 不要勾选 Network 下的 disable cache. 不说啦, 都是泪...

2018-12-08-11-13-21

咱们不能允许所有的人都访问呀

通过 Access-Control-Allow-Origin, 可以在后端设置可以跨域访问我们的域名列表, * 代表所有的域名都可以跨域访问我们的后端, 这样其实是有隐患的. 为了安全起见, 我们把可以跨域访问的域名限制为我们已知的域名. 老规矩.

2018-12-08-11-24-04

后端代码

// 修改一行代码, 一定要添加协议哟
response.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8080');
复制代码

修改以后浏览器访问 http://127.0.0.1:8080

2018-12-08-11-27-57

如果想要开放多个域名的跨域访问咋办咧?

如果我们有多个业务域名需要跨域访问同一个服务器, 可以把允许的域名列表保存到一个数组里. 接到请求之后先判断当前请求域名是否在我们允许的域名列表里, 如果在的话直接添加到响应头 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

2018-12-08-11-46-03
响应结果成功打印, 没有任何问题.

其次访问 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()
复制代码

2018-12-08-11-54-13

没有任何报错, 返回结果成功打印. 成功...

你的请求怎么没有携带 Cookie

一般情况下, 前端发出的跨域的 ajax OR fetch 请求是不会携带 Cookie 的. 但是, 后端小哥哥还要. 咋弄咧? 加上呗.

2018-12-08-12-05-05

前端代码:

// 在 xhr.send 之前添加这一行
xhr.withCredentials = true;
复制代码

添加完以后, 刷新浏览器.

2018-12-08-12-07-24

对于这个报错, 不知道你有没有啥好说的, 反正我是没啥话了...

后端代码:

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 中多了一条

2018-12-08-12-37-50

请求中携带了 Cookie

2018-12-08-12-37-26

通过下边的动图可以看出, 我们前后端 Cookie 传递非常的通畅.

cookie-2134567

我在响应头上给你返回了 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 打印出了空行

2018-12-08-13-26-21

但是在 Network Tab 下后端确实返回了响应头 token 字段. 懵逼了...

2018-12-08-13-28-41

原来, Access-Control- 系列还有一个响应头 Access-Control-Expose-Headers, 我们在后端代码 response.end(...) 之前加上 response.setHeader('Access-Control-Expose-Headers', 'token');再次会浏览器查看

2018-12-08-13-31-40

成功了 😄.

预检请求不返回内容把

我们的响应结果本来应该是在正式的请求中才需要返回的, 但是我们看下预检请求的返回详情发现

2018-12-08-13-34-09

预检请求只是浏览器层面的解析, 前端代码根本拿不到. 这里的内容仅仅是浪费带宽和用户的流量. 所以我们改造一下.预检请求不再返回内容.

后端代码:

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

libin1991 commented 5 years ago

系列文章:

前一篇文章中, 我们一起学习了第一种跨域处理方案 JSONP. 这种方法相对比较原始, 优点是兼容性好, 就连现代前端没怎么听说过的 IE 6 上跑起来都是妥妥的. 然鹅, 它也就这一点优点了. 其缺点有: 只支持 GET 请求, 配置繁琐(前后端都需要调整代码), 在 window 上注册各种回调函数, 开发体验差....

2018-12-07-10-07-21

CORS

由于 JSONP 的方案存在诸多缺点且老旧, 这里我们一起学习一种比较现代的跨域问题解决方案---CORS

兼容性

从 mdn 官网粘来的兼容性列表如下:

2018-12-07-10-25-52

ie 10 都可以跑, 足以满足现代前端开发者的需求了.

概念 😳

概念性的东西在这儿 MDN 偶尔需要梯子, 自备哈.

搭建跨域的环境

我们先创建一个跨域的环境, 代码基于我们 jsonp 时候的示例项目 cross-domain, 首先, 在 fe 和 be 目录下创建 cors 目录. 其次, 分别添加 index.htmlindex.js. 修改以后的项目目录如下图.

2018-12-07-14-26-54

编写前端代码 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
  • 刷新浏览器

2018-12-07-16-47-05
我们细细分析这个熟悉的报错, 前一段告诉我们我们的请求被 block 了. 后边居然直接告诉我们解决方案了, 方案了 'Access-Control-Allow-Origin' header is present on the requested resource., 这么明显的暗示, 难道我们就不试试???
2018-12-07-17-27-44

响应头添加 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);
});
复制代码

改动后的代码.

巨大的 PS: 修改过后端代码以后, 一定要重启 node 服务

浏览器刷新一下, 我了个乖乖. 好了 😄

2018-12-07-17-33-48

开心过后, 我们想一下, jsonp 的缺点是只能支持 GET 请求, 作为现代的跨域请求方式. cors 能不能支持其他的请求方式呢?

其他请求方式的支持

作为现代的跨域问题解决方案, 应该是能解决多种请求方式的. 光说不练假把式. 咱们试试 😄

改动前端代码

// 改动前
xhr.open('GET', 'http://localhost:8888')
// 改动后
xhr.open('POST', 'http://localhost:8888')
复制代码

修改后代码来浏览器上看一下?

2018-12-07-18-22-46

木有任何问题, 返回的数据顺利的打印. 没有任何的报错.

趁着兴头试试PUT请求

再次改动前端代码

// 改动前
xhr.open('POST', 'http://localhost:8888')
// 改动后
xhr.open('PUT', 'http://localhost:8888')
复制代码

修改后代码来浏览器上看一下?

2018-12-07-18-25-55

哎呀我滴妈? 很眼熟的错误, 但是不要认错人哈, 这次的报错和之前的报错长的很像, 但是关键词不一样了. 根据之前的经验, 后端添加 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);
});
复制代码

修改后代码来浏览器上看一下?

2018-12-07-18-32-14

成功了 😄

2018-12-07-18-33-45

其他的 http 方法和 PUT 方法处理的方式是一样的. 举一反三即可.

后端说, 你的请求要加一个 token 呀

既然是现代的开发, 那么会话的管理一般是会用 jwt(后续可能会写相关的文章), jwt 一个闪耀的标志就是请求头添加了 jwt token. 明人不说暗话.

2018-12-07-18-39-07

修改前端代码:

// 添加了一行
xhr.setRequestHeader('token', 'quanquanbunengshuo')
复制代码

修改后代码来浏览器上看一下?

2018-12-07-18-43-04

相信大家已经摸清了我的套路, 闲话不扯.

后端代码

// 添加了一行
response.setHeader('Access-Control-Allow-Headers', 'token');
复制代码

修改后代码来浏览器上看一下?

2018-12-07-18-45-14

目前为止, 跨域请求成功了, 请求方式兼容了, 自定义请求头好使了. 是不是大吉大利, 可以吃鸡了呢?

致, 被打入冷宫的 Network tab

我们自始至终都在查看浏览器的 Console tab, 作为一个通信性质的文章, 不看一下 Network 明显有点说不过去辣.

2018-12-07-18-50-42

既然存在, 那肯定是要看看的, 我们把 tab 切换到 Network.

2018-12-07-18-52-58

哎呦喂? 两个请求, 一个 OPTIONS 一个 PUT, 这是什么鬼?

下集预告: 刚刚看到了 Network 就出现了血案. 当然如果仅仅是停留在会用 CORS 实现跨域上, 到目前为止已经没有什么问题了, 用来面试也是杠杠滴. 下一步, 我们一起探讨 CORS 条件下, 预检请求Cookie 携带那些事儿. 周五码字感觉好累...... 约小姐姐去辣 😄

9150e4e5ly1fkhkg77t1mg204603w12l