跨域的几种方式

跨域问题

现在绝大多数公司的项目都是前后端分离的,前后端分离后势必会遇到跨域问题。如下图

分析一下此时都发生了什么?

  • 我的地址是:http://localhost:3001/
  • 需要访问的地址是:http://localhost:8000/api/...
  • 上图,代表我已经发出了请求信息,而且后端也收到了请求,也返回结果了,但是浏览器将结果拦截了,并且报错。

同源策略

同源策略:是一种约定,它是浏览器最核心也最基本的安全功能。
只要控制台出现 Access-Control-Allow-Origin 就是跨域了。
同源和跨域的详细解释

解决跨域问题的几种方案:

  • CORS 跨域资源共享 【很常用】
    规范化的跨域请求解决方案,安全可靠。
    优势:
    在服务端进行控制是否允许跨域,可自定义规则
    支持各种请求方式
    缺点:
    会产生额外的请求

  • proxy 服务器代理 【最常用】

    • React、Vue 举例
    • 自己基于node实现
  • JSONP
    最早的解决方案,利用script标签可以跨域的原理实现。
    限制:
    需要服务的支持
    只能发起GET请求

  • nginx 反向代理
    思路是:利用nginx反向代理把跨域变为不跨域,支持各种请求方式
    缺点:需要在nginx进行额外配置,语义不清晰

  • 修改本地 hosts 文件

  • 其它跨域技术

1 CROS

  CORS:是一个W3C标准,全称是“跨域资源共享”(Cross-origin resource sharing)
  浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,特殊请求的时还会多出一次附加请求,但用户不会有感觉,因此,实现CORS通信的关键是服务器
  只要服务器实现了CORS接口,就可以跨源通信。

1.1 简单请求

只要同时满足以下两大条件,就属于简单请求。:
(1) 请求方法是以下三种方法之一:HEAD、GET、POST
(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

当浏览器发现发现的ajax请求是简单请求时,会在请求头中携带一个字段:Origin
Origin中会指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域。
如果服务器允许跨域,需要在返回的响应头中携带下面信息:

Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
  • Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*,代表任意
  • Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true
    注意:
    如果跨域请求要想操作cookie,需要满足3个条件:
  • 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。
  • 浏览器发起ajax需要指定withCredentials 为true
  • 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名

1.2 特殊请求

不符合简单请求的条件,会被浏览器判定为特殊请求,例如请求方式为PUT。
特殊请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

一个“预检”请求的样板:

OPTIONS /cors HTTP/1.1
Origin: https://www.jianshu.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.leyou.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

与简单请求相比,除了Origin以外,多了两个头:

  • Access-Control-Request-Method:接下来会用到的请求方式,比如PUT
  • Access-Control-Request-Headers:会额外用到的头信息

服务的收到预检请求,如果许可跨域,会发出响应:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: https://www.jianshu.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

除了Access-Control-Allow-Origin和Access-Control-Allow-Credentials以外,这里又额外多出3个头:

  • Access-Control-Allow-Methods:允许访问的方式
  • Access-Control-Allow-Headers:允许携带的头
  • Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了

如果浏览器得到上述响应,则认定为可以跨域,后续就跟简单请求的处理是一样的了。

1.3 看代码

1.3.1 fetch 跨域请求举例:

前端代码:

// GET
fetch('http://localhost:6888/test_get?token=xxx&id=xxx',{ // 参数放在路径的查询信息里
    method: 'GET',
    mode: 'cors', // cors 方式跨域
}).then(res => {
    return res.json();
}).then(json => {
    console.log('获取的结果', json.data);
    return json;
}).catch(err => {
    console.log('请求错误', err);
})

// POST
fetch('http://localhost:6888/test_post',{
    method: 'POST',
    body: JSON.stringify({name: 'zaozuo'}),
    mode: 'cors',
}).then(res => {
    return res.json();
}).then(json => {
    console.log('获取的结果', json.data);
    return json;
}).catch(err => {
    console.log('请求错误', err);
})

2 proxy 服务器代理

通过服务器的文件能访问第三方数据。这个服务器文件又和当前请求的页面同源。这个时候,当前请求的页面去访问服务器文件,就等同于直接请求第三方资源。

2.1 React 中安装 webpack-dev-server 包,来配置代理

webpack.config.js 文件中设置 proxy

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    mode: 'production',
    entry: './src/main.js',
    output: {
        filename: 'main.[hash].min.js',
        path: path.resolve(__dirname, 'build')
    },
    devServer: {
        port: '3000',
        compress: true,
        open: true,
        hot: true,
        proxy: {
            '/': {
                target: 'http://127.0.0.1:3001',
                changeOrigin: true
            }
        }
    },
    // 配置WEBPACK的插件
    plugins: [
        new HtmlWebpackPlugin({
            template: `./public/index.html`,
            filename: `index.html`
        })
    ]
};

2.2 vue.config.js

module.exports = {
  devServer: {
    //proxy: '<url>'
    proxy: {
      '/api': {
        target: '<url>',
        ws: true,
        changeOrigin: true
      }
    }
  }
}

2.3 使用中间件

  在Vue、React中:(课件、视频:2019-02-18)
    react-create-app 跨域 通过 porxy 去跨域:
    1 找到 DevServer 文件 —>start.js
    2 里面引入 setupProxy.js (跨域的中间件)
      require('../src/setupProxy')(devServer);
      setupProxy文件中有:       

const proxy = require("http-proxy-middleware");
module.exports = function(app) {
  app.use(()=>{
    proxy("/api",{ 
      target:'跨域的地址',
      changeOrigin:true
    });
  })
}

    3可以用 axios ,也可以用 fetch, .....        

axios.get('/api?json=true')
.then(e=>{
  console.log(e);
});

2.4 自己基于 node 实现

比如说此时前端开发的本地地址是:172.0.0.1:8000, 需要访问https://www.jianshu.com/asimov/subscriptions/recommended_collections,这里肯定跨域了。
我们可以编写一个 server.js 文件

/*-CREATE SERVER-*/
const express = require('express'),
    app = express();
app.listen(1001, () => {
    console.log(`THE WEB SERVICE IS CREATED SUCCESSFULLY AND IS LISTENING TO THE PORT:1001`);
});

// 代理
const request = require('request');
app.get('/subscriptions/recommended_collections', (req, res) => {
    let jianURL = `https://www.jianshu.com/asimov${req.url}`;
    req.pipe(request(jianURL)).pipe(res);
});

/* STATIC WEB */
app.use(express.static('./'));

运行此文件 node server.js 开启 node 服务器。

前端向 node 服务器访问 /subscriptions/recommended_collections 这段地址,node 服务器会自动拼接上https://www.jianshu.com/asimov实现跨域,正常开发的时候基本不会用到这种方式,这里只做了解。

3 JSONP

JSONP是(json+padding) 把数据内填充起来。其实就是函数调用。长得像这样:callback({"key": "value"}) <所有浏览器都兼容>
<script src = " "> 会尽可能解析javaScript代码(引入的文件,和后缀名关系不大,和里面的代码关系才大。比如可以引入txt / ese...都是可以读的)
script 标签中的 src 能够直接跨域访问资源,并且尽量解析 js 代码。
jsonp 必须具备以下条件:
1. 保证全局有个函数
2. 数据必须是这个函数的调用格式
3. 当要请求数据的时候,创建一个script标签,把src等于请求的接口,再把script标签插入到页面,这个时候就做到了按需请求。
例子:

function handleResponse(data) {
  console.log(data);
};
//window.onload = function () {
  document.onclick = function () {
      let oS = document.createElement('script');
      oS.src = 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=小燕子&cb=handleResponse';
      //oS.src = 'http://localhost/jsonp?callback=fn2';
      document.getElementsByTagName('head')[0].appendChild(oS);
      oS.remove();
  }
//}

JSONP 之所以在开发人员中极为流行,主要原因是它非常简单易用。与图像 Ping 相比,它的优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。不过,JSONP 有又两点不足。
首先,JSONP 是从其他域中加载代码执行。如果其它域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃 JSONP 调用之外,没有办法追究。因此在使用不是你追究运维的 Web 服务时,一定要保证它安全可靠。
其次,要确定 JSONP 请求时否失败并不容易。虽然 HTML5 给<acript>元素新增了一个 onerror 事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用计时器检测指定时间内是否接收到了响应。但就是这样也不能尽如人意,毕竟不是每个用户上网的速度和带宽都一样。

4 基于 nginx 实现反向代理


处理原理

5 修改本地 hosts 文件

一个域名一定会被解析为一个或多个ip。这一般会包含两步:

5.1 本地域名解析

浏览器会首先在本机的hosts文件中查找域名映射的IP地址,如果查找到就返回IP ,没找到则进行域名服务器解析,一般本地解析都会失败,因为默认这个文件是空的。

  • Windows下的hosts文件地址:C:/Windows/System32/drivers/etc/hosts
  • Linux下的hosts文件所在路径: /etc/hosts
  • 修改 hosts 文件:
# My hosts 添加一下内容 
127.0.0.1 localhost
127.0.0.1 www.jianshu.com

127.0.0.1 是本地IP,可以对应多个地址,用空格分开,注意换行,#类似注释,不编译。

ping一下域名试试是否畅通:

5.2 域名服务器解析

本地解析失败,才会进行域名服务器解析,域名服务器就是网络中的一台计算机,里面记录了所有注册备案的域名和ip映射关系,一般只要域名是正确的,并且备案通过,一定能找到。

6 其它跨域技术

利用 DOM 中能够执行跨域请求的功能,在不依赖 接口 的情况下也能发送某种请求。虽然 CORS 技术已经无处不在,但开发人员之间发明的这些技术任然被广泛使用,毕竟这样不需要修改服务器端代码

6.1 图像 Ping

利用<img>标签。我们知道,一个网页可以从任何网页中加载图像,不用担心跨域不跨越。这也是在线广告跟踪浏览量的主要方式。可以动态创建图像,使用它们的 onload 和 onerror 事件处理程序来确定是否接收到了响应。
动态创建图像经常用于图像 Ping。图像 Ping 是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以是任何内容,但通常是像素图或204响应。通过图像 Ping,浏览器得不到任何具体的数据,但通过监听 load 和 error 事件,它能知道响应是什么时候接收到的。来看下面的例子。

var img = new Image();
img.onload = img.onerror = function(){
  alert("Done!")
}
img.src = "http://www.example.com/test?name=Nichplas"

这里创建了一个 Image 的实例,然后将 onload 和 onerror 事件处理程序指定为同一个函数。这里无论是什么响应,只要请求完成,就能得到通知。请求从设置 src 属性那一刻开始,而这个例子在请求中发送一个 name 参数。
图像 Ping 最常用于跟踪用户点击页面或动态广告曝光次数。图像 Ping 有两个主要的缺点,一是只能发送 GET 请求,而是无法访问服务器的响应文本。因此,图像 Ping 只能用于浏览器与服务器间的简单通信。

6.2 iframe

postMessage、window.name、document.domin、location.hash、...这些方案结合 iframe 也可以实现跨域处理。

posted @ 2018-12-12 13:06  真的想不出来  阅读(1195)  评论(0编辑  收藏  举报