Skip to content
On this page

解决跨域的方式

什么是跨域?怎么解决跨域?什么是预检请求?

跨域:协议、域名、端口有一个不同就是跨域。

浏览器出于安全考虑,使用了同源策略;同源策略是一种约定,它是浏览器最核心也最基本的安全功能。

那么是出于什么安全考虑才引入这种机制呢?主要是为了防止XSSCSRF 攻击。CSRF 攻击是利用用户的登录态发起恶意请求

也就是说,没有同源策略的情况下,A 网站可以被任意其他来源的 Ajax 访问内容,如果你当前 A 网站已经登录,那么对方就可以通过 Ajax 获得你的任何信息。

同源策略限制内容有:

  • CookieLocalStorageIndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了

但是有三个标签是允许跨域加载资源:

  • <img src=XXX>

  • <link href=XXX>

  • <script src=XXX>

请求跨域了,那么请求到底发出去没有?

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。通过表单的方式可以发起跨域请求,为什么 Ajax 就不会。因为归根结底,跨域是为了阻止用户读取另一个域名下的内容, Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF ,因为请求毕竟是发出了。

JSONP

JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。

JS
<script src="http://test/api?param1=ab&callback=jsonp"></script>
<script>
  function jsonp(data) {
    console.log(data)
	}
</script>

优点:简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题

缺点:仅支持 get 方法具有局限性,不安全可能会遭受 XSS 攻击

开发时遇到多个 JSONP 请求时, 可以封装一个

js
function jsonp(url, jsonpCallback, success) {
  let script = document.createElement('script')
  script.src = url
  script.async = true
  script.type = 'text/javascript'
  window[jsonpCallback] = function(data) {
    success && success(data)
  }
  document.body.appendChild(script)
}
jsonp('http://xxx', 'callback', function(value) {
  console.log(value)
})

CORS

CORS 需要浏览器和后端同时支持, IE8IE9 需要通过 XDomainRequest 实现。

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。

简单请求

Ajax 为例,当满足以下条件时,会触发简单请求

使用下列方法之一:

  • GET

  • HEAD

  • POST

Content-Type 的值仅限于下列三者之一:

  • text/plain

  • multipart/form-data

  • application/x-www-form-urlencoded

请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。

复杂请求

那么很显然,不符合以上条件的请求就肯定是复杂请求了。

对于复杂请求来说,首先会发起一个预检请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

对于预检请求来说,如果你使用过 Node 来设置 CORS 的话,可能会遇到过这么一个坑。

以下以 express 框架举例:

js
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS')
  res.header(
    'Access-Control-Allow-Headers',
    'Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials'
  )
  next()
})

该请求会验证你的 Authorization 字段,没有的话就会报错。

当前端发起了复杂请求后,你会发现就算你代码是正确的,返回结果也永远是报错的。因为预检请求也会进入回调中,也会触发 next 方法,因为预检请求并不包含 Authorization 字段,所以服务端会报错。

想解决这个问题很简单,只需要在回调中过滤 option 方法即可

js
res.statusCode = 204
res.setHeader('Content-Length', '0')
res.end()

document.domain

该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。

只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域

postMessage

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

js
// 发送消息端
window.parent.postMessage('message', 'http://test.com')
// 接收消息端
var mc = new MessageChannel()
mc.addEventListener('message', event => {
  var origin = event.origin || event.originalEvent.origin
  if (origin === 'http://test.com') {
    console.log('验证通过')
  }
})

websocket

WebsocketHTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。

WebSocketHTTP 都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocketserverclient 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 clientserver 之间的双向通信就与 HTTP 无关了。 原生 WebSocket API 使用起来不太方便,我们使用 Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口,也对不支持 webSocket 的浏览器提供了向下兼容。

我们先来看个例子:本地文件 socket.htmllocalhost:3000 发生数据和接受数据

html
<!-- socket.html -->
<script>
    let socket = new WebSocket('ws://localhost:3000');
    socket.onopen = function () {
      socket.send('码不停指'); // 向服务器发送数据
    }
    socket.onmessage = function (e) {
      console.log(e.data); // 接收服务器返回的数据
    }
</script>
js
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws'); // 记得安装 ws
let wss = new WebSocket.Server({ port:3000 });
wss.on('connection',function(ws) {
  ws.on('message', function (data) {
    console.log(data);
    ws.send('imondo.cn')
  });
})

反向代理

通过一些代理配置实现跨域请求

  • nginx

现在生产环境上使用最广泛的就是 nginx, 反向代理, 请求至真实的后端请求

bash
location /api {
    proxy_pass http://www.test.com/api; # 反向代理到后端服务
    proxy_redirect off;
  }

开发环境下使用 webpackdevSerer 配置

js
// webpack
devServer: {
  proxy: {
    '/api/*': {
      target: 'http://test.com/devops/',
      changeOrigin: true,
      pathRewrite: {
        '^/api': '/'
      }
    }
  }
}

viteserver 配置

js
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' 的实例
        }
      },
      // Proxying websockets or socket.io
      '/socket.io': {
        target: 'ws://localhost:3000',
        ws: true
      }
    }
  }
})

一些问题

  1. jsonp 没有问题,如果后台不配合怎么办?这种方式可能只会在一些老的工程中会出现。
  2. cors,但是在联调后台还是不愿意设置,特别要考虑到大公司这种环境。
  3. postmessage, websocket 这种通信模式根本不适合类假于 ajax 这种模拟跨遇方式。
  4. 可能你会想到介入 node 中间件进行一个代理。做一个 proxy 的二次请求。如果工程是在一个难以集成环境或者不容易集成的环境的情况下。还有什么方案?
  5. 你可能会写到用 nginx,但是不代表每个前端都会 nginx
  6. 终级办法用关闭浏览器跨域策略。但是可能面经会问其根本原因。浏览器发送了本质上是返回了。正是因为 options 的预检导致的问题。所以关闭浏览器跨遇策略就是关闭了预检

参考