章
目
录
在进行前端网页开发尤其是在调用接口时经常会遇到跨域问题,导致接口调用失败(浏览器端类似报错如下图),接下来潘老师带大家来了解下到底什么是跨域,为什么会产生跨域问题,以及我们有哪些方法可以规避跨域问题。
跨域的概念很简单,即当一个请求URL的协议、域名、端口三者之间任意一个与当前页面URL不同则视为跨域,而跨域问题产生的原因主要是由浏览器的“同源策略”限制导致的。
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。所谓同源即:如果两个 URL 的 protocol(协议)、port (端口,如果有指定的话)和 host (主机名) 都相同的话,则这两个 URL 是同源。
当前页面url | 被请求页面url | 是否跨域 | 原因 |
---|---|---|---|
http://www.panziye.com/ |
http://www.panziye.com/index.html |
否 | 同源(协议、域名、端口相同) |
http://www.panziye.com/ |
https://www.panziye.com/index.html |
是 | 协议不同(http/https) |
http://www.panziye.com/ |
http://www.baidu.com/ |
是 | 主域名不同(panziye/baidu) |
http://www.panziye.com/ |
https://blog.panziye.com/ |
是 | 子域名不同(www/blog) |
http://www.panziye.com/ |
http://www.panziye.com:8080/ |
是 | 端口不同(默认80/8080) |
同源策略的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。假设你成功登录了某个银行网站,自己的账户余额在你登录后都可以随意操作,大家都知道很多网站系统在你登录成功后都会给你的浏览器发送Cookie,Cookie中一般会记录你的部分信息甚至是登录状态,而当你在未退出银行网站的情况下,接着又去浏览了其他网站,如果其他网站可以读取银行网站的 Cookie,会发生什么?很显然,你的Cookie中信息会全部泄露,甚至其他网站还会使用你的Cookie来登录你的账号冒充你来操作你的账户,由于浏览器同时还规定,提交表单不受同源策略的限制,从而你的钱就会不翼而飞。这就是所谓的CSRF
攻击,中文名称为:跨站请求伪造攻击,即攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。
随着互联网的发展,”同源策略”也越来越严格。目前,如果非同源,共有三种行为受到限制:
- 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
- 无法接触非同源网页的 DOM
- 无法向非同源地址发送 AJAX 请求
document.domain
解决无法读取非同源网页的 Cookie问题由于浏览器是通过document.domain
属性来检查两个页面是否同源,因此我们只要通过设置相同的document.domain
,两个页面就可以共享Cookie,不过此方案仅限主域名相同,子域名不同的跨域应用场景,比如A网页是http://blog.panziye.com/a.html
,B网页是http://www.panziye.com/b.html
,那么A和B两个网页都设置相同的document.domain
如下
// 两个页面都设置 document.domain='panziye.com';
这样两个网页就可以共享Cookie了,在A网页中通过脚本设置一个Cookie:
document.cookie = "test=hello";
在B网页中则可以通过如下代码获取该Cookie:
var allCookies = document.cookie;
另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如 .panziye.com。
Set-Cookie: key=value; domain=.panziye.com; path=/
这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。
如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。
document.getElementById("subFrame").contentWindow.document
报错如下:
上面命令中,父窗口想获取子窗口的DOM,因为跨源导致报错。反之亦然,子窗口获取主窗口的DOM也会报错。
window.parent.document.body
如果两个窗口一级域名相同,只是二级域名不同,那么设置之前介绍的document.domain属性,就可以规避同源政策,拿到DOM。对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题。
- 片段识别符(fragment identifier)
- window.name
- 跨文档通信API(Cross-document messaging)
前两种我们不再去介绍,我们只看下第三种该如何去实现:
跨文档通信 API(Cross-document messaging)是HTML5为了解决该问题新引入的一个API,这个API为window对象新增了一个window.postMessage
方法,允许跨窗口通信,不论这两个窗口是否同源。它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
举例来说,父窗口http://panziye.com
向子窗口http://pzy.com
发消息,调用postMessage
方法就可以了:
// 父窗口打开一个子窗口 var openWindow = window.open('http://pzy.com', 'title'); // 父窗口向子窗口发消息(第一个参数代表发送的内容,第二个参数代表接收消息窗口的url) openWindow.postMessage('Hello PZY!', 'http://pzy.com');
postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即”协议 + 域名 + 端口”。也可以设为*,表示不限制域名,向所有窗口发送。子窗口向父窗口发送消息的写法类似。
window.opener.postMessage('Hello PanZiYe!', 'http://panziye.com');
父窗口和子窗口都可以通过message事件,监听对方的消息:
// 监听 message 消息 window.addEventListener('message', function (e) { console.log(e.source); // e.source 发送消息的窗口 console.log(e.origin); // e.origin 消息发向的网址 console.log(e.data); // e.data 发送的消息 },false);
JSONP 是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,兼容性好(兼容低版本IE),缺点是只支持get请求,不支持post请求。
核心思想:网页通过添加一个<script>
元素,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。
a)原生实现:
<script src="http://panziye.com/test?callback=handleCallback"></script> // 向服务器panziye.com发出请求,该请求的查询字符串有一个callback参数,用来指定回调函数的名字 // 处理服务器返回回调函数的数据 <script type="text/javascript"> function handleCallback(result){ // 处理获得的数据 console.log(result.data) } </script>
b) jQuery Ajax:
$.ajax({ url: 'http://www.panziye.com/test', type: 'get', dataType: 'jsonp', // 请求方式为jsonp jsonpCallback: "handleCallback", // 自定义回调函数名 data: {} });
c)Vue.js:
this.$http.jsonp('http://www.panziye.com/test', { params: {}, jsonp: 'handleCallback' }).then((result) => { console.log(result); })
CORS 是跨域资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。使用要求:
- 普通跨域请求:只需服务器端设置Access-Control-Allow-Origin
- 带cookie跨域请求:前后端都需要进行设置
1)前端设置,包含以下几种使用情形:
a)原生ajax:
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容 // 前端设置是否带cookie xhr.withCredentials = true; xhr.open('post', 'http://www.panziye.com/login', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('user=admin'); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { alert(xhr.responseText); } };
b) jQuery Ajax:
$.ajax({ url: 'http://www.panziye.com/login', type: 'get', data: {}, xhrFields: { withCredentials: true // 前端设置是否带cookie }, crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie });
c)vue-resource
Vue.http.options.credentials = true
d)axios
axios.defaults.withCredentials = true
2)服务端设置,服务器端对于CORS的支持,主要是通过设置Access-Control-Allow-Origin
来进行的。如果浏览器检测到相应的设置,就可以允许Ajax进行跨域的访问。根据不同的语言,设置如下:
a)Java后台:
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/' response.setHeader("Access-Control-Allow-Origin", "http://www.panziye.com"); // 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示 response.setHeader("Access-Control-Allow-Credentials", "true"); // 提示OPTIONS预检时,后端需要设置的两个常用自定义头 response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
SpringBoot
项目可以直接在Controller
类上加上如下注解实现跨域,其中origins 是一个数组,存的是允许跨域的请求主机地址,另外在controller方法中获取参数时,需要使用@RequestBody注解,否则可能无法获取请求参数
@CrossOrigin(origins = {"http://localhost:8081"},allowCredentials="true")
b)Node.js后台:
var http = require('http'); var server = http.createServer(); var qs = require('querystring'); 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(); }); }); server.listen('8080'); console.log('Server is running at port 8080...');
c) PHP后台:
header("Access-Control-Allow-Origin:*");
d)Apache需要使用mod_headers模块来激活HTTP头的设置,它默认是激活的。你只需要在Apache配置文件的
Header set Access-Control-Allow-Origin *
nginx作为反向代理服务器,就是把http请求转发到另一个或者一些服务器上。通过把本地一个url前缀映射到要跨域访问的web服务器上,就可以实现跨域访问。对于浏览器来说,访问的就是同源服务器上的一个url。而nginx通过检测url前缀,把http请求转发到后面真实的物理服务器。并通过rewrite命令把前缀再去掉。这样真实的服务器就可以正确处理请求,并且并不知道这个请求是来自代理服务器的。
假设我们有个前后端分离项目,前端项目部署在本机localhost
的8888
端口的服务器上,后端项目部署在本机localhost
的8080
端口的服务器上,后端提供了一个获取用户的API,请求如下:localhost:8080/api/user/listUser
,而我们前端在浏览器上如果直接请求此api会因为端口不同导致不同源而存在跨域问题,那么我们可以使用Nginx反向代理配置来实现,Nginx的conf
配置文件配置如下:
server { listen 8888; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root html; index index.html index.htm; } location /api/ { proxy_pass http://127.0.0.1:8080; } }
这样,我们访问localhost:8888/api/user/listUser
这个不跨域的端口就会被服务器反向代理到localhost:8080/api/user/listUser
,那么跨域问题就解决了
基于vue-cli开发的vue项目,如果不需要后端设置cors允许跨域,那么则需要前端自己配置本地代理了。在你的项目上设置代理,所有接口处前面加上/api
用来识别做代理。可以在src
目录下新增名为vue.config.js
的文件,新增配置类似如下:
module.exports = { // 部署生产环境和开发环境下的URL:可对当前环境进行区分 publicPath: process.env.NODE_ENV === 'production' ? '/public/' : './', // 输出文件目录:在npm run build时,生成文件的目录名称 outputDir: 'dist', // 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录 assetsDir: "assets", // 是否在构建生产包时生成 sourceMap 文件,false将提高构建速度 productionSourceMap: false, // 默认情况下,生成的静态资源在它们的文件名中包含了 hash 以便更好的控制缓存,你可以通过将这个选项设为 false 来关闭文件名哈希。(false的时候就是让原来的文件名不改变) filenameHashing: false, // 代码保存时进行eslint检测 lintOnSave: false, // webpack-dev-server 相关配置 devServer: { // 自动打开浏览器 open: true, host: '127.0.0.1', // 端口-本vue项目启动后占用的端口 port: 8081, // https https: false, // 热更新 hotOnly: false, // 使用代理 proxy: { '/api': { // 目标代理服务器地址-也就是你的后端项目地址 target: 'http://127.0.0.1:8080/', // 开启代理,本地创建一个虚拟服务器 允许跨域 changeOrigin: true, pathRewrite: { '^/api': '' }, }, }, }, }
核心解决跨域问题的代码配置就是proxy
那部分,这样前端访问localhost:8888/api/user/listUser
这个不跨域的端口就会被服务器反向代理到localhost:8080/user/listUser
地址,并且你会发现/api
被去掉了,如果不想被去掉(可能你在后端配置了context-path为/api),那就把pathRewrite: { '^/api': '' }
这部分去掉即可。这样就直接简化了后端代码的开发。
总结
以上就是什么是跨域?产生原因是什么,以及有哪些解决跨域问题的一些方案。