聊聊前端跨域那些事儿

810 阅读6分钟

提到跨域我们就不得不说一下同源 ; 那什么又是同源呢 ?

同源策略 : 是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介

那什么样的情况会涉及到跨域呢 ?

一句话总结 : 只要 请求 协议, 域名, 端口, 任意一个不相同的地址, 都会涉及到跨域问题 ; 跨域也可以说不是一个问题, 是浏览器的一种现象 ;

跨域异常展示

下面我们通过一个简单的案例展示以下跨域异常请求 ;

可以看出没有涉及到跨域的请求可以正常获取到结果, 而涉及到跨域的就不能获取响应结果了 ; 既然出了问题, 就肯定要解决问题 ; 下面就介绍几种跨域的解决方案

jsonp 解决跨域

众所周知在 html 页面中 像 img script iframe 这些带有 src 属性的标签, 后面的引用是不会受到跨域影响的, jsonp的原理简单来说就是利用 html 页面中的 script 的 src 不受同源政策约束, 来跨域获取数据的 ;

// 服务端代码片段  koa
router.get('/getData', async ctx => {
  ctx.body = 'alert("jsonp")';
})
<script src="http://localhost:4000/getData"></script>

可以看出我们的script 是完全不受跨域影响的, 甚至可以定义变量, 数据来供前端去使用; 下面看具体实现

// 后端代码  koa
router.get('/getData', async ctx => {
  let returnData = {
    status: 1,
    data: {
      list: [
        { name: 'zs', age: 24 },
        { name: 'ls', age: 22 }
      ]
    }
  }
  // 返回出去一个回调函数, 这里的回调名称要和前端约定
  let jsonpStr = `${ctx.query.cb}(${JSON.stringify(returnData)})`
  ctx.body = jsonpStr
})
// 前端代码
btns[1].onclick = function () {
  $.ajax({
    type: 'get',
    dataType: 'jsonp',
    data: {}, // 如果需要传参在 data 中以 key value 形式传参; 参数会传递 url 中
    url: 'http://localhost:4000/getData',
    jsonp: 'cb',  // 这个回调名称要和后端约定
    success: function (res) {
      console.log(res.data)
    }
  })
}

此种方法成功解决了上面我们说的跨域问题, 但是 jsonp 并不是完美的 ;

  • 只支持 get 请求, 所有的请求参数只能方法哦 url 后面 即 ? 后面用参数拼接
  • 安全问题 : callback参数注入和资源访问授权设置

cors 跨域

CORS(Cross-origin resource sharing),跨域资源共享,是一份浏览器技术的规范,用来避开浏览器的同源策略

简单来说就是解决跨域问题的除了jsonp外的另一种方法;比jsonp更加优雅。

cors 跨域设置 , 主要工作在于后端的修改 ; 下面来看一下

同样在 3000服务器请求 5000服务器 肯定是报错了

大概意思就是请求被 CORS 策略阻止, 缺少一个请求头 ; 可以在后端加上这个请求头试下

app.use(async (ctx, next) => {
  /**
   * @param 请求头
   * @param 允许访问的地址, * 代表所有
   */
  ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000');
  await next();
});

漂亮, 解决了跨域问题, 真的这么简单吗 ; 此时我们再客户端加上请求头来再访问一次 ;

let xhr = new XMLHttpRequest()
xhr.open('get', 'http://localhost:5000/getData', true)
xhr.setRequestHeader('Content-Type', 'application/json;charset=utf8')
xhr.onload = function () {
  let res = JSON.parse(xhr.responseText)
  console.log(res)
}
xhr.send()

大概意思发送来的请求没有通过访问控制检查 ; 检查我们发送至后端的请求, 发现请求方式为 OPTIONS 原因就在这里了 ;

预检请求

“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响

什么样的请求 是预检请求呢 ? 有预检请求, 肯定就会有简单请求吧 ?

简单请求

GET
POST
HEAD
以上三种请求只要携带了 content-type 就是预检请求
text/plain
multipart/form-data
application/x-www-form-urlencoded

预检请求

PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH

既然已经知道了问题原因, 那就直接上解决方案吧 ;

// 判断是不是预检请求 如果是的话, 随便给点东西就行
if (ctx.method == 'OPTIONS') {
  ctx.body = '';
} else {
  await next();
}

此时再看, 同时发送的两个请求, 其中预检请求已经解决了 但是还有一个不行的; 好现在我们总结一下还有哪些问题没有解决 ; 下面上解决方案 ;

  • 带有请求头的请求没有给出响应 ;
  • 每次 request 都会给出两个响应 ;
app.use(async (ctx, next) => {
  // 允许客户端携带的请求头, 如果需要自定义, 可自行添加
  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With', 'your-Headers');
  if (ctx.method == 'OPTIONS') {
    // 用来指定本次预检请求的有效期, 在此期间不用发出另一条预检请求。
   ctx.set(ctx.set('Access-Control-Max-Age', 3600 * 24)); // 单位 s
    ctx.body = '';
  } else {
    await next();
  }
});

目前为止我们已经解决 get 方法的跨域问题 ; 为什么说是 get 呢, 不要急 ; 这里直接附 完整代码 吧 , 大家看注释;

app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000');
  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With', 'your-Headers');
  // 允许的请求方式
  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
  if (ctx.method == 'OPTIONS') {
   ctx.set(ctx.set('Access-Control-Max-Age', 3600 * 24)); // 单位 s
    ctx.body = '';
  } else {
    await next();
  }
});

后端代理方案解决跨域

跨域是浏览器规范 , 通过服务器去发送请求 , 绕过前端浏览器 , 也能解决浏览器限制 ;

koa-server-http-proxy 中间件实现代理

// 此种方式是当前服务器配置, 从当期服务器代理到其他服务器
const koaServerHttpProxy = require('koa-server-http-proxy');
app.use(koaServerHttpProxy({
  target: 'http://localhost:5000',  // 目标服务器
  pathRewrite: { '^/api': '' }
}));

axios 实现请求转发

// 当前服务
router.get('/axios', async ctx => {
  let { data } = await axios.get('http://localhost:5000/axios')
  ctx.body = data
})
// 目标服务
router.get('/axios', async ctx => {
  ctx.body = {
    status: 200,
    info: 'axios 转发请求成功...'
  }
})

Vue config 解决跨域

// 本地项目如果直接访问其他域名的接口, 可以预见到肯定是会出现跨域问题的
created() {
 axios.post('http://api.diopoo.com/v1/api/index/banner', {
  app: 'index',
  position: '1'
 }).then(res => this.res = res)
},

Vue 在开发环境中提供了一种跨域方式 , 我们需要在项目的根目录创建 vue.config.js 文件进行配置

/* 跨域页面 */
// 资源都代理到 /api 这里 所以用他访问
axios.post('/api/index/banner', {
 app: 'index',
    position: '1'
}).then(res => this.res = res)
/* vue.config.js */
devServer: {
  proxy: {
    '/api': {
      target: 'http://api.diopoo.com/v1/api',  // 代理的目标地址
      ws: true, // 求大佬评论区解释这个干吗的
      changeOrigin: true, // 允许跨域
      pathRewrite: {
        '^/api': '' // 把多余出来的 /api 替换成空
      }
    }
  }
}

webpack 跨域

vue.config.js 中内置了 webpack 的一些配置 , 既然它可以 那么 webpack 一定也是可行的 , 下面附上 webpack 基础配置 , 注意测试 webpack 一定要自己启动一个服务器 ; 同样也是只适用于开发环境的

其他他俩的配置是差不多的 , 大家可以感受下 , 重点看 proxy 和 index 中的代码

/* webpack.config.js */
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: 'main.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'main.js'
  },
  mode: 'development',
  devServer: {
    proxy: {
      '/api': {
        target: 'http://api.diopoo.com/v1/api', // 代理地址
        changeOrigin: true,  // 允许跨域
        pathRewrite: {
          '^/api': ''  // 替换
        }
      }
    }
  },
  plugins: [
    new HtmlWebpackPlugin()
  ]
}
/* index.js */
import axios from 'axios'
// 同样 /api 请求
axios.post('/api/index/banner', {
  app: 'index',
  position: '1'
}).then(res => console.log(res))

本文使用 mdnice 排版