章
目
录
在进行前端网页开发尤其是在调用接口时经常会遇到跨域问题,导致接口调用失败(浏览器端类似报错如下图),接下来潘老师带大家来了解下到底什么是跨域,为什么会产生跨域问题,以及我们有哪些方法可以规避跨域问题。

跨域的概念很简单,即当一个请求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': '' }这部分去掉即可。这样就直接简化了后端代码的开发。
总结
以上就是什么是跨域?产生原因是什么,以及有哪些解决跨域问题的一些方案。





