Skip to content

跨域 ​

因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名、端口有一个不同就是跨域,Ajax 请求会失败。

我们可以通过以下几种常用方法解决跨域的问题

JSONP ​

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

涉及到的端

JSONP 需要服务端和前端配合实现。

html
<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
  function jsonp(data) {
    console.log(data)
  }
</script>
<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
  function jsonp(data) {
    console.log(data)
  }
</script>

JSONP 使用简单且兼容性不错,但是只限于 get 请求

具体实现方式

  • 在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 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)
})
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 (Cross-Origin Resource Sharing,跨域资源共享) 是目前最为广泛的解决跨域问题的方案。方案依赖服务端/后端在响应头中添加 Access-Control-Allow-* 头,告知浏览器端通过此请求

涉及到的端

CORS 只需要服务端/后端支持即可,不涉及前端改动

  • CORS需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
  • 浏览器会自动进行 CORS 通信,实现CORS通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
  • 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源

只要后端实现了 CORS,就实现了跨域

img

以 koa框架举例

添加中间件,直接设置Access-Control-Allow-Origin请求头

js
app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', '*')
  ctx.set(
    'Access-Control-Allow-Headers',
    'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild',
  )
  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS')
  if (ctx.method == 'OPTIONS') {
    ctx.body = 200
  } else {
    await next()
  }
})
app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', '*')
  ctx.set(
    'Access-Control-Allow-Headers',
    'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild',
  )
  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS')
  if (ctx.method == 'OPTIONS') {
    ctx.body = 200
  } else {
    await next()
  }
})

具体实现方式

CORS 将请求分为简单请求(Simple Requests)和需预检请求(Preflighted requests),不同场景有不同的行为

  • 简单请求

    :不会触发预检请求的称为简单请求。当请求满足以下条件时就是一个简单请求:

    • 请求方法:GET、HEAD、POST。

    • 请求头:Accept、Accept-Language、Content-Language、Content-Type

      • Content-Type 仅支持:application/x-www-form-urlencoded、multipart/form-data、text/plain
  • 需预检请求:当一个请求不满足以上简单请求的条件时,浏览器会自动向服务端发送一个 OPTIONS 请求,通过服务端返回的Access-Control-Allow-* 判定请求是否被允许

CORS 引入了以下几个以 Access-Control-Allow-* 开头:

  • Access-Control-Allow-Origin 表示允许的来源
  • Access-Control-Allow-Methods 表示允许的请求方法
  • Access-Control-Allow-Headers 表示允许的请求头
  • Access-Control-Allow-Credentials 表示允许携带认证信息

当请求符合响应头的这些条件时,浏览器才会发送并响应正式的请求

nginx 反向代理 ​

反向代理只需要服务端/后端支持,几乎不涉及前端改动,只用切换接口即可

nginx 配置跨域,可以为全局配置和单个代理配置(两者不能同时配置)

  1. 全局配置,在nginx.conf文件中的 http 节点加入跨域信息
bash
http {
  # 跨域配置
  add_header 'Access-Control-Allow-Origin' '$http_origin' ;
  add_header 'Access-Control-Allow-Credentials' 'true' ;
  add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
  add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;
}
http {
  # 跨域配置
  add_header 'Access-Control-Allow-Origin' '$http_origin' ;
  add_header 'Access-Control-Allow-Credentials' 'true' ;
  add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
  add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;
}
  1. 局部配置(单个代理配置跨域), 在路径匹配符中加入跨域信息
bash
server {
  listen       8080;
  server_name  server_name;

  charset utf-8;

  location / {
    # 这里配置单个代理跨域,跨域配置
    add_header 'Access-Control-Allow-Origin' '$http_origin' ;
    add_header 'Access-Control-Allow-Credentials' 'true' ;
    add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
    add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;

    #配置代理 代理到本机服务端口
    proxy_pass http://127.0.0.1:9000;
    proxy_redirect   off;
    proxy_set_header Host $host:$server_port;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}
server {
  listen       8080;
  server_name  server_name;

  charset utf-8;

  location / {
    # 这里配置单个代理跨域,跨域配置
    add_header 'Access-Control-Allow-Origin' '$http_origin' ;
    add_header 'Access-Control-Allow-Credentials' 'true' ;
    add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
    add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;

    #配置代理 代理到本机服务端口
    proxy_pass http://127.0.0.1:9000;
    proxy_redirect   off;
    proxy_set_header Host $host:$server_port;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

Node 中间层接口转发 ​

js
const router = require('koa-router')()
const rp = require('request-promise')

// 通过node中间层转发实现接口跨域
router.post('/github', async (ctx, next) => {
  let { category = 'trending', lang = 'javascript', limit, offset, period } = ctx.request.body
  lang = lang || 'javascript'
  limit = limit || 30
  offset = offset || 0
  period = period || 'week'
  let res = await rp({
    method: 'POST',
    // 跨域的接口
    uri: `https://e.juejin.cn/resources/github`,
    body: {
      category,
      lang,
      limit,
      offset,
      period,
    },
    json: true,
  })

  ctx.body = res
})

module.exports = router
const router = require('koa-router')()
const rp = require('request-promise')

// 通过node中间层转发实现接口跨域
router.post('/github', async (ctx, next) => {
  let { category = 'trending', lang = 'javascript', limit, offset, period } = ctx.request.body
  lang = lang || 'javascript'
  limit = limit || 30
  offset = offset || 0
  period = period || 'week'
  let res = await rp({
    method: 'POST',
    // 跨域的接口
    uri: `https://e.juejin.cn/resources/github`,
    body: {
      category,
      lang,
      limit,
      offset,
      period,
    },
    json: true,
  })

  ctx.body = res
})

module.exports = router

Proxy ​

如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象

通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果 web 应用和接口服务器不在一起仍会跨域

在vue.config.js文件,新增以下代码

js
module.exports = {
  devServer: {
    host: '127.0.0.1',
    port: 8080,
    open: true, // vue项目启动时自动打开浏览器
    proxy: {
      '/api': {
        // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
        target: 'http://xxx.xxx.xx.xx:8080', //目标地址,一般是指后台服务器地址
        changeOrigin: true, //是否跨域
        pathRewrite: {
          // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
          '^/api': '',
        },
      },
    },
  },
}
module.exports = {
  devServer: {
    host: '127.0.0.1',
    port: 8080,
    open: true, // vue项目启动时自动打开浏览器
    proxy: {
      '/api': {
        // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
        target: 'http://xxx.xxx.xx.xx:8080', //目标地址,一般是指后台服务器地址
        changeOrigin: true, //是否跨域
        pathRewrite: {
          // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
          '^/api': '',
        },
      },
    },
  },
}

通过axios发送请求中,配置请求的根路径

js
axios.defaults.baseURL = '/api'
axios.defaults.baseURL = '/api'

此外,还可通过服务端实现代理请求转发,以express框架为例

js
var express = require('express')
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false }))
module.exports = app
var express = require('express')
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false }))
module.exports = app

websocket ​

webSocket本身不存在跨域问题,所以我们可以利用webSocket来进行非同源之间的通信

原理:利用webSocket的API,可以直接new一个socket实例,然后通过open方法内send要传输到后台的值,也可以利用message方法接收后台传来的数据。后台是通过new WebSocket.Server({port:3000})实例,利用message接收数据,利用send向客户端发送数据。具体看以下代码:

js
function socketConnect(url) {
  // 客户端与服务器进行连接
  let ws = new WebSocket(url) // 返回`WebSocket`对象,赋值给变量ws
  // 连接成功回调
  ws.onopen = (e) => {
    console.log('连接成功', e)
    ws.send('我发送消息给服务端') // 客户端与服务器端通信
  }
  // 监听服务器端返回的信息
  ws.onmessage = (e) => {
    console.log('服务器端返回:', e.data)
    // do something
  }
  return ws // 返回websocket对象
}
let wsValue = socketConnect('ws://121.40.165.18:8800') // websocket对象
function socketConnect(url) {
  // 客户端与服务器进行连接
  let ws = new WebSocket(url) // 返回`WebSocket`对象,赋值给变量ws
  // 连接成功回调
  ws.onopen = (e) => {
    console.log('连接成功', e)
    ws.send('我发送消息给服务端') // 客户端与服务器端通信
  }
  // 监听服务器端返回的信息
  ws.onmessage = (e) => {
    console.log('服务器端返回:', e.data)
    // do something
  }
  return ws // 返回websocket对象
}
let wsValue = socketConnect('ws://121.40.165.18:8800') // websocket对象

document.domain(不常用) ​

  • 该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
  • 只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域
  • 自 Chrome 101 版本开始,document.domain 将变为可读属性,也就是意味着上述这种跨域的方式被禁用了

postMessage(不常用) ​

在两个 origin 下分别部署一套页面 A 与 B,A 页面通过 iframe 加载 B 页面并监听消息,B 页面发送消息

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

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('验证通过')
  }
})
// 发送消息端
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('验证通过')
  }
})

window.name(不常用) ​

主要是利用 window.name 页面跳转不改变的特性实现跨域,即 iframe 加载一个跨域页面,设置 window.name,跳转到同域页面,可以通过 $('iframe').contentWindow.name 拿到跨域页面的数据

实例说明

比如有一个www.example.com/a.html页面。需要通过a.html页面里的js来获取另一个位于不同域上的页面www.test.com/data.html中的数据。

data.html页面中设置一个window.name即可,代码如下

html
<script>
  window.name = '我是data.html中设置的a页面想要的数据'
</script>
<script>
  window.name = '我是data.html中设置的a页面想要的数据'
</script>
  • 那么接下来问题来了,我们怎么把data.html页面载入进来呢,显然我们不能直接在a.html页面中通过改变window.location来载入data.html页面(因为我们现在需要实现的是a.html页面不跳转,但是也能够获取到data.html中的数据)
  • 具体的实现其实就是在a.html页面中使用一个隐藏的iframe来充当一个中间角色,由iframe去获取data.html的数据,然后a.html再去得到iframe获取到的数据。
  • 充当中间人的iframe想要获取到data.html中通过window.name设置的数据,只要要把这个iframe的src设置为www.test.com/data.html即可,然后a.html想要得到iframe所获取到的数据,也就是想要得到iframe的widnow.name的值,还必须把这个iframe的src设置成跟a.html页面同一个域才行,不然根据同源策略,a.html是不能访问到iframe中的window.name属性的
html
<!-- a.html中的代码 -->
<iframe id="proxy" src="http://www.test.com/data.html" style="display: none;" onload="getData()">
  <script>
    function getData(){
      var iframe = document.getElementById('proxy);
      iframe.onload = function(){
        var data = iframe.contentWindow.name;
        //上述即为获取iframe里的window.name也就是data.html页面中所设置的数据;
      }
      iframe.src = 'b.html'; //这里的b为随便的一个页面,只有与a.html同源就行,目的让a.html等访问到iframe里的东西,设置成about:blank也行
    }
  </script>
</iframe>
<!-- a.html中的代码 -->
<iframe id="proxy" src="http://www.test.com/data.html" style="display: none;" onload="getData()">
  <script>
    function getData(){
      var iframe = document.getElementById('proxy);
      iframe.onload = function(){
        var data = iframe.contentWindow.name;
        //上述即为获取iframe里的window.name也就是data.html页面中所设置的数据;
      }
      iframe.src = 'b.html'; //这里的b为随便的一个页面,只有与a.html同源就行,目的让a.html等访问到iframe里的东西,设置成about:blank也行
    }
  </script>
</iframe>

上面的代码只是最简单的原理演示代码,你可以对使用 js 封装上面的过程,比如动态的创建iframe,动态的注册各种事件等等,当然为了安全,获取完数据后,还可以销毁作为代理的iframe

扩展阅读 ​

跨域与监控

前端项目在统计前端报错监控时会遇到上报的内容只有 Script Error 的问题。这个问题也是由同源策略引起。在 <script> 标签上添加 crossorigin="anonymous" 并且返回的 JS 文件响应头加上 Access-Control-Allow-Origin: * 即可捕捉到完整的错误堆栈

跨域与图片

前端项目在图片处理时可能会遇到图片绘制到 Canvas 上之后却不能读取像素或导出 base64 的问题。这个问题也是由同源策略引起。解决方式和上文相同,给图片添加 crossorigin="anonymous" 并在返回的图片文件响应头加上 Access-Control-Allow-Origin: * 即可解决