跨域方案大全

平时在开发中总是会遇到各种跨域问题,一直没有很好地了解其中的原理,以及其各种实现方案。今天在这好好总结一下。

本文完整的源代码请猛戳github博客,建议大家动手敲敲代码。

1、什么是跨域?为什么会有跨域?

一般来说,当一个请求url的协议、域名、端口三者之间任意一个与当前页面地址不同即为跨域。
之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

为什么会有同源策略呢?
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。

设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?

很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。

由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。

同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了
    下面为允许跨域资源嵌入的示例,即一些不受同源策略影响的标签示例:
  • 标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。
  • 标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的Content-Type消息头。不同浏览器有不同的限制: IE, Firefox, Chrome, SafariOpera
  • 嵌入图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG
  • 嵌入多媒体资源。
  • , 的插件。
  • @font-face引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)
  • 
    
    
    
        
        
    
    
    

    BBBBBBBBB

    2.3、websocket

    WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
    原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

    
    
    
    
        
        
    
    
    
    user input:

    node服务端文件

    var http = require('http');
    var socket = require('socket.io');
    
    // 启http服务
    var server = http.createServer(function(req, res) {
      res.writeHead(200, {
        'Content-type': 'text/html'
      });
      res.end();
    });
    
    server.listen('8080');
    console.log('Server is running at port 8080...');
    
    // 监听socket连接
    socket.listen(server).on('connection', function(client) {
      // 接收信息
      client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
      });
    
      // 断开处理
      client.on('disconnect', function() {
        console.log('Client socket has closed.');
      });
    });
    
    

    2.4、Node中间件代理

    实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。

    主要访问路径

    • 客户端发出请求
    • 代理服务接受客户端请求 。
    • 大理服务将请求 转发给应用服务器。
    • 应用服务器接收到请求代理服务器求情 ,响应数据。
    • 代理服务器将响应数据转发给客户端。

    实现代码:
    前端代码示例:

    
    
    
    
        
        
    
    
    

    1111

    代理服务器

    var express = require('express');
    var proxy = require('http-proxy-middleware');
    var app = express();
    
    var options = {
      dotfiles: 'ignore',
      etag: false,
      extensions: ['htm', 'html'],
      index: false,
      maxAge: '1d',
      redirect: false,
      setHeaders: function (res, path, stat) {
        res.set('x-timestamp', Date.now())
      }
    }
    app.use(express.static('public', options))
    
    app.use('/', proxy({
      // 代理跨域目标接口
      target: 'http://127.0.0.1:4000',
      changeOrigin: true,
    
      // 修改响应头信息,实现跨域并允许带cookie
      onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://127.0.0.1');
        res.header('Access-Control-Allow-Credentials', 'true');
      },
    
      // 修改响应信息中的cookie域名
      cookieDomainRewrite: '127.0.0.1'  // 可以为false,表示不修改
    }));
    
    app.listen(3000);
    console.log('Proxy server is listen at port 3000...');
    

    应用服务器

    // 服务器
    const http = require("http");
    const server = http.createServer();
    const qs = require("querystring");
    
    server.on("request", function(req, res) {
      var params = qs.parse(req.url.split('?')[1]);
      console.log(req.url, params);
      // 向前台写 cookie
      res.writeHead(200, {
        "Set-Cookie": "l=a123456;Path=/;Domain=127.0.0.1;HttpOnly" // HttpOnly:脚本无法读取
      });
    
      res.write(JSON.stringify({ data: 'I LOVE YOU', ...params }));
      res.end();
    });
    
    server.listen("4000");
    console.log('listen 4000...')
    

    最终效果


    2.5、nginx反向代理

    跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

    实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

    nginx具体配置:

    #proxy服务器
    server {
        listen       81;
        server_name  www.domain1.com;
    
        location / {
            proxy_pass   http://www.domain2.com:8080;  #反向代理
            proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
            index  index.html index.htm;
    
            # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
            add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
            add_header Access-Control-Allow-Credentials true;
        }
    }
    

    Nodejs后台示例:

    var http = require('http');
    var server = http.createServer();
    var qs = require('querystring');
    
    server.on('request', function(req, res) {
        var params = qs.parse(req.url.split('?')[1]);
    
        // 向前台写cookie
        res.writeHead(200, {
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
        });
    
        res.write(JSON.stringify(params));
        res.end();
    });
    
    server.listen('8080');
    console.log('Server is running at port 8080...');
    

    前端代码示例:

    var xhr = new XMLHttpRequest();
    
    // 前端开关:浏览器是否读写cookie
    xhr.withCredentials = true;
    
    // 访问nginx中的代理服务器
    xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
    xhr.send();
    

    2.6、CORS

    普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。
    虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求复杂请求

    简单请求
    只要同时满足以下两大条件,就属于简单请求

    • 1:使用下列方法之一:GET、HEAD、POST
    • 2:Content-Type 的值仅限于下列三者之一:text/plainmultipart/form-dataapplication/x-www-form-urlencoded

    复杂请求
    凡是不同时满足上面两个条件,就属于复杂请求。

    复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

    我们用PUT向后台请求时,属于复杂请求,后台需做如下配置:

    // 允许哪个方法访问我
    res.setHeader('Access-Control-Allow-Methods', 'PUT')
    // 预检的存活时间
    res.setHeader('Access-Control-Max-Age', 6)
    // OPTIONS请求不做任何处理
    if (req.method === 'OPTIONS') {
      res.end() 
    }
    // 定义后台返回的内容
    app.put('/getData', function(req, res) {
      console.log(req.headers)
      res.end('我不爱你')
    })
    
    

    接下来我们看下一个完整复杂请求的例子,并且介绍下CORS请求相关的字段

    // index.html
    let xhr = new XMLHttpRequest()
    document.cookie = 'name=xiamen' // cookie不能跨域
    xhr.withCredentials = true // 前端设置是否带cookie
    xhr.open('PUT', 'http://localhost:4000/getData', true)
    xhr.setRequestHeader('name', 'xiamen')
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          console.log(xhr.response)
          //得到响应头,后台需设置Access-Control-Expose-Headers
          console.log(xhr.getResponseHeader('name'))
        }
      }
    }
    xhr.send()
    
    //server1.js
    let express = require('express');
    let app = express();
    app.use(express.static(__dirname));
    app.listen(3000);
    
    
    //server2.js
    let express = require('express')
    let app = express()
    let whitList = ['http://localhost:3000'] //设置白名单
    app.use(function(req, res, next) {
      let origin = req.headers.origin
      if (whitList.includes(origin)) {
        // 设置哪个源可以访问我
        res.setHeader('Access-Control-Allow-Origin', origin)
        // 允许携带哪个头访问我
        res.setHeader('Access-Control-Allow-Headers', 'name')
        // 允许哪个方法访问我
        res.setHeader('Access-Control-Allow-Methods', 'PUT')
        // 允许携带cookie
        res.setHeader('Access-Control-Allow-Credentials', true)
        // 预检的存活时间
        res.setHeader('Access-Control-Max-Age', 6)
        // 允许返回的头
        res.setHeader('Access-Control-Expose-Headers', 'name')
        if (req.method === 'OPTIONS') {
          res.end() // OPTIONS请求不做任何处理
        }
      }
      next()
    })
    app.put('/getData', function(req, res) {
      console.log(req.headers)
      res.setHeader('name', 'jw') //返回一个响应头,后台需设置
      res.end('我不爱你')
    })
    app.get('/getData', function(req, res) {
      console.log(req.headers)
      res.end('我不爱你')
    })
    app.use(express.static(__dirname))
    app.listen(4000)
    
    

    2.7、location name +iframe

    原理:window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在。
    下面a.htmlb.html是同域的,都是http://localhost:3000;而c.htmlhttp://localhost:4000

    // a.html(http://localhost:3000/b.html)
      
      
    

    b.html为中间代理页,与a.html同域,内容为空。
    c页面

     // c.html(http://localhost:4000/c.html)
      
    

    总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

    2.8、document. hash + iframe

    实现原理: a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

    具体实现步骤:一开始a.html给c.html传一个hash值,然后c.html收到hash值后,再把hash值传递给b.html,最后b.html将结果放到a.html的hash值中。
    同样的,a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000

     // a.html
      
      
    
     // b.html
      
    
     // c.html
     console.log(location.hash);
      let iframe = document.createElement('iframe');
      iframe.src = 'http://localhost:3000/b.html#idontloveyou';
      document.body.appendChild(iframe);
    

    2.9、 document.domain + iframe

    实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

    该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。 只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。

    我们看个例子:页面a.zf1.cn:3000/a.html获取页面b.zf1.cn:3000/b.html中a的值

    // a.html
    
     helloa
      
      
    
    
    // b.html
    
       hellob
       
    
    

    3、总结

    1. 日常工作中,用得比较多的跨域方案是cors和nginx反向代理
    2. CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
    3. 不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。
    4. SONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

    后续更多文章将在我的github第一时间发布,欢迎关注。

    参考
    浏览器同源政策及其规避方法
    跨域资源共享 CORS 详解
    前端常见跨域解决方案(全)

    你可能感兴趣的:(跨域方案大全)