非面试向跨域实践详解

前言

笔者常常在前端开源群答疑,加上以前的招聘面试经历。发现许多新手前端在问起跨域问题的解决方案,一套一套的,但是实际遇到跨域问题了就不知道怎么解决了。此次写这篇文章从实践角度聊一聊跨域问题。javascript

跨域基本概念

出于浏览器的同源策略限制,浏览器会拒绝跨域请求。 这就是跨域问题的产生缘由,同源策略是用于隔离潜在恶意文件的重要安全机制。css

这句话的三个关键字:html

  • 同源
  • 限制
  • 浏览器拒绝

什么是同源

那么第一个问题来了,什么算是同源。解决这个问题须要先了解一下URL的完整结构: 前端

URL结构
两个URL的才算同源。反而言之,三者任何一个不相同都算跨域。 例如某个页面地址为 www.domain.com/page1.html, 该页面访问如下API接口跨域关系表:

API 接口地址 是否跨域 缘由
www.domain.com/api/users/1 协议、主机、端口所有都相同
www.domain.com:80/api/users/1 端口不一样
www.baidu.com/api/users/1 协议不一样
api.domain.com/v1/users/1 主机不一样

有哪些限制

  1. XmlHttpRequest(即ajax请求)和Fetch两种接口发出的HTTP请求进行限制。
  2. 对于嵌入资源标签scriptimglinkvideo等标签加载资源的请求(HTTP GET请求)不作限制。

具体的限制规则还有不少,这里只说常见和本文用得上的。vue

浏览器拒绝

那么那些环境算是浏览器?java

  • PC端常见的 Chrome/Safari/Edge
  • 移动端的Chrome/Safari/各个App内嵌Webview 浏览器又是怎么拒绝的 先来看一张图,一个用户点击了一个按钮,发出了一个AJAX GET请求。那么常见的流程如图:

那么若是用户发出的AJAX GET请求是一个跨域请求,那么会在上图中哪个阶段被阻止? 可是第3阶段,也就是说用户发送的信息能够到达服务端,服务器是可以接受处理并返回了。返回的浏览器发现这是一个跨域请求。就直接拒绝,同时把返回的信息替换为报错信息,返回给JavaScript程序。 对于更复杂的POST/PUT等请求, MDN CORS文档里面有更详细的处理方法。这里就不细说。react

这一点很重要,可是老是被新人忽略。因此重要的事情说三遍,webpack

  • 拒绝跨域请求是浏览器
  • 拒绝跨域请求是浏览器
  • 拒绝跨域请求是浏览器

反过来讲,Nginx、Java/Nodejs等编程语言的HTTPClient以及手机App,他们发出的HTTP请求就彻底没有跨域问题,由于他们不是浏览器,没有实现W3C规范。ios

跨域解决方案

JSONP

在浏览器中假设有如下一段代码会执行结果会是什么样?nginx

<script> window.callback = function (data) { console.log(data); delete window.callback; } </script>
<script> callback({ "code": 1, "data": [1,2,3] }); </script>
复制代码

毫无疑问,确定是在控制台输出了一个对象信息。 记得刚才在介绍跨域基本概念的时候说个浏览器不限制script标签加载js文件。那么把这两者的特性相结合。第二个script标签改成从网络加载. 就能够实现跨域. 例如 一个跨域APIhttp://api.domain.com/v1/users/1

  1. 在window对象上挂载一个函数callbackFun
  2. 建立一个script标签: <script src="http://api.domain.com/v1/users/1?callback=callbackFun"></script>
  3. script就会向服务器发出 GET http://api.domain.com/v1/users/1?callback=callbackFun的请求
  4. 让后端返回以下内容 ContentType为application/javascript
callbackFun({/*须要的数据*/});
复制代码
  1. 数据返回成功之后处理数据,删除script标签

以上步骤就是JSONP的思想。实现一个完善的JSNOP请求库还有细节要处理,好比超时取消、回调函数防重名等。不少开源库(jQuery, axios)都实现了JSNOP请求。想要代码的去Github阅读源码,这里就不给出代码。

优劣势

JSONP虽然是一种实现跨域访问的方法,前端想要使用JSONP进行跨域访问却不容易。

  1. 只支持GET方法
  2. 要后端的配合 GET http://api.domain.com/v1/users/1 返回
ContentType:	application/json
复制代码
{
    "code": 1,
    "data" : {"userid": 1}
}
复制代码

GET http://api.domain.com/v1/users/1?callback=callbackFun 返回

ContentType:	application/javascript
复制代码
callbackFun({
    "code": 1,
    "data" : {"userid": 1}
});
复制代码

既然能够和后端商量配合你改造接口,那还有更好的方案能够解决。何须用这种方案。

JSONP有一个有点就是兼容性好,IE678统统兼容,因此通常JSNOP是后端同窗若是主动须要开放API给他人使用,同时有须要极高的兼容的一个妥协方案。通常状况下不推荐这个方案。

JSONP 开心一刻

真实经历。以前开发项目须要调用另外一个项目组的接口。 跨域形成接口掉不通,而后找Z君沟通, Z君说:"你用JSONP来掉接口就行了。这都不知道...." 而后我还在想大神这么NB的么,JSONP兼容都提早作完了。我试了JSONP。坑爹呀,你后端根本就没兼容JSONP,我怎么调用,呵呵... 呵呵呵呵....

请求代理

JSONP方案不推荐,那么又须要访问跨域接口,怎么办呢? 重要事情不在意再多说一遍拒绝跨域请求是浏览器。 那么若是有一个非W3C标准的HttpClient帮助咱们转发请求,不就能够了实现跨域访问了。

App端

一般App对于webview都有很强的控制权,能够在Webview的JS环境中注入一些方法。 那么移动端程序员能够在Webview中注入一个接口,运行在里面的js代码能够经过这个方法把本身的请求地址、请求参数、请求体等数据交给App Native端,让App Native代为收发请求。App Native不是浏览器,不受跨域限制。

具体实现方法能够搜 Hybird App开发或者请教移动端开发的同窗。

Web端

Web端必然运行在浏览器环境中,那么没有App Native。还有服务器上能够作反向代理。 所谓的反向代理,原理和App Native请求代理的原理差很少,就是咱们请求非跨域下的反向代理服务,反向代理服务会把你的请求转发给目标服务器。 反向代理服务能够是Nginx也能够是java/Nodejs程序等等。这些程序也不受跨域限制,能够接受目标服务器的请求,并返回给咱们。

React/Vue 开发阶段跨域处理

React/Vue 这种SPA开发施行的彻底的先后端分离的模式,开发阶段必然是须要跨域访问接口的。 Vue开发能够这样配置:

// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: '<url>',
        ws: true,
        changeOrigin: true
      },
      '/foo': {
        target: '<other_url>'
      }
    }
  }
}
复制代码

详情见Vue CLI文档。React也有相似的配置,详情见 create-react-app文档

那么他们具体是怎么实现的呢? 本地起一个服务端程序提供反向代理的能力。而React/Vue本地启动的这个服务端程序就是Webpack-dev-server。来探索Webpack-dev-server源码,源码中启动server的关键代码在lib/Server.js中,挑重点

/* 此处省略许多行代码 */

// 27行 引入express 做为服务端框架
const express = require('express');

/* 此处省略许多行代码 */


// 31行 引入 http-proxy-middleware 提供反向代理的能力
const httpProxyMiddleware = require('http-proxy-middleware');

/* 此处省略许多行代码 */

// 328行 获取 proxyMiddleware 并加载到为express的中间件Middleware
app.use((req, res, next) = > {
    if (typeof proxyConfigOrCallback === 'function') {
        const newProxyConfig = proxyConfigOrCallback();

        if (newProxyConfig !== proxyConfig) {
            proxyConfig = newProxyConfig;
            // 334行 根据 proxyConfig 获取 处理proxy请求的中间件proxyMiddleware
            proxyMiddleware = getProxyMiddleware(proxyConfig);
        }
    }

    const bypass = typeof proxyConfig.bypass === 'function';

    const bypassUrl = (bypass && proxyConfig.bypass(req, res, proxyConfig)) || false;

    if (bypassUrl) {
        req.url = bypassUrl;

        next();
    } else if (proxyMiddleware) {
        // 347行 最最关键一行 通过屡次断定某个请求是须要代理转发的请求,那么把它交给proxyMiddleware进行处理, proxyMiddleware
        return proxyMiddleware(req, res, next);
    } else {
        next();
    }
});
复制代码

以上代码有点NodeJS服务端开发的同窗基本能看明白,看不明白也不要紧。你知道React/Vue能够经过相应的配置项得到接口跨域访问的能力就能够了。其中最核心的就是依靠Express的网络请求能力充当反向代理服务器。

React/Vue 线上部署阶段跨域处理

开发阶段还能够经过本地启动一个Express服务器做为代理,帮助咱们处理跨域问题,问题是生产环境是不推荐这么作的。React/Vue 项目一般在build之后会生成如下文件:

  • xxx.html 文件1份
  • xxx.xxxxxx.js Javacript文件若干
  • xx.xxxx.css 文件若干
  • xxx.map 文件若干,固然也可能没有 并且里面的js/css/图片等文件一般部署在cdn上,最为要紧的页面入口index.html则须要当心部署,不然易遇到2个问题
  1. 页面没办法访问
  2. 接口跨域致使没办法访问

1

对于index.html的部署,Vue-Router文档写的很清楚。推荐经过nginx try-file命令来进行部署。同时nginx又是一个反向代理服务器。假设 网页须要在host http://www.domain.com/下, 真实API服务部署在http://api.domain.com/api。那么经过反向代理把接口代理到 http://www.domain.com/api下。那么跨域访问就变成了同域名访问。 那么nginx的配置文件能够这样写

server { 
    listen       80;
    server_name  www.domain.com ;
    root www; # 存放html文件的文件夹
    location ^/api { # 接口代理到 8080
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_pass       http://api.domain.com/api;
    }
    location / { # 其余请求返回index.html
        try_files $uri $uri/ /index.html;
    }
  }
复制代码

这样作完, 访问API就会被代理转发,访问其余路径就返回html。以下图所示:

线上部署的方式可能根据系统架构选型而多种多样。这只是其中一种比较通用且为官方推荐的方式。仅作参考。相似ngixn的服务端软件仍是Caddy、Envoy

这种方案的优势是不须要后端同窗改动接口,只须要运维小哥帮助配置一下nginx便可完成兼容。缺点是多一次转发可能带来性能损失。

CORS

实际状况多种多样,有些时候没办法使用JSONP,也经过nginx转发又会产生性能损失。那么还有一个终极大招———— CORS.

W3C的同源策略出来之后形成了不少不便,没法应对某些跨域访问的强需求。为此W3C增长了CORS相关的规范, 文档以前也说起过:MDN CORS文档

重要的事情再重复一遍:拒绝跨域请求是浏览器,那么CORS的原理就是CORS相关的规范中制定了一些响应头(Response Header),这些响应头以Access-Control-Request-开头。简单枚举几个,具体这些头的含义和用法见MDN CORS文档.

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
复制代码

浏览器在接收到设置过CORS响应头的返回之后,会根据CORS规范检查合法性,检查经过则再也不阻止,放行经过。

简而言之就是CORS响应头就是用来告诉浏览器:"我是虽然是跨域请求,可是我是合法的,请不要拒绝我"。

CORS方案的优势是支持各类方法 GET、POST、PUT、DELTE等等。并且改动量比较小。能够在服务端程序好比Java或者NodeJS上作,也可经过前置代理服务器nginx完成。

缺点就是

  1. 浏览器兼容性差
  2. 下降了安全性,毕竟W3C之因此禁止跨域,是为了安全。如今推出CORS方案虽然已经在安全和灵活方面作到一个较好平衡。可是若是CORS响应头设置不当,仍是可能会产生安全问题。

其余

其余还有用与父页面与子页面(iframe)之间的通讯的跨域问题,window.name、postMessage等方法。这里就不详细说了。平常用的确实很少,有须要再查把。

要点总结

  • 跨域的基本概念
    • 跨域是W3C组织为了保证安全指定的规范
    • 协议、主机、端口所有都相同才是同源,不然就是跨域
    • 限制XHR与Fetch,不限制资源类标签
    • 拒绝跨域请求是浏览器 拒绝跨域请求是浏览器 拒绝跨域请求是浏览器 重要事情说三遍
  • 常见跨域解决方案
    • JSONP 只能发出GET请求。通常不推荐,除非须要很强的接口兼容性
    • 访问代理
      • APP端能够经过Native端发请求
      • Web端能够经过架设反向代理服务器
        • React/Vue平常开发就是经过Express服务器作的反向代理
        • 生产环节部署可使用nginx
    • CORS是W3C准许跨域规范,须要后端配合改程序
    • 其余略过

生产环境中建议选择顺序是 反向代理 > CORS >> JSONP。 由于反向代理兼容性最好,程序改动少。 CORS适用于没法容忍反向代理的性能损失和第三方OpenAPI访问。 JSONP 只有当后端须要兼容性高,没办法部署反向代理服务器的状况以及前端访问第三方提供的JSONP接口。其余任何状况下不推荐。

相关文章
相关标签/搜索