跨域

91 阅读7分钟

跨域问题梳理

1.浏览器的同源策略

1.1 同源策略概述

同源策略是浏览器为确保资源安全,而遵循的一种策略,该策略对访问资源进行了一些限制。
W3C 上对同源策略的说明:Same origin policy

1.2 什么是源(origin)?

1.源的组成部分

image.png

2.下面表格中,只有最后一行的两个源是同源。

源 1源 2是否同源
www.xyz.com/homewww.xyz.com/home⛔非同源️,协议
www.xyz.com/homemail.xyz.com/home⛔非同源,域名
www.xyz.com:8080/homewww.xyz.com:8090/home⛔非同源,端口
www.xyz.com:8080/homewww.xyz.com:8080/search✅同 源︎

3.同源请求与非同源请求

image.png

4.总结:『所处源』与『目标源』不一致,就是『非同源』,又称『异源』或『跨域』。

2. 跨域会受到哪些限制

浏览器会对跨域做出哪些限制?

例如有两个源:『源A』和『源B』,它们是『非同源』的,那么浏览器会有如下限制:

2.1. 限制DOM访问

『源A』的脚本不能访问『源B』的 DOM。

<!-- 注意:本页面是页面1,./demo.html是同源的页面2,https://www.baidu.com是非同源的页面2
-->
<button onClick="showDOM()">获取页面2的DOM</button>
<!-- <iframe id="framePage" src="./demo.html"></iframe> -->
<iframe id="framePage" src="https://www.baidu.com"></iframe>

<script type="text/javascript" >
  function showDOM(){
    const framePage = document.getElementById('framePage')
    console.log(framePage.contentWindow.document) //同源的可以获取,非同源的无法获取
    /*注意1 (同源情况下)
    framePage.contentWindow 拿到页面2的window
    framePage.contentWindow.document 拿到页面2的document
    framePage.contentWindow.document.body 拿到页面2的body
    注意2(非同源情况下)
    2024年《# 禹神:一小时彻底搞懂跨域&解决方案》视频里可以
    <iframe id="framePage" src="https://www.baidu.com"></iframe>可以在页面1上展示百度的页面,但是无法通过framePage.contentWindow.document获取到百度的document,会报错
    但是2025年我运行的时候
    <iframe id="framePage" src="https://www.baidu.com"></iframe>
    报错:Refused to frame 'https://www.baidu.com/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'self' https://chat.baidu.com
    应该是百度增加了CSP机制,它是一种浏览器安全机制,只有在当前页面的祖先是本身(‘self’)或指定的一组特定域名下,才允许通过 iframe 嵌入。如果嵌入的页面不满足这些条件,浏览器就会拒绝加载这个 iframe,并抛出类似于上述的错误消息。
    */
  }
</script>

2.2. 限制Cookie访问

『源A』不能访问『源B』的 cookie。

<iframe id="baidu" src="http://www.baidu.com" width="500" height="300"></iframe>

<script type="text/javascript" >
  // 访问的是当前源的cookie,并不是baidu的cookie
  console.log(document.cookie)//拿到当前源的cookie(页面1和同源iframe嵌入的页面2都可以获取到)
</script>

2.3. 限制Ajax获取数据

『源A』可以给『源B』发请求,但是无法获取『源B』响应的数据。

const url = 'https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc'
let result = await fetch(url)
let data = await result.json();
console.log(data)

备注:在上述限制中,浏览器对 Ajax 获取数据的限制是影响最大的一个,且实际开发中经常遇到。

3. 几个注意点

  1. 跨域限制仅存在浏览器端,服务端不存在跨域限制。
  2. 即使跨域了,Ajax 请求也可以正常发出,但响应数据不会交给开发者。

  1. <link><script><img>...... 这些标签发出的请求也可能跨域,只不过浏览器对标签跨域不做严格限制,对开发几乎无影响。

4. CORS 解决 Ajax 跨域问题

4.1. CORS 概述

CORS 全称:Cross-Origin Resource Sharing(跨域资源共享),是用于控制浏览器校验跨域请求的一套规范,服务器依照 CORS 规范,添加特定响应头来控制浏览器校验,大致规则如下:

  • 服务器明确表示拒绝跨域请求,或没有表示,则浏览器校验不通过
  • 服务器明确表示允许跨域请求,则浏览器校验通过

备注说明:使用 CORS 解决跨域是最正统的方式,且要求服务器是“自己人”。

4.2. CORS 解决简单请求跨域

整体思路:服务器在给出响应时,通过添加Access-Control-Allow-Origin响应头,来明确表达允许某个源发起跨域请求,随后浏览器在校验时,直接通过。

服务端核心代码(以express框架为例):

// 处理跨域中间件
function corsMiddleWare(req,res,next){
  // 允许 http://127.0.0.1:5500 这个源发起跨域请求
  // res.setHeader('Access-Control-Allow-Origin','http://127.0.0.1:5500')
  
  // 允许所有源发起跨域请求
  res.setHeader('Access-Control-Allow-Origin','*')
  next()
}

// 配置路由并使用中间件
app.get('/',corsMiddleWare,(req,res)=>{
  res.send('hello!')
})

4.3. 简单请求与复杂请求

CORS 会把请求分为两类,分别是:① 简单请求、② 复杂请求。

简单请求复杂请求
✅请求方法(method)为:GETHEADPOST1. 不是简单请求,就是复杂请求。
  1. 复杂请求会自动发送预检请求。 | | ✅请求头字段要符合 《CORS 安全规范》简记:只要不手动修改请求头,一般都能符合该规范。 | | | ✅请求头的Content-Type的值只能是以下三种:- text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded | |

关于预检请求:

  1. 发送时机: 预检请求在实际跨域请求之前发出,是由浏览器自动发起的。
  2. 主要作用: 用于向服务器确认是否允许接下来的跨域请求。
  3. 基本流程: 先发起OPTIONS请求,如果通过预检,继续发起实际的跨域请求。
  4. 请求头内容: 一个OPTIONS预检请求,通常会包含如下请求头
请求头含义
Origin发起请求的源
Access-Control-Request-Method实际请求的 HTTP 方法
Access-Control-Request-Headers实际请求中使用的自定义头(如果有的话)

4.4. CORS 解决复杂请求跨域

  1. 第一步: 服务器先通过浏览器的预检请求, 服务器需要返回如下响应头
响应头含义
Access-Control-Allow-Origin允许的源
Access-Control-Allow-Methods允许的方法
Access-Control-Allow-Headers允许的自定义头
Access-Control-Max-Age预检请求的结果缓存时间(可选)

  1. 第二步: 处理实际的跨域请求(与处理简单请求跨域的方式相同)

服务端核心代码:

// 处理预检请求
app.options('/students', (req, res) => {
  // 设置允许的跨域请求源
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
  // 设置允许的请求方法
  res.setHeader('Access-Control-Allow-Methods', 'GET')
  // 设置允许的请求头
  res.setHeader('Access-Control-Allow-Headers', 'school')
  // 设置预检请求的缓存时间(可选)
  res.setHeader('Access-Control-Max-Age', 7200)
  // 发送响应
  res.send()
})

// 处理实际请求
app.get('/students', (req, res) => {
  // 设置允许的跨域请求源
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
  // 随便设置一个自定义响应头
  res.setHeader('abc',123)
  // 设置允许暴露给客户端的响应头
  res.setHeader('Access-Control-Expose-Headers', 'abc')
  // 打印请求日志
  console.log('有人请求/students了')
  // 发送响应数据
  res.send(students)
})

4.5. 借助 cors 库快速完成配置

上述的配置中需要自己配置响应头,或者需要自己手动封装中间件,借助cors库,可以更方便完成配置

  • 安装cors
npm i cors
  • 简单配置cors
app.use(cors())
  • 完整配置cors
// cors中间件配置
const corsOptions = {
  origin: 'http://127.0.0.1:5500', // 允许的源
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'], // 允许的方法
  allowedHeaders: ['school'], // 允许的自定义头
  exposedHeaders: ['abc'], // 要暴露的响应头,注意:不暴露,浏览器无法获取
  optionsSuccessStatus: 200 // 预检请求成功的状态码
};

app.use(cors(corsOptions)); // 使用cors中间件

5. JSONP 解决跨域问题

  1. JSONP 概述: JSONP 是利用了<script>标签可以跨域加载脚本,且不受严格限制的特性,可以说是程序员智慧的结晶,早期一些浏览器不支持 CORS 的时,可以靠 JSONP 解决跨域。
  2. 基本流程:
    • 第一步: 客户端创建一个<script>标签,并将其src属性设置为包含跨域请求的 URL,同时准备一个回调函数,这个回调函数用于处理返回的数据。
    • 第二步: 服务端接收到请求后,将数据封装在回调函数中并返回。
    • 第三步: 客户端的回调函数被调用,数据以参数的形势传入回调函数。
  1. 图示:

  1. 代码示例:

例子一:

image.png image.png

image.png

上述代码报错原因: 标签发的请求,标签认为回来的都是js,所以将hello看成一个js变量,就报hello没有定义

例子二: image.png

image.png image.png 例子三:

image.png

image.png

image.png

例子四:

image.png

image.png

image.png

注意:上述例子都是一进入页面就打印数据,如何点击按钮再打印

<button onclick="getTeachers()">获取数据</button>

<script type="text/javascript" >
  function callback(data){
    console.log(data)
  }

  function getTeachers(url){
    // 创建script元素
    const script = document.createElement('script')
    // 指定script的src属性
    script.src= 'http://127.0.0.1:8081/teachers'
    // 将script元素添加到body中触发脚本加载
    document.body.appendChild(script)
    /* script标签加载完毕后移除该标签,因为创建script的目的是获取callback函数携带的data的数据,此时已获取到*/
    script.onload = ()=>{
    //对原文档没有侵入就完成功能
      script.remove()
    }
  }
</script>

其中callback可以通过前端自定义,不固定

image.png

image.png

  1. jQuery 封装的 jsonp
$.getJSON('http://127.0.0.1:8081/teachers?callback=?',(data)=>{
  console.log(data)
})

好奇jQuery封装的callback是什么?打印一下看看

image.png

image.png

6. 配置代理解决跨域

6.1. 自己配置代理服务器

借助http-proxy-middleware配置代理

const { createProxyMiddleware } = require('http-proxy-middleware');

app.use('/api',createProxyMiddleware({
  target:'https://www.toutiao.com',
  changeOrigin:true,
  pathRewrite:{
    '^/api':''
  }
}))

6.2. 使用 Nginx 搭建代理服务器

参考之前的直播内容

6.3. 借助脚手架搭建服务器

参考之前的直播内容