likes
comments
collection
share

【计算机网络】【HTTP】跨域

作者站长头像
站长
· 阅读数 6

一、什么是跨域

:域既是 window 网络操作系统的逻辑组织单元,也是Internet的逻辑组织单元,他是安全边界

只有域的所有者才能访问管理域内部的资源,若其它的域要访问或者管理,则需要该域赋予其它域相关权限

跨域,全称跨源资源共享,英文名称 CORS(Cross-Origin Resource Sharing),跨域既是从其它域获取资源的操作。

二、为什么会跨域

跨域出现的原因是浏览器的同源策略引起的

2.1 什么是源

web内容的源由用于访问它的URL的方案(协议),主机(域名)和端口定义。只有当方案,主机和端口都匹配时,两个对象具有相同的起源

所谓同源指的是域名、协议、端口相同。若有其中一个不同,浏览器将会认为非同源,也就是跨域。

2.2 URL结构

URL 代表着是统一资源定位符(Uniform Resource Locator)。URL无非就是一个给定的独特资源在 web 上的地址

URL结构组成:

  • Schme 或者 Protocol

    【计算机网络】【HTTP】跨域

  • Domain Name 也叫做host域名

    【计算机网络】【HTTP】跨域

  • port 端口号

    【计算机网络】【HTTP】跨域

  • Parameters参数

    【计算机网络】【HTTP】跨域

  • Anchor 锚点,一般用于定位

【计算机网络】【HTTP】跨域

2.3 同源不同源举例

2.4 浏览器为什么需要同源策略

同源策略是一个很重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助组个恶意文档,减少可能被攻击的媒介

2.5 什么是同源策略

同源策略是web的一种安全约定,浏览器的同源策略只是对其的一种实现

浏览器同源策略将认为任何站点装载的内容都是不安全的。所以会对跨域的操作或请求进行限制的,从而让用户安全的上网

存储在浏览器中的数据,如 localstorage、Cookie和IndexDB不能通过脚本跨域访问

浏览的同源策略主要有两种:

  • DOM同源策略:禁止对不同源页面的DOM元素进行操作,主要是在 iframe 标签加载跨域页面出现
  • XMLHTTPRequest 同源策略:禁止使用XHR对象对不同源地址发起请求

2.5.1 DOM同源策略

如果没有DOM同源策略,也就是说不同域的iframe之间可相互访问操作

那么将会出现这种攻击操作:我们 iframe 包含整个网站的登录页,并且监听目标网站的登录按钮,当用户触发按钮的时候,我们拿到目标网站 input 的DOM元素,并且取值,保存到自己的服务器上

但是因为有 DOM同源策略的存在,禁止操作不同源页面的DOM元素,甚至我们还可以将子级的网站设置禁止在非同源网站上iframe

如何禁止在非同源网站上iframe:X-Frame-Options

X-Frame-Options是一个HTTP标头(header),用来告诉浏览器这个网页是否可以放在iframe内。用法如下:

X-Frame-Options: DENY     // 不允许iframe
X-Frame-Options: SAMEORIGIN   // 只允许同源的网站iframe
X-Frame-Options: ALLOW-FROM http://yancoo.cn/  // 只允许指定网站iframe

2.5.2 XMLHTTPRequest 同源策略

如果没有XHR同源策略,一级不允许跨域获取Cookies等的限制,那么攻击者可以发起CSRF (跨站请求伪造)攻击

场景可以如下:

  • 你登录某个银行网站,www.pingan.com ,银行网站返回你的登录状态并且保存在cookies里
  • 你没有安全退出清空cookies,又刚好不小心浏览了恶意网站 www.eyi.com
  • 一进入 www.eyi.com ,它将会向 银行网站 发起XHR请求(发送请求将会带上目标网站设置的cookies)
  • 银行拿到cookies,验证通过,返回数据。

三、跨域的解决方案

3.1 JSONP(JSON with Padding)

利用script标签不受跨域限制而形成的一种方案。

通过动态创建 <script>标签来实现跨域请求的技术。服务器端返回的数据被包装在一个函数调用中,该函数名由客户端指定。

  • 优点:简单易用

  • 缺点:只支持get请求,不支持post、put、delete等;不安全,容易受[xss][18]攻击。

举个栗子: ```js // index.html function jsonp({url, param, cb}){ return new Promise((resolve, reject)=>{ let script = document.createElement('script') window[cb] = function(data){ resolve(data); document.body.removeChild(script) } params = {...params, cb} let arrs = []; for(let key in params){ arrs.push(${key}=${params[key]}) } script.src = ${url}?${arrs.join('&')} document.body.appendChild(script) }) } jsonp({ url: 'http://localhost:3000/say', params: {wd: 'haoxl'}, cb: 'show' }).then(data=>{ console.log(data) })

//server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res){
    let {wd,cb} = req.query
    console.log(wd)
    res.end(`${cb}('hello')`)
})
app.listen(3000)
```

3.2 CORS

CORS是一种标准机制,通过在服务端设置响应头来允许或拒绝跨域请求。这是解决跨域问题的最常见方法

  • 在服务器端设置响应头中的Access-Control-Allow-Origin字段来指定允许访问的域名或使用通配符*表示允许所有域名访问。

  • 其他相关的CORS头,如Access-Control-Allow-MethodsAccess-Control-Allow-Headers,用于控制允许的HTTP方法和请求头。

    【计算机网络】【HTTP】跨域

举个栗子:

 <!--index.html-->
 <body>
     Nice to meet you
 </body> 


 <script>
 let xhr = new XMLHttpRequest;
 // 强制前端设置必须带上请示头cookie
 document.cookie = 'name=haoxl'
 xhr.withCredentials = true
 xhr.open('GET','http://localhost:4000/getData', true);
 // 设置自定义请求头
 xhr.setRequestHeader('name','haoxl')
 xhr.onreadystatechange = function(){
     if(xhr.readyState === 4){
         if(xhr.status>=200 && xhr.status < 300 || xhr.status === 304){
             console.log(xhr.response);
             //获取后台传来的已改变name值的请示头
             console.log(xhr.getResponseHeader('name'));
         }
     }
 }
 xhr.send()
 </script>


 // server1.js
 let express = require('express');
 let app = express();
 app.use(express.static(__dirname));
 app.listen(3000)



 // server2.js
 let express = require('express');
 let app = express();
 let whiteList = ['http://localhost:3000']
 app.use(function(req, res, next){
     let origin = req.headers.origin;
     if(whiteList.includes(origin)){
         //设置那个源可以访问我,参数为 * 时,允许任何人访问,但是不可以和 cookie 凭证的响应头共同使用
         res.setHeader('Access-Control-Allow-Origin', origin);
         //允许带有name的请求头的可以访问
         res.setHeader('Access-Control-Allow-Headers','name');
         // 设置哪些请求方法可访问
         res.setHeader('Access-Control-Allow-Methods', 'PUT');
         // 设置带cookie请求时允许访问
         res.setHeader('Access-Control-Allow-Credentials', true);
         // 后台改了前端传的name请示头后,再传回去时浏览器会认为不安全,所以要设置下面这个 
         res.setHeader('Access-Control-Expose-Headers','name');
         // 预检的存活时间-options请示
         res.setHeader('Access-Control-Max-Age',3)
         // 设置当预请求发来请求时,不做任何处理
         if(req.method === 'OPTIONS'){
             res.end();//OPTIONS请示不做任何处理
         }
     }
     next();
 });

 app.put('/getData', function(req, res){
     console.log(req.headers)
     res.setHeader('name','hello');
     res.end('hello world');
 }

 app.get('/getData', function(){
     res.end('Nice to meet you')
 })
 app.use(express.static(__dirname));
 app.listen(3000)

3.3 postMessage

对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号(443为https的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

window.postMessage() 方法被调用时,会在所有页面脚本执行完毕之后(e.g., 在该方法之后设置的事件、之前设置的timeout 事件,etc.)向目标窗口派发一个MessageEvent消息。

  • 语法:otherWindow.postMessage(message, targetOrigin, [transfer]);
    • otherWindow: 指目标窗口,也就是给哪个window发消息,是 window.frames 属性的成员或者由 window.open 方法创建的窗口;
    • message 属性是要发送的消息,类型为 String、Object (IE8、9 不支持);
      • data 属性为 window.postMessage 的第一个参数;
      • origin 属性表示调用window.postMessage() 方法时调用页面的当前状态;
      • source 属性记录调用 window.postMessage() 方法的窗口信息;
    • targetOrigin:属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。
    • transfer:是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

举个栗子:a.html 给b.html发消息

 // a.html
 <iframe src="http://localhost:4000/b.html" id="frame" onload="load()"></iframe>
 <script>
 function load(params){
     let frame = document.getElementById('frame');
     //获取iframe中的窗口,给iframe里嵌入的window发消息
     frame.contentWindow.postMessage('hello','http://localhost:4000')
     // 接收b.html回过来的消息
     window.onmessage = function(e){
         console.log(e.data)
     }
 }
 </script>


 // b.html
 <script>
 //监听a.html发来的消息
 window.onmessage = function(e){
     console.log(e.data)
     //给发送源回消息
     e.source.postMessage('nice to meet you',e.origin)
 }
 </script>

3.4 window.name

页面可能会因某些限制而改变他的源。脚本可以将 document.domain 的值设置为其当前域或其当前域的超级域。如果将其设置为其当前域的超级域,则较短的域将用于后续源检查。

// a.html
<iframe src="http://localhost:4000/c.html" onload="load()"></iframe>
<script>
let first = true
function load(){
    if(first){
        let iframe = document.getElementById('iframe');
        // 将a中的iframe再指向b
        iframe.src='http://localhost:3000/b.html';
        first = false;
    }else{
        //在b中则可得到c给窗口发的消息
        console.log(iframe.contentWindow.name);
    }
}
</script>

// c.html
<script>
window.name = 'nice to meet you'
</script>


//server.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000);

3.5 location.hash

window.location 只读属性,返回一个Location对象,其中包含有关文档当前位置的信息。 window.location : 所有字母必须小写!只要赋给 location 对象一个新值,文档就会使用新的 URL 加载,就好像使用修改后的 URL 调用了window.location.assign()一样。需要注意的是,安全设置,如 CORS,可能会限制实际加载新页面。

举个栗子:a、b同域,c单独一个域。a现在想访问c:a通过iframe给c传一个hash值,c收到hash值后再创建一个iframe把值通过hash传递给b,b将hash结果放到a的hash值中。

// a.html
<iframe src="http://localhost:4000/c.html#iloveyou"></iframe>
<script>
//接收b传来的hash值
window.onhashchange = function(){
    console.log(location.hash)
}
</script>



// c.html
//接收a传来的hash值
console.log(location.hash)
//创建一个iframe,把回复的消息传给b
let iframe = document.createElement('iframe');
iframe.src='http://localhost:3000/b.html#idontloveyou';
document.body.appendChild(iframe);



//b.html
<script>
//a.html引的c, c又引的b,所以b.parent.parent即是a
window.parent.parent.location.hash = location.hash
</script>

3.6 window.domain

window.domain:获取/设置当前文档的原始域部分。 案例:解决一级域与二级域之间通信。 模拟时需要创建两个不同域的域名用来测试,打开C:\Windows\System32\drivers\etc 该路径下找到 hosts 文件,在最下面创建一个一级域名和一个二级域名。改为:

127.0.0.1   www.haoxl.com
127.0.0.1   test.haoxl.com

预设a.html = www.haoxl.com, b.html = test.haoxl.com

// a.html
<iframe src="http://test.haoxl.com" onload="load()"></iframe>
<script>
function load(){
    //告诉页面它的主域名,要与b.html的主域名相同,这样才可在a中访问b的值
    document.domain = 'haoxl.com'
    function load(){
        // 在a页面引入b页面后,直接通过下面方式获取b中的值
        console.log(frame.contentWindow.a);
    }
}
</script>


// b.html
document.domain = 'haoxl.com'
var a = 'hello world'

3.7 websocket

WebSocket对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。它是基于TCP的全双工通信,即服务端和客户端可以双向进行通讯,并且允许跨域通讯。基本协议有ws://(非加密)和wss://(加密)

//socket.html
let socket = new WebSocket('ws://localhost:3000');
// 给服务器发消息
socket.onopen = function() {
    socket.send('hello server')
}
// 接收服务器回复的消息
socket.onmessage = function(e) {
    console.log(e.data)
}

// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//npm i ws
// 设置服务器域为3000端口
let wss = new WebSocket.Server({port:3000});
//连接
wss.on('connection', function(ws){
    // 接收客户端传来的消息
    ws.on('message', function(data){
        console.log(data);
        // 服务端回复消息
        ws.send('hello client')
    })
})

3.8 Nginx

Nginx (engine x) 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器。

举个栗子:在nginx根目录下创建json/a.json,里面随便放些内容

// client.html
let xhr = new XMLHttpRequest;
xhr.open('get', 'http://localhost/a.json', true);
xhr.onreadystatechange = function() {
    if(xhr.readyState === 4){
        if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304 ){
            console.log(xhr.response);
        }
    }
}


// server.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);



// nginx.conf
location / {// 代表输入/时默认去打开root目录下的html文件夹
    root html;
    index index.html index.htm;
}
location ~.*\.json{//代表输入任意.json后去打开json文件夹
    root json;
    add_header "Access-Control-Allow-Origin" "*";
}

3.9 http-proxy-middleware

NodeJS 中间件 http-proxy-middleware 实现跨域代理,原理大致与 nginx 相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置 cookieDomainRewrite 参数修改响应头中 cookie 中的域名,实现当前域的 cookie 写入,方便接口登录认证。

  • vue框架:利用 node + webpack + webpack-dev-server 代理接口跨域。在开发环境下,由于 Vue 渲染服务和接口代理服务都是 webpack-dev-server,所以页面与代理接口之间不再跨域,无须设置 Headers 跨域信息了。

    module.exports = {
        entry: {},
        module: {},
        ...
        devServer: {
            historyApiFallback: true,
            proxy: [{
                context: '/login',
                target: 'http://www.proxy2.com:8080',  // 代理跨域目标接口
                changeOrigin: true,
                secure: false,  // 当代理某些 https 服务报错时用
                cookieDomainRewrite: 'www.domain1.com'  // 可以为 false,表示不修改
            }],
            noInfo: true
        }
    }
    
  • 非vue框架的跨域(2 次跨域)

    <!-- index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>nginx跨域</title>
    </head>
    <body>
        <script>
            var xhr = new XMLHttpRequest();
    
            // 前端开关:浏览器是否读写 cookie
            xhr.withCredentials = true;
    
            // 访问 http-proxy-middleware 代理服务器
            xhr.open('get', 'http://www.proxy1.com:3000/login?user=admin', true);
            xhr.send();
        </script>
    </body>
    </html>
    
    
    
    // 中间代理服务器
    var express = require("express");
    var proxy = require("http-proxy-middleware");
    var app = express();
    
    app.use(
        "/",
        proxy({
            // 代理跨域目标接口
            target: "http://www.proxy2.com:8080",
            changeOrigin: true,
    
            // 修改响应头信息,实现跨域并允许带 cookie
            onProxyRes: function(proxyRes, req, res) {
                res.header("Access-Control-Allow-Origin", "http://www.proxy1.com");
                res.header("Access-Control-Allow-Credentials", "true");
            },
    
            // 修改响应信息中的 cookie 域名
            cookieDomainRewrite: "www.proxy1.com" // 可以为 false,表示不修改
        })
    );
    
    app.listen(3000);
    
    
    
    // 服务器
    var http = require("http");
    var server = http.createServer();
    var qs = require("querystring");
    
    server.on("request", function(req, res) {
        var params = qs.parse(req.url.substring(2));
    
        // 向前台写 cookie
        res.writeHead(200, {
            "Set-Cookie": "l=a123456;Path=/;Domain=www.proxy2.com;HttpOnly" // HttpOnly:脚本无法读取
        });
    
        res.write(JSON.stringify(params));
        res.end();
    });
    
    server.listen("8080");
    
    

资料来源: