常见的跨域解决办法

1,175 阅读6分钟

跨域问题的由来

跨域问题的根源都是浏览器的同源策略的限制,它用于限制网站或者加载的脚本与其他网站进行资源交互,这样能够有效帮助阻拦恶意文件,保护用户的安全。

同源策略规定,只有两个页面具有同样的协议 protocol 、主机名 host 、端口号 port 才称为同源能够直接进行交互,三者有任意一种不同就会造成跨域问题

其实即使跨域了,ajax请求也并未被拦截而是成功发送到服务端,服务端正常处理请求后返回资源,浏览器接收到资源后一看,当前网页与请求地址不同源,拒绝将服务端返回的资源传递给代码。所以跨域问题是发生在浏览器的,与网络请求没有关系

同源策略的限制

如果两个网页不同源则

  1. 无法读取 cookielocalstorageIndexedDB
  2. 无法获取或操作另一个源的DOM
  3. 无法发送 ajax 请求

跨域的解决办法

一、 JSONP跨域

JSONP 是服务器与客户端通信的一种简单方法,其主要原理就是利用 script 标签的 src属性能过跨域访问

简单来说就是通过在页面添加一个 script 标签向服务端发送请求,服务端接收请求时,将数据放到指定的回调函数参数中传回来

jsonp 只支持 get 请求( script 标签就是 get 请求)并不支持 postput 等其他请求方式

// 服务端配置
// 用express简单搭建一个服务器
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  // 从query中拿取回调函数名,以及相应数据
  const callback = req.query.callback;
  const name = req.query.name || "hello world";
  res.send(`${callback}('${name}')`);
  res.end();
})
// 监听3000端口
app.listen(3000, () => {
  console.log("listen 3000");
});
<!-- 前端代码 -->
<h1 id="test"></h1>
<script>
  function sendAjax(name) {
    // 定义callback名(可以随意)
    const callback = "doSomething"
    // 在全局定义这个函数
    window[callback] = function (data) {
      document.querySelector('#test').innerHTML = data;
      // 用完后移除
     delete window[callback];
    }
    // 创建一个script标签,设置好src后添加到head中
    const script = document.createElement('script');
    script.src = `http://localhost:3000?name=${name}&callback=${callback}`;
    document.head.appendChild(script);
    // 移除script标签
    document.head.removeChild(script);
  }
  sendAjax("123");
</script>

二、cors 跨域

CORS是基于http1.1的一种跨域解决方案,它的全称是Cross-Origin Resource Sharing,跨域资源共享。

总体思路就是:如果浏览器要跨域访问服务器的资源,需要得到服务器的许可。

针对不同的请求,cors规定了几种不同的交互模式

  1. 简单请求
  2. 复杂请求(需要先发送预检请求)

简单请求

简单请求的判断

  1. 请求方法为以下的一种
  • get
  • post
  • head
  1. 请求头仅包含安全字段,常见安全字段
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (仅能为以下值 text/plainmultipart/form-dataapplication/x-www-form-urlencoded)

满足以上条件则视为简单请求。

简单请求交互

  1. 在请求头中加入origin字段

例如在页面 http://127.0.0.1:5500 有以下代码造成了跨域

// 端口号不同造成跨域问题
fetch("http://127.0.0.1:3000");
<!-- 请求头的部分信息 -->
Connection: keep-alive
Host: localhost:3000
...
Origin: http://127.0.0.1:5500
Referer: http://127.0.0.1:5500/index.html

origin 字段会告诉服务器是哪个源地址在请求服务器

  1. 服务器响应头中包含 Access-Control-Allow-Origin 字段

当服务器接收到请求后,对请求头中的origin进行判断后,如果允许访问,需要在响应头中添加Access-Control-Allow-Origin 字段 值可以是

  • * : 表示允许任何源访问
  • 具体的源 : 例如http://127.0.0.1:5500,表示仅允许当前源访问

复杂请求交互

当浏览器认为这不是一个简单请求时,会按照下列步骤进行请求

  • 浏览器先发送预检请求(opption),询问服务器是否允许
  • 服务器进行判断,允许访问
  • 浏览器发送真实请求
  • 服务器完成真实响应

例如在页面 http://127.0.0.1:5500 有以下代码造成了跨域

fetch("http://localhost:3000/test", {
  // post方法
  method: "post",
  // 自定义请求头
  headers: {
    name: "Nt",
    "content-type": "application/json"
  },
  // 请求体
  body: JSON.stringify({name:"CORS"}),
})
  1. 浏览器发送预检请求
预检请求体(方法为 OPTIONS )
Access-Control-Request-Headers: content-type,name
Access-Control-Request-Method: POST
Connection: keep-alive
Host: localhost:3000
...
Origin: http://127.0.0.1:5500
Referer: http://127.0.0.1:5500/index.html

预检请求它的目的是询问服务器是否允许后续的真实请求,它包含了后面真实请求的相关信息,所有预检请求有以下特征

  • 请求方法为 OPTIONS
  • 没有请求体
  • 请求头中包含
    • origin:发出请求的源
    • Access-Control-Request-Headers:后续真实请求会改动的请求头
    • Access-Control-Request-Method:后续真实请求的方法
  1. 服务器允许 服务器收到预检请求后,可以检查预检请求中的信息,如果允许访问,需要做如下响应
...
Access-Control-Allow-Headers: content-type,name
Access-Control-Allow-Method: POST
Access-Control-Allow-Origin: http://127.0.0.1:5500
...

对于预检请求,服务度不需要响应任何消息体,只需在响应头中添加

  • Access-Control-Allow-Headers:允许真实请求改动的请求头
  • Access-Control-Allow-Method:允许真实请求的请求方法
  • Access-Control-Allow-Origin:与简单请求相同表示允许访问的源
  1. 浏览器发送真实请求,服务端响应真实请求

预检请求后,浏览器就会发送真实请求,后续的请求处理与简单请求相同

// 手动实现简单cors
// 这里还是使用express框架

// 定义一个中间件
app.use((req, res, next) => {
  // 如果请求方法是options那该请求就是复杂请求
  if (req.method.toLocaleLowerCase() === "options") {
    // 读取请求头中的相关信息
    const nextHead = req.headers['access-control-request-headers'];
    const nextMethod = req.headers['access-control-request-method'];
    // 拿到信息后可以进行一些判断,这里我就直接放行了
    res.setHeader('Access-Control-Allow-Headers', nextHead);
    res.setHeader('Access-Control-Allow-Method', nextMethod);
  }
  // 简单请求
  // Origin字段无论简单请求还是复杂都需要配置
  // 从请求头中获取origin
  const nextOrigin = req.headers.origin;
  // 这里我还是直接放行了
  res.setHeader('Access-Control-Allow-Origin', nextOrigin);
  next();
})

三、websocket 跨域

WebSocketHTML5 中新增的协议,支持持久性连接,解决了 HTTP协议 通信只能由客户端发起的缺陷。

如何建立websocket连接

客户端需通过http请求与WebSocket服务端协商升级协议,协议升级完成后,后续的数据交换遵照WebSocket的协议。 请求头中会一些特征字段

<!-- 表示协议需要升级 -->
Connection: Upgrade
<!-- 请求扩展 -->
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
<!-- 与后续服务端响应首部Sec-WebSocket-Accept配套,提供安全保护 -->
Sec-WebSocket-Key: A+C1VOMnuprTFHwx7R0bUA==
<!-- websocket版本 -->
Sec-WebSocket-Version: 13
<!-- 表示要升级到websocket协议 -->
Upgrade: websocket

服务端接收到返回

Connection: Upgrade
<!-- 与Sec-WebSocket-Key配套 -->
Sec-WebSocket-Accept: 2zJTRh8A8UEc9ZttNOE9ilY73gg=
Upgrade: websocket

至此协议升级成功后续请求通过websocket发送 websocket协议不存在跨域问题,不需要任何额外的配置就能进行跨域访问

四、nginx代理

Nginx作为反向代理服务器,就是把http请求转发到另一个或者一些服务器上。通过把本地一个url前缀映射到要跨域访问的web服务器上,从而实现实现跨域访问。