一:什么是跨域
为了网站的安全考虑,浏览器对我们的请求做了同源策略的限制,即不同源之间的请求是不允许的。何为同源呢,就是我们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,也称二级域名,.com是顶级域名,前面的 www 是三级域名。值得注意的是,域名相同(一二级),子域名不同,也算跨越。一张图概括就是:
二、跨域带来的问题
同源策略的限制,本是为了我们网站的安全考虑,但有时候也带来了一些不便。对于开发者来说,可能遇到最多的问题就是ajax请求跨域了。除此之外,还有cookie,sessionStorage、localstorage、IndexDB 等缓存数据,以及 dom 和 js 对象无法操作的限制。
三、跨域解决方案
关于上面说到的跨域带来的诸多不便,我们只讲request的解决方案,其他几个问题用得不多,不做讨论,有兴趣的可以看一下这篇文章。
特别需要说明的一点:对于我们的ajax请求被拦截来说,拦截的不是请求,而是响应,我们的请求其实是已经发出去了的,只是响应回来的内容,当浏览器识别是非同源时,给我们拦截了。
1、后端代码设置允许跨域,大致代码实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// 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就登场了,我们通过下面这个配置便能解决。
1 2 3 4 5 6 7 |
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方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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 为例:
1 2 3 4 5 6 7 8 9 10 |
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) |
最后,前端这样简单地调用一下就好了:
1 2 3 4 5 6 7 8 9 10 |
jsonp({ url: 'http://localhost:3000', params: { a: 1, b: 2 } }).then(data => { // 拿到数据进行处理 console.log(data); // 数据包 }) |
注:由于jsonp是通过url发请求传参,所以只支持get请求。
发表评论