📕
余烬的小册
数据结构与算法GitHub
  • 总述
  • 经验记录
    • 经验总结
      • web component
      • 前端性能优化总结与分析
      • 我的长列表优化方案
      • 双向通讯解决方案
      • 🔧基于istanbul实现代码测试覆盖率工具
      • 表单系统(低代码表单)
      • 跨端小程序
      • 设计一个即时聊天功能
      • 跨页面通讯 3658699fe4cb4d0bbe22b0881390bacd
    • 踩坑记录
      • HTML踩坑记录
      • Flutter踩坑记录
      • CSS踩坑记录
  • 源码解析
    • Vue源码解析
      • Vue2源码解析系列-响应式原理
      • Vue2源码解析系列-模板编译
      • Vue2源码解析系列-渲染系统(待更新)
        • Patch
      • Vue2源码解析系列-调度系统(todo)
      • Vue2组件更新流程(todo)
      • 如何学习Vue源码
      • Vue3源码解析系列-响应系统
      • Vue3源码解析系列-渲染系统
      • Vue3源码解析系列-组件化和渲染优化(todo)
      • Vue router源码解析(todo)
    • React源码解析(todo)
    • 微前端
      • qiankun源码解析(todo)
    • Vite源码解析
      • Vite Client源码
      • Vite Server源码(todo)
  • 前端技术
    • javaScript
      • ES6
        • 变量声明
        • 模块化
        • 箭头函数
        • 你不知道的for...of
        • 新的数据结构Set和Map
        • JavaScript异步编程终极解决方案
        • ES6 Class 3a0c0a225a534984aabe9a943c5df975
      • JavaScript Error
      • JavaScript浅拷贝和深拷贝
      • JavaScript闭包
      • JavaScript最佳实践
      • JavaScript设计模式
      • async函数的polyfill
    • 深入理解JavaScript系列
      • JavaScript中的继承
      • JavaScript原始类型和引用类型
      • JavaScript浅拷贝和深拷贝
      • JavaScript手写系列
      • JavaScript之this
      • 词法环境和环境记录
      • JavaScript内存泄漏
      • 执行上下文
      • 从ECMAScript规范中学习this
    • TypeScript
      • TypeScript基础教程
      • Typescript高级操作
      • TypeScript工具类型
      • Typescript手写实现工具类型
      • Typescript总结(思维导图)
    • 浏览器原理
      • 页面渲染原理
      • 浏览器存储
      • JavaScript事件循环
      • 事件循环
      • 跨域
      • DOM事件流
      • 从输入url到页面渲染
      • 判断节点之间的关系及根据节点关系查找节点
      • history API
    • 跨端技术
      • Flutter
        • Flutter布局组件
    • 前端工程化
      • Babel插件开发指南
      • 循环依赖
      • pm2
    • React
      • React 状态管理
      • React组件通讯
      • Redux入门
      • Flux
      • React Hook(todo)
      • Effect
  • 服务器端
    • 计算机网络
      • 应用层
      • 运输层
      • 物理层
      • 数据链路层
      • HTTP缓存
      • HTTPS
      • 网络层
    • NodeJs
      • Node.js
      • nodejs最佳实践
      • 《深入浅出Nodejs》小结
      • mongoose填充(populate)
      • node事件循环
      • Node子进程
      • nestjs从零开始
      • nodejs流
      • Nodejs调试
      • Koa源码解析
    • 服务器
      • 操作系统
      • Linux
      • nginx常用指令
      • nginx常用配置
    • 数据库
      • Mysql常见语法
      • MongoDB Indexes索引
  • 前端安全与性能优化
    • 前端安全
      • 跨站脚本攻击(XSS)
      • 跨站点请求伪造(CSRF)
      • 点击劫持
      • 中间人攻击
      • 越权攻击与JWT
    • 前端性能优化
      • 前端监控系统
      • 前端性能优化总结与分析 7348bba0918645b1899006dc842a64c1
      • 衡量性能的核心指标 0dc15ef127cf4f4a9f1137c377420292
      • 图片懒加载
  • 杂项
    • 其他
      • Git
      • web component框架
      • 实现滚动框的懒加载
      • Stencil指南
    • CSS
      • 定位和层叠上下文
      • BFC
      • 盒模型
      • css选择器
      • css变量
由 GitBook 提供支持
在本页
  • 浏览器的同源策略
  • 跨域方案
  • 图片探测
  • 本地服务器代理
  • 反向代理
  • JSONP
  • 跨源资源共享(CORS)
  • document.domain
  • PostMessage
在GitHub上编辑
  1. 前端技术
  2. 浏览器原理

跨域

上一页事件循环下一页DOM事件流

tags: 前端工程化 summary: 本文介绍了前端开发中的跨域问题,首先介绍了浏览器的同源策略,然后介绍了几种常见的跨域方案,例如正向代理、反向代理、JSONP和CORS等,并给出相关代码示例。 Created time: December 24, 2022 8:42 PM emoji: https://s1.ax1x.com/2023/01/22/pSJh3dA.png

浏览器的同源策略

同源策略是一个重要的安全策略,它用于限制一个的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。所谓的同源是指协议、域名和端口号都相同,如果有一个不同都不算是同源。

浏览器限制脚本内发起的跨源 HTTP 请求。 例如,XMLHttpRequest和 Fetch API遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,假如发送一个跨域请求,浏览器则会将响应结果丢弃,除非其符合CORS策略。

跨域方案

图片探测

虽然xhr对象和fetch API会受到浏览器的同源策略的限制,但是HTML本身是不会受到同源策略的影响的,因此我们可以用img标签来发送一个GET请求,设置display:none来隐藏元素,可以通过监听onload和onerror事件来判断请求是否发送成功。

利用img实现跨域要特别注意一点,图片是能够被缓存的,因此需要在图片的url后加上一个时间戳,例如:

img.src = url + '?t=' + new Date().getTime();

这种方法的缺点很明显,只能使用GET方法,且无法获取响应内容。

本地服务器代理

这是开发环境中最常用的一种跨域方法,在webpack或vite中进行相应的配置,然后由nodejs开发服务器代理请求转发给目标服务器,由于同源策略只在浏览器中起作用,从而绕开了同源策略的限制,开发服务器得到响应后再返回给浏览器。

// 以vite为例
export default defineConfig({
  server: {
    proxy: {
      // 字符串简写写法
      '/foo': 'http://localhost:4567',
      // 选项写法
      '/api': {
        target: 'http://jsonplaceholder.typicode.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      },
      // 正则表达式写法
      '^/fallback/.*': {
        target: 'http://jsonplaceholder.typicode.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/fallback/, '')
      },
      // 使用 proxy 实例
      '/api': {
        target: 'http://jsonplaceholder.typicode.com',
        changeOrigin: true,
        configure: (proxy, options) => {
          // proxy 是 'http-proxy' 的实例
        }
      }
    }
  }
})

反向代理

除了正向代理外,反向代理也可以解决跨域限制。反向代理解决跨域的思路是有两种

  1. 对反向代理服务器使用CORS,这种方法本质上仍然是CORS方法,但是它的意义在于设置好反向代理的跨域资源共享(CORS)后,所有目标服务器不用再做任何处理,只需要设置好反向代理即可。

  2. 利用反向代理将后端接口代理到与网站的同源路径下。

JSONP

JSONP全称是json with Padding,这是一种比较老的解决方案。

jsonp的思路是

  1. 首先定义好回调函数,后端的数据作为回调函数的参数。

  2. 动态创建script标签请求后端接口

  3. 后端返回回调执行语句,并将数据作为参数传过来。

// 服务器 nodejs
const http = require("http");
const server = http
  .createServer((req, res) => {
		const data = 123 // 取数据
    res.end(`cb(${data})`);  // 数据作为参数
  })
  .listen("3333");
// 浏览器
<script>
		// 定义好回调函数,后端数据会当做参数传入
    function cb(data) {
      console.log(`data是${data}`);
    }
    const scr = document.createElement("script");
    scr.setAttribute("src", "http://localhost:3333");
    document.documentElement.appendChild(scr);
</script>

jsonp解决跨域的本质是利用了script 等html标签所发送的http请求不受到同源策略的限制这一特性,script请求的内容会作为JavaScript代码直接执行,因此只需要和后端约定好回调函数名、要传回的数据等就可以使用jsonp完成跨域。

jsonp的优点是兼容性好,缺点也很明显:使用起来繁琐;只支持get方法;不安全,容易被跨站脚本攻击等。

跨源资源共享(CORS)

CORS是生产环境下常用的一种解决跨域的方法,它使得XMLHttpRequest和 Fetch API 可以跳过同源策略的限制。

CORS分为两种情况:简单请求和非简单请求。

简单请求

满足以下所有条件的才是简单请求,否则是非简单请求。

  • 使用下列方法之一:

    • [GET](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/GET)

    • [HEAD](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/HEAD)

    • [POST](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/POST)

    • [Accept](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept)

    • [Accept-Language](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept-Language)

    • [Content-Language](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Language)

    • [Content-Type](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type)(需要注意额外的限制)

  • [Content-Type](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type) 的值仅限于下列三者之一:

    • text/plain

    • multipart/form-data

    • application/x-www-form-urlencoded

  • 请求中的任意 [XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) 对象均没有注册任何事件监听器;[XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) 对象可以使用 [XMLHttpRequest.upload](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/upload) 属性访问。

  • 请求中没有使用 [ReadableStream](https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStream) 对象。

简单请求仅使用Origin和[Access-Control-Allow-Origin](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) 字段进行控制,浏览器发送请求时会在请求头加上origin字段,该字段的值是当前网站的源,得到服务器响应后检查相应头的[Access-Control-Allow-Origin](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) 字段。

// 服务器
const http = require("http");
const server = http
  .createServer((req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.end("123");
  })
  .listen("3333", () => {
    console.log("port 3333");
  });
// 客户端
<script>
    const xhr = new XMLHttpRequest();
    const url = "http://localhost:3333/";
    xhr.open("GET", url);
    xhr.send();
</script>

非简单请求

简单请求的判定条件比较苛刻,通常使用XMLHttpRequest和 Fetch 发送的http请求是非简单请求。

与简单请求不同,非简单请求会先发送一个options方法的预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

预检请求会携带三个请求头字段Origin、Access-Control-Request-Headers 、Access-Control-Request-Method: GET 。响应头也有对应的三个字段Access-Control-Allow-Headers 、Access-Control-Allow-Methods、Access-Control-Allow-Origin 。只有三者都对应,才能实现跨域。

// 客户端
<script>
    const xhr = new XMLHttpRequest();
    const url = "http://localhost:3333/";
    xhr.open("GET", url);
    xhr.setRequestHeader("Authorization", "123");
    xhr.setRequestHeader("a", "111");
    xhr.send();
</script>
// 服务器
const http = require("http");
const server = http
  .createServer((req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Methods", " POST, GET, OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "a,authorization");
    res.end("123");
  })
  .listen("3333", () => {
    console.log("port 3333");
  });

除了上面6个首部字段外,响应头还可以设置Access-Control-Max-Age ,例如Access-Control-Max-Age: 86400 就表示在86400s内无需再发送预检请求。

身份凭证

默认情况下跨域的fetch和xhr不会携带身份凭证(通常是cookie),如果要携带身份凭证则需要进行相应的设置,例如xhr要设置xhr.withCredentials=true ,并且响应头还得满足:

  1. Access-Control-Allow-Credentials: true

  2. Access-Control-Allow-Origin 的值不能是*

如果是响应携带身份凭证,则Access-Control-Allow-Headers 、Access-Control-Allow-Methods、Access-Control-Allow-Origin 的值都不能是’*’。

// 客户端
<script>
    const xhr = new XMLHttpRequest();
    const url = "http://localhost:3333/";
    xhr.open("GET", url);
    xhr.setRequestHeader("Authorization", "123");
    xhr.withCredentials = true;
    xhr.send();
</script>
// 服务器
const http = require("http");
const server = http
  .createServer((req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
    res.setHeader("Access-Control-Allow-Methods", " POST, GET, OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "a,authorization");
    res.setHeader("Access-Control-Allow-Credentials", "true");
    res.end("123");
  })
  .listen("3333", () => {
    console.log("port 3333");
  });

总结

请求头
响应头
作用

origin

access-control-allow-origin

允许的源

access-control-request-headers

access-control-allow-headers

允许的请求头

access-control-request-method

access-control-allow-method

允许的方法

access-control-allow-credentials

是否可携带凭证

access-control-max-age

有效时间,在有效时间内不会再发送预检请求

document.domain

当同一域名下的两个子域名需要实现跨域时,可以将document.domain设置成父域名,然后就可以实现cookie共享。

注意: 目前几乎所有主流浏览器都支持此特性,但是未来可能会被废弃。

PostMessage

window.postMessage() 是一种可以安全地实现跨源通信的方法。

使用PostMessage首先需要获取到另一个窗口的引用,然后调用otherWindow.postMessage(message, targetOrigin, [transfer]);

otherWindow

message

targetOrigin

通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个 URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口;防止数据被恶意的第三方截获。

transfer 可选

是一串和 message 同时传递的 [Transferable](https://developer.mozilla.org/zh-CN/docs/Web/API/Transferable) 对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

而对于接收方,则需要通过window.addEventListener('message',function(res){}) 来接收消息。其中res参数有三个很重要的属性。

origin

调用 postMessage时消息发送方窗口的 origin,这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成。接收方跨域通过该属性避免接收不明网站发送的信息。

data

postMessage发送的消息。

source

示例

// http://127.0.0.1:5500/a1.html
<script>
    const w1 = window.open("http://127.0.0.1:5500/a2.html");
    setTimeout(() => {
			// 避免a2页面还没加载完成就发送消息
      w1.postMessage("123", "http://127.0.0.1:5500/a2.html");
    }, 1000);
</script>

// http://127.0.0.1:5500/a2.html
<script>
    window.addEventListener(
      "message",
      function (res) {
        // 确认发送方身份
        if (res.origin === "http://127.0.0.1:5500") {
        }
      },
      false
    );
</script>

注意 如果不需要接收message消息,则应该直接避免监听message消息,如果需要接收message消息,则应当始终使用 origin 和 source 属性验证发件人的身份,防止跨站脚本攻击。

除了被用户代理自动设置的首部字段(例如 [Connection](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Connection),[User-Agent](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/User-Agent))和在 Fetch 规范中定义为 的其他首部,允许人为设置的字段为 Fetch 规范定义的 。该集合为:

其他窗口的一个引用,比如 iframe 的 contentWindow 属性、执行返回的窗口对象、或者是命名过或数值索引的。

将要发送到其他 window 的数据。它将会被序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。[]

对发送消息的对象的引用; 您可以使用此来在具有不同 origin 的两个窗口之间建立双向通信。

origin
禁用首部名称
对 CORS 安全的首部字段集合
window.open
window.frames
结构化克隆算法
1
窗口