什么是跨域?产生原因是什么,有哪些解决方案规避跨域问题?

Web前端 潘老师 4年前 (2020-10-04) 2157 ℃ (0) 扫码查看



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

跨域的概念很简单,即当一个请求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 请求
1、设置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 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法规避同源政策,而要使用下面介绍的PostMessage API,不过我们在此不作说明了。

另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如 .panziye.com。

Set-Cookie: key=value; domain=.panziye.com; path=/

这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。

2、跨文档通信 API:window.postMessage()

如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。

document.getElementById("subFrame").contentWindow.document

报错如下:

Uncaught DOMException: Blocked a frame from accessing a cross-origin frame

上面命令中,父窗口想获取子窗口的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);
3、使用JSONP

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); 
})
4、使用CORS

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 *
5、使用Nginx反向代理实现

nginx作为反向代理服务器,就是把http请求转发到另一个或者一些服务器上。通过把本地一个url前缀映射到要跨域访问的web服务器上,就可以实现跨域访问。对于浏览器来说,访问的就是同源服务器上的一个url。而nginx通过检测url前缀,把http请求转发到后面真实的物理服务器。并通过rewrite命令把前缀再去掉。这样真实的服务器就可以正确处理请求,并且并不知道这个请求是来自代理服务器的。
假设我们有个前后端分离项目,前端项目部署在本机localhost8888端口的服务器上,后端项目部署在本机localhost8080端口的服务器上,后端提供了一个获取用户的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,那么跨域问题就解决了

6、针对Vue项目解决本地环境跨域

基于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': '' }这部分去掉即可。这样就直接简化了后端代码的开发。

总结

以上就是什么是跨域?产生原因是什么,以及有哪些解决跨域问题的一些方案。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/java/web/1426.html
喜欢 (9)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】