跨域解决方案实践

同源策略

  • 什么时候会跨域:协议、域名、端口号三者有一个不同就是跨域。

  • 对跨域的辨析:跨域请求并非是浏览器限制了发起跨站请求,而是请求可以正常发起并到达目标服务器,但是服务器返回的结果会被浏览器拦截。

先来看一下不进行额外处理的跨域请求。

$.ajax({
  url: 'http://10.92.191.223:3000/test',
  success: function(data){
    console.log(data);
  }
})

意料之中的是,控制台会报 CORS 跨域的错误。

跨域解决方案

其他跨域方案可参考 10 种跨域解决方案(附终极方案)

JSONP

  • 原理:<script><link><img> 引入的资源不受同源策略限制,并且动态载入的 script 脚本会自动执行。所以以请求 script 脚本的方式来调用 api 即可(但需要后端配合格式化返回的数据)。

  • 优点:兼容性很好。

  • 缺点:

    • 只支持 GET 请求它(因为 script 脚本的请求方式就是 GET)。
    • 只支持跨域 HTTP 请求,不能解决不同域的两个页面之间如何进行 JavaScript 调用的问题。
  • 实现流程:创建一个 <script> 并载入页面中,src 是跨域的 api 接口地址,但后面需要带上一个标记有回调函数的请求参数,如 http://10.92.191.223:3000/test/?callback=handleCallback。后端接受到请求后需要进行特殊的处理,将回调函数名和数据拼接成一个函数调用的形式返回给前端,如 handleCallback({"status": "success", "message": "跨域成功"})。因为是 script 脚本,所以前端请求到这个脚本后会立即执行这个脚本内容,即调用这个回调函数。

// 前端代码
function jsonp(url, callback) {
  // 创建一个唯一的回调函数名称
  let fn = Symbol();
  // 先在 window 上定义这个回调函数,后端返回数据后会立即执行这个回调函数
  window[fn] = function(response) {
    try {
      callback(response);
    } finally {
      delete window[fn];
      document.body.removeChild(script);
    }
  };
  let script = document.createElement("script");
  script.type = 'text/javascript';
  // 判断 url 是否已经有其他的查询参数
  if(url.indexOf("?") === -1) {
    url += `?callback=${fn}`;
  } else {
    url += `&callback=${fn}`;
  }
  script.src = url;
  document.body.appendChild(script);
}

function handleCallback(data) {
  console.log('成功拿到后端返回的数据,并执行回调函数');
  console.log(data);
}

// 后端代码
const express = require('express')
const app = express()

app.get('/', (req, res) => res.send('Hello World!'))
app.get('/test', (req, res) => {
  res.end('handleCallback({"status": "success", "message": "跨域成功"})');
})

app.listen(3000, () => {
  console.log('Example app listening on port 3000!')
})

jQuery 的 JSONP

jQuery 也已经封装好了 JSONP ,而且使用十分简单,不需要我们再去写 JSONP 函数。使用方式如下。

// 前端代码
$.ajax({
  url: 'http://10.92.191.223:3000/test',
  dataType: 'jsonp',    // 指定服务器返回的数据类型
  // 不使用 jsonpCallback 指定回调函数名时,jQuery 会生成一个随机串来充当回调函数名,此时直接在 seccess 中处理返回的数据即可
  success: function(data){
    console.log("请求成功后的回调函数");
    console.log(data);
  }
})

// 后端代码
app.get('/test', (req, res) => {
  res.end(req.query.callback + '({"status": "success", "message": "跨域成功"})');
})

CORS

MDN
跨域资源共享 CORS 详解

  • 原理:在服务端设置 Access-Control-Allow-Origin 响应头,允许哪些域名可以访问资源。
res.header("Access-Control-Allow-Origin", "*");

与跨源有关的响应头字段

  • Access-Control-Allow-Origin:指定了允许访问该资源的外域 URI,设置为通配符 * 则表示所有网站都可以访问该资源。

  • Access-Control-Expose-Headers:指定浏览器可以使用或读取 response 中的 哪些响应头。在跨域访问时,XMLHttpRequest 对象的 getResponseHeader 方法只能拿到 6 个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma,如果要获取其他头部信息则需要服务器设置本响应头。

  • Access-Control-Allow-Headers:指明了实际请求中所允许携带的头部字段。

  • Access-Control-Allow-Methods:指明了实际请求中所允许使用的 HTTP 方法。

  • Access-Control-Allow-Credentials:表示是否允许浏览器发送 Cookie。该字段只能设置为 true, 表示服务器明确许可。如果服务器不允许浏览器发送 Cookie,删除该字段即可。

  • Access-Control-Max-Age:指定了预请求的请求结果能够被缓存多久,在此有效期内可以直接发起实际请求而不用先通过预请求确认。

默认情况下 CORS 请求不会携带 Cookie,如果要带 Cookie 的话,则需要满足以下几个要求才行:

  1. 服务器同意接受 Cookie,设置了 Access-Control-Allow-Credentials: true 字段。
  2. 服务器的 Access-Control-Allow-Origin 字段不能设置为通配符 *,必须指定为和请求网页一致的域名。
  3. 请求头需要设置 withCredentials: truecrossDomain: true
  4. 浏览器 Cookie 依然遵循同源政策,需要将 Domain 属性设置为相应服务器的域名。

简单请求

  • 满足以下两个请求就是简单请求,反之则是非简单请求:

    • 请求方法是以下三种方法之一:HEADGETPOST
    • HTTP的头信息不超出以下几个字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(只限于这几个值:application/x-www-form-urlencodedmultipart/form-datatext/plain
  • 简单请求的流程:

    • 浏览器直接发出 CORS 请求,也就是在请求头之中,增加一个 Origin 字段,表示本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
    • 如果 Origin 指定的源不在许可范围内,服务器会返回一个正常的 HTTP 回应。但浏览器发现响应头中没有包含 Access-Control-Allow-Origin 字段,浏览器就知道发生了跨域错误,于是抛出一个错误被 XMLHttpRequestonerror 回调函数捕获(也就是我们跨域时在控制台看到的那个报错)。注意这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是 200。
    • 如果 Origin 指定的域名在许可范围内,服务器返回的响应头就必须包含 Access-Control-Allow-Origin 字段,以及上述一些可选的其他字段。

非简单请求

  • 非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUTDELETE,或者 Content-Type 字段的类型是 application/json

  • 非简单请求的流程:

    • 非简单请求的 CORS 请求会在正式通信之前增加一次 HTTP 查询请求,也就是预请求。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 方法和请求头。只有得到肯定答复后浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。
    • 预请求使用的是 OPTIONS 请求方法(用来从服务器获取更多信息,不会对服务器资源产生影响),其中包含了 OriginAccess-Control-Request-MethodAccess-Control-Request-Headers 等几个请求头,分别表示请求的来源、稍后的 CORS 正式请求使用的请求方法和使用到的请求头。
    • 服务器收到预请求以后,基于上述的几个请求头来判断是否接受稍后的实际请求,确认允许本次跨域请求后就作出回应,包括:Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers,以及上述一些其他的可选字段。如果服务器否定了预请求,则返回一个正常的 HTTP 请求,但没有任何 CORS 相关的头信息字段,此时浏览器就会抛出 CORS 请求被拒绝的错误。

WebSocket

  • 原理:WebSocket 是一种双向通信协议,在建立连接之后 server 与 client 都能主动向对方发送或接收数据,并且不受同源策略限制。
// 前端代码
<script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
window.onload = function(){
  var socket = io('ws://localhost:3000');
  socket.on('connect', function() {
    console.log('客户端和服务端建立起连接');
  })
  socket.on('onclient', function(params) {
    console.log('客户端响应服务端的触发事件', params);
  })
  socket.emit('onserver', 'onserver');
}

// 后端代码
const http = require('http');
const server = http.createServer();
const socket = require('socket.io')(server);
server.listen(3000, () => {
  console.log('Example app listening on port 3000!')
})

socket.on('connection', function (client) {
  console.log('服务端监测到有客户端连接');
  client.on('onserver', function(params) {
    console.log('服务端响应客户端的触发事件', params);
  })
  client.emit('onclient', 'onclient');
})

中间件服务器

  • 原理:同源策略只是对浏览器的限制,服务器之间的请求不受同源策略的影响。所以可以开启一个中间件服务器,前端将 HTTP 请求发送到这个中间服务器上,由这个中间件转发请求到目标服务器上,再将收到的响应数据转发给请求主机。

下面的示例中,目标服务器是 http://10.92.191.223:3000,中间服务器是 http://10.92.191.223:3001

/* 前端请求代码 */
getApi() {
  axios({
    method: 'get',
    url: 'http://10.92.191.223:3001/test',
  }).then((res) => {
    console.log(res);
  }).catch((error) => {
    console.log(error);
  })
}

/* 中间服务器 */
const express = require('express');
const proxy = require('http-proxy-middleware');
const app = express();

app.all('*', function(req, res, next) {
  // 中间服务器需要开启 CORS,否则请求到中间服务器也会有跨域问题
  res.header("Access-Control-Allow-Origin", "*");
  next();
});
// 路径是 /test 的请求会被转发
app.use('/test', proxy({
  target: 'http://10.92.191.223:3000',
  changeOrigin: true,
}))
app.listen(3001, () => {
  console.log('Example app listening on port 3001!')
})

/* 目标服务器 */
const express = require('express');
const app = express();

app.get('/', (req, res) => res.send('Hello World!'))
app.get('/test', (req, res) => {
  res.json({"status": "success", "message": "跨域成功"});
})
app.listen(3000, () => {
  console.log('Example app listening on port 3000!')
})

  转载请注明: DangoSky 跨域解决方案实践

 上一篇
下拉刷新上拉加载更多的实现原理 下拉刷新上拉加载更多的实现原理
背景最近有一个需求:大致是要展示一个列表信息,但每次接口只返回 20 条数据,当用户滑动到到页面底部并继续上拉页面时再继续调接口获取更多的数据(相当于分页)。这里就需要使用到一个上拉加载更多的功能,实现的效果可以看这里。考虑到项目时间比较
2020-03-11
下一篇 
React Hook 中的闭包问题 React Hook 中的闭包问题
React Hook 中的闭包问题本文不对 React Hook 做过多的介绍,只是记录笔者在学习过程中遇到的问题。关于 React Hook 的介绍请参考官方文档,或者也可以看我的个人笔记。 闭包直接开门见山,通过代码来看问题。 fun
2019-11-16
  目录