跨域问题及常见的解决办法和实现原理

微信扫一扫,分享到朋友圈

跨域问题及常见的解决办法和实现原理

一:什么是跨域

为了网站的安全考虑,浏览器对我们的请求做了同源策略的限制,即不同源之间的请求是不允许的。何为同源呢,就是我们url的协议,域名,端口一致,否则就被视为不同源。以我们最常见的 url格式 https://www.baidu.com 为例,https 是协议,www.baidu.com 是域名。需要说明的是,我们很少在 url 中看见端口,这是因为我们提前做了约定,http 协议的默认端口为 80,而 https 的默认端口为443,所以当你访问 https://www.baidu.com 的时候,实则访问的是 https://www.baidu.com:443。另外,对于刚接触前端不久的朋友,可能认为 www.baidu.com 是域名,其实它的域名应该是 baidu.com,也称顶级域名,前面的 www 属于二级域名,只是我们习惯性的把二级域名定义为 www 而已,就 baidu.com 来说,其他二级域名还有 echarts.baidu.com,wenku.baidu.com,map.baidu.com 等等。当然,如果你有需要,或者说你喜欢,你也可以设置三级域名,四级域名。值得注意的是,顶级域名相同,子域名不同,也算跨越。一张图概括就是:

二、跨域带来的问题

同源策略的限制,本是为了我们网站的安全考虑,但有时候也带来了一些不便。对于开发者来说,可能遇到最多的问题就是ajax请求跨域了。除此之外,还有cookie,sessionStorage、localstorage、IndexDB 等缓存数据,以及 dom 和 js 对象无法操作的限制。

三、跨域解决方案

关于上面说到的跨域带来的诸多不便,我们只讲request的解决方案,其他几个问题用得不多,不做讨论,有兴趣的可以看一下 这篇文章

特别需要说明的一点:对于我们的ajax请求被拦截来说,拦截的不是请求,而是响应,我们的请求其实是已经发出去了的,只是响应回来的内容,当浏览器识别是非同源时,给我们拦截了。

1、后端代码设置允许跨域,大致代码实现如下

// java
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com");
// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true");
// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
 
// node
server.on('request', function(req, res) {
    var postData = '';
    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });
    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);
        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            /*
             * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
             * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
             */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
        });
        res.write(JSON.stringify(postData));
        res.end();
    });
});
 
// php
<?php
header("Access-Control-Allow-Origin:*");

2、启用代理服务,代理服务又分前端和后端两种情况。下面举个例子说一下代理服务的工作原理。

假如有三个人,你,小花,小花的弟弟,你跟小花的弟弟关系很好(其实好不好无所谓了,反正有钱就好办事)。小花呢,有一本日记,但她只给家里人看(这就相当于同源),你是看不到的(因为你不是她的家人,不同源)。你想看小花的日记,直接找小花,会被拒绝(小花是有原则的人,给钱人家也不愿意,同源策略的限制)。所以呢,你就找到小花的弟弟(代理),说你想看小花的日记,他弟弟再去找小花要日记本,拿到日记本后,再把日记本给你,你就看到了。

前端的代理叫正向代理,正向代理帮助客户端访问自己访问不到的资源,然后将结果返回给客户端。上面举的例子就属于正向代理。

后端的代理叫反向代理,反向代理帮其它的服务器拿到请求,然后选择一个合适的服务器,将请求转交给它。

反向代理最常用的就是Nginx代理了,比如说现在客户端的域名为client.com,服务器的域名为server.com,客户端向服务器发送 Ajax 请求,便是跨域了,那这个时候 Nginx就登场了,我们通过下面这个配置便能解决。

server {
  listen  80;
  server_name  client.com;
  location /api {
    proxy_pass server.com;
  }
}

Nginx 相当于起了一个跳板机,这个跳板机的域名也是 client.com ,让客户端首先访问  client.com/api ,这当然没有跨域,然后 Nginx 服务器作为反向代理,将请求转发给 server.com ,当响应返回时又将响应给到客户端,这就完成整个跨域请求的过程。

3、jsonp

虽然 XMLHttpRequest 对象遵循同源政策,但是 script 标签不一样,它可以通过 src 填上目标地址从而发出 GET 请求,实现跨域请求并拿到响应,这也就是 JSONP 得以实现的关键点。

它的实现原理,就是在script标签的src属性后面添加一些查询参数(我们需要传递过去的参数,0个或N个),和一个callback方法(这个方法必须是全局的),当我们通过 src 请求 script 的时候,服务器便可以拿到我们传递的参数,根据参数找到我们想要的结果数据,最后把数据作为参数传入 callback 方法中,返回 ‘callback(data) ‘ 的 js 代码,前端获取到响应后,就会执行一次callback(data)(方法必须是全局的原因,不是全局的方法没法执行),如此整个过程便根据请求参数返回了我们想要的结果。代码实现如下:

首先,封装一个jsonp方法

const jsonp = ({ url, params, callbackName }) => {
  const generateURL = () => {
    let dataStr = '';
    for(let key in params) {
      dataStr += `${key}=${params[key]}&`;
    }
    dataStr += `callback=${callbackName}`;
    return `${url}?${dataStr}`;
  };
  return new Promise((resolve, reject) => {
    // 初始化回调函数名称
    callbackName = callbackName || Math.random().toString.replace(',', '');
    // 创建 script 元素并加入到当前文档中
    let scriptEle = document.createElement('script');
    scriptEle.src = generateURL();
    document.body.appendChild(scriptEle);
    // 绑定到 window 上,为了后面调用
    window[callbackName] = (data) => {
      resolve(data);
      // script 执行完了,成为无用元素,需要清除
      document.body.removeChild(scriptEle);
    }
  });
}

然后,在服务端对请求做相应的处理,以 express 为例:

let express = require('express')
let app = express()
app.get('/', function(req, res) {
  let { a, b, callback } = req.query
  console.log(a); // 1
  console.log(b); // 2
  // 注意哦,返回给script标签,浏览器直接把这部分字符串执行
  res.end(`${callback}('数据包')`);
})
app.listen(3000)

最后,前端这样简单地调用一下就好了:

jsonp({
  url: 'http://localhost:3000',
  params: {
    a: 1,
    b: 2
  }
}).then(data => {
  // 拿到数据进行处理
  console.log(data); // 数据包
})

注:由于jsonp是通过url发请求传参,所以只支持get请求。

印度发生一起Realme 5手机电池起火事件 用户轻伤无大碍

上一篇

理解 Dubbo SPI 扩展机制

下一篇

你也可能喜欢

跨域问题及常见的解决办法和实现原理

长按储存图像,分享给朋友