浏览器专题系列 - 跨域与跨站(二)

简介: 浏览器专题系列 - 跨域与跨站(二)

解决跨域的方案


Tips: 对于前端页面的运行可以 使用 http-server


jsonp

原理


利用 <script>标签没有跨域限制的漏洞


通过 <script>标签的src属性指向一个需要访问的地址并提供一个回调函数来接收回调数据


script获取到的内容会被当做js脚本进行执行


所以需要服务端在回调上做一个字符串拼接操作 callbackFunName(内容)

可以通过url传递需要的参数


如需要发送一个get请求http://sugarat.top/path1/path2?param1=1


  1. 客户端注册一个全局方法function callbackFunName(res){}
  2. 服务端收到请求后获取到url上的参数
  3. 服务端返回字符串callbackFunName({"name":"sugar","age":18})
  4. 客户端当做js脚本直接解析执行
  5. 就调用了方法callbackFunName并把里面的{"name":"sugar","age":18} 当做一个对象进行了传递


只支持get请求


简单使用示例


服务端代码


// 以Node.js为例
const http = require('http')
const app = http.createServer((req, res) => {
    const jsonData = {
        name: 'sugar',
        age: 18
    }
    res.end(`diyCallBackFun(${JSON.stringify(jsonData)})`)
})
app.listen(3000)


客户端代码


<script>
    // jsonp的回调函数
    function diyCallBackFun(data) {
      console.log(data)
  }
</script>
<script>
let $srcipt = document.createElement('script')
$srcipt.src = 'http://localhost:3000/path1/path2?param1=1&param2=2'
document.body.appendChild($srcipt)
</script>
<!-- 最终构造出的标签 -->
<!-- <script src="localhost:3000/path1/path2?param1=1&param2=2"></script> -->


页面中插入上述代码并运行可以在控制台看机输出


网络异常,图片无法展示
|


通用方法封装


/**
* JSONP方法
* @param {string} url 请求路径
* @param {string} callbackName 全局函数名称(后端拼接的方法名称) 
* @param {function} success 响应的回调函数
*/
function jsonp(url, callbackName, success) {
   const $script = document.createElement('script')
   $script.src = url + `&callback=${callbackName}`
   $script.async = true
   $script.type = 'text/javascript'
   window[callbackName] = function (data) {
       success && success(data)
   }
   document.body.appendChild($script)
}


CORS


跨域资源共享(Cross-origin resource sharing)

允许浏览器向跨域服务器发送ajax请求


实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信

服务端在响应头设置 Access-Control-Allow-Origin 就可以开启 CORS


原理


如果发起的跨域AJAX请求是简单请求,浏览器就会自动在头信息之中,添加一个Origin字段, 用来表示 请求来自哪个源


如:origin: http://localhost:8080


网络异常,图片无法展示
|


如果Origin的内容不包含在请求的响应头Access-Control-Allow-Origin中,就会抛出以下错误


网络异常,图片无法展示
|


与CORS有关的以Access-Control-开头的响应头:


  • Access-Control-Allow-Origin:该字段是CORS中必须有的字段,它的值是请求时Origin字段的值以,分割多个域名,或者是*,表示对所有请求都放行
  • Access-Control-Expose-Headers:列出了哪些首部可以作为响应的一部分暴露给外部(XMLHttpRequest)
  • 默认情况下,只有七种 simple response headers (简单响应首部)可以暴露给外部:
  • Cache-Control:控制缓存
  • Content-Language:资源的语言组
  • Content-Length:资源长度
  • Content-Type:支持的媒体类型
  • Expires:资源过期时间
  • Last-Modified:资源最后修改时间
  • Pragma:报文指令


  • Access-Control-Allow-Credentials:值类型是布尔类型,表示跨域请求是否允许携带cookie
  • CORS请求默认不携带cookie
  • 还需要设置xhr(XMLHttpRequest)对象的withCredentials属性为true


简单示例


以Node.js为例子


const http = require('http')
let server = http.createServer(async (req, res) => {
    //  -------跨域支持-----------
    // 放行指定域名
    res.setHeader('Access-Control-Allow-Origin', '*')
    //跨域允许的header类型
    res.setHeader("Access-Control-Allow-Headers", "*")
    // 允许跨域携带cookie
    res.setHeader("Access-Control-Allow-Credentials", "true")
    // 允许的方法
    res.setHeader('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS')
    let { method, url } = req
    // 对预检请求放行
    if (method === 'OPTIONS') {
        return res.end()
    }
    console.log(method, url)
    res.end('success')
})
// 启动
server.listen(3000, err => {
    console.log(`listen 3000 success`);
})


反向代理


因为跨域是针对浏览器做出的限制


对后端服务没有影响


可以使用 Nginx,Node Server,Apache等技术方案为请求做一个转发

下面是一些示例


Nginx配置


server {
    listen 80;
  listen 443 ssl http2;
    server_name test.sugarat.top;
    index index.php index.html index.htm default.php default.htm default.html;
    root /xxx/aaa;
    # 省略其它配置
    location /api {
        proxy_pass http://a.b.com;
        # 防止缓存
      add_header Cache-Control no-cache;
    }
}


访问 http://test.sugarat.top/api/user/login,实际是nginx服务器 访问http://a.b.com/api/user/login


关于proxy_pass属性,更多详细内容可参考proxy_pass url 反向代理的坑


Node Server

这里采用Node原生http模块+axios实现请求的转发


const http = require('http')
const axios = require('axios').default
// 要转发到哪里去
const BASE_URL = 'http://www.baidu.com'
// 启动服务的端口
const PORT = 3000
const app = http.createServer(async (req, res) => {
    const { url, method } = req
    console.log(url);
    // 对预检请求放行
    if (method === 'OPTIONS') {
        return res.end()
    }
    // 获取传递的参数
    const reqData = await getBodyContent(req)
    console.log(reqData);
    const { data } = await axios.request({
        method,
        url,
        baseURL: BASE_URL,
        data: reqData
    })
    res.setHeader('Access-Control-Allow-Origin', '*')
    res.setHeader('Content-Type', 'application/json;charset=utf-8')
    res.end(JSON.stringify(data))
})
app.listen(PORT, () => {
    console.log(`listen ${PORT} success`);
})
function getBodyContent(req) {
    return new Promise((resolve, reject) => {
        let buffer = Buffer.alloc(0)
        req.on('data', chunk => {
            try {
                buffer = Buffer.concat([buffer, chunk])
            } catch (err) {
                console.error(err);                
            }
        })
        req.on('end', () => {
            let data = {}
            try {
                data = JSON.parse(buffer.toString('utf-8'))
            } catch (error) {
                data = {}
            } finally {
                resolve(data)
            }
        })
    })
}


测试页面


<h1>测试</h1>
<script>
    fetch('http://localhost:3000/sugrec?name=test').then(res=>res.json()).then(console.log)
</script>


运行结果,请求被成功转发


网络异常,图片无法展示
|


websocket

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现


使用示例


客户端

<body>
    <p><span>链接状态:</span><span id="status">断开</span></p>
    <label for="content">
        内容
        <input id="content" type="text">
    </label>
    <button id="send">发送</button>
    <button id="close">断开</button>
    <script>
        const ws = new WebSocket('ws:localhost:3000', 'echo-protocol')
        let status = false
        const $status = document.getElementById('status')
        const $send = document.getElementById('send')
        const $close = document.getElementById('close')
        $send.onclick = function () {
            const text = document.getElementById('content').value
            console.log('发送: ', text);
            ws.send(text)
        }
        ws.onopen = function (e) {
            console.log('connection open ...');
            ws.send('Hello')
            status = true
            $status.textContent = '链接成功'
        }
        $close.onclick = function () {
            ws.close()
        }
        ws.onmessage = function (e) {
            console.log('client received: ', e.data);
        }
        ws.onclose = function () {
            console.log('close');
            status = false
            $status.textContent = '断开连接'
        }
        ws.onerror = function (e) {
            console.error(e);
            status = false
            $status.textContent = '链接发生错误'
        }
    </script>
</body>


服务端


这里采用Node实现,需安装websocket模块

const WebSocketServer = require('websocket').server;
const http = require('http');
const server = http.createServer(function (request, response) {
    console.log((new Date()) + ' Received request for ' + request.url);
    response.writeHead(200);
    response.end();
});
server.listen(3000, function () {
    console.log((new Date()) + ' Server is listening on port 3000');
});
const wsServer = new WebSocketServer({
    httpServer: server,
    autoAcceptConnections: false
});
function originIsAllowed(origin) {
    return true;
}
wsServer.on('request', function (request) {
    if (!originIsAllowed(request.origin)) {
        request.reject();
        console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
        return;
    }
    var connection = request.accept('echo-protocol', request.origin);
    console.log((new Date()) + ' Connection accepted.');
    connection.on('message', function (message) {
        if (message.type === 'utf8') {
            console.log('Received Message: ' + message.utf8Data);
            connection.sendUTF(`${new Date().toLocaleString()}:${message.utf8Data}`);
        }
    });
    connection.on('close', function (reasonCode, description) {
        console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
    });
});


运行结果


网络异常,图片无法展示
|


location.hash


location的hash值发生变化,页面不会刷新,且浏览器提供了hashchange事件

主要用于iframe跨域通信


示例


父页面


<body>
    <h1>父页面</h1>
    <button id="send">send</button>
    <iframe id="iframe1" src="http://localhost:3001/2.html"></iframe>
    <script>
        const $send = document.getElementById('send')
        const $iframe = document.getElementById('iframe1')
        const oldSrc = $iframe.src
        $send.onclick = function () {
            $iframe.src = oldSrc + '#' + Math.random() * 100
        }
    </script>
</body>


子页面


<body>
    <h1>子页面</h1>
    <script>
        window.addEventListener('hashchange',function(e){
            console.log(e);
            console.log(location.hash);
        })
    </script>
</body>


运行结果


网络异常,图片无法展示
|


window.name


只要当前的这个浏览器tab没有关闭,无论tab内的网页如何变动,这个name值都可以保持,并且tab内的网页都有权限访问到这个值


iframe中的页面利用上述特性,实现任意页面的window.name的读取

使用示例


父页面 1.html


<body>
    <h1>父页面</h1>
    <button id="send">send</button>
    <script>
        document.getElementById('send').addEventListener('click', function () {
            getCrossIframeName('http://localhost:3000/2.html', console.log)
        })
        function getCrossIframeName(url, callback) {
            let ok = false
            const iframe = document.createElement('iframe')
            iframe.src = url
            iframe.style.width = '0px'
            iframe.style.height = '0px'
            iframe.onload = function () {
                if (ok) {
                    // 第二次触发时,同域的页面加载完成
                    callback(iframe.contentWindow.name)
                    // 移除
                    document.body.removeChild(iframe)
                } else {
                    // 第一次触发onload事件,定向到同域的中间页面
                    // 经测试 中间页面不存在也可以,如存在页面内容为空也可
                    iframe.contentWindow.location.href = '/proxy.html'
                    ok = !ok
                }
            }
            document.body.appendChild(iframe)
        }
    </script>
</body>


中间页面 proxy.html


<!-- 空文件即可 -->


目标页面 2.html


<body>
    <script>
        const data = { name: '传输的数据', status: 'success', num: Math.random() * 100 }
        window.name = JSON.stringify(data)
    </script>
</body>


运行结果


网络异常,图片无法展示
|


window.postMessage


window.postMessage 方法可以安全地实现跨源通信,可以适用的场景:


  • 与其它页面之间的消息传递
  • 与内嵌iframe通信


用法


otherWindow.postMessage(message, targetOrigin);


targetOrigin值示例:


  • 协议+主机+端口:只有三者完全匹配,消息才会被发送
  • *:传递给任意窗口
  • /:和当前窗口同源的窗口


使用示例


父页面


<body>
    <h1>父页面</h1>
    <button id="send">send</button>
    <iframe id="iframe1" src="http://localhost:3001/2.html"></iframe>
    <script>
        const $send = document.getElementById('send')
        const $iframe = document.getElementById('iframe1')
        const oldSrc = $iframe.src
        $send.onclick = function () {
            $iframe.contentWindow.postMessage(JSON.stringify({ num: Math.random() }),'*')
        }
    </script>
</body>


子页面


<body>
    <h1>子页面</h1>
    <script>
        window.addEventListener('message', function (e) {
            console.log('receive', e.data);
        })
    </script>
</body>


运行结果


网络异常,图片无法展示
|


document.domain


二级域名相同的情况下,比如 a.sugarat.top 和 b.sugarat.top 适用于该方式。

只需要给页面添加 document.domain = 'sugarat.top' 表示二级域名都相同就可以实现跨域


简单示例


首先修改host文件,添加两个自定义的域名,模拟跨域环境


网络异常,图片无法展示
|


父页面


<body>
    <h1>父页面</h1>
    <iframe id="iframe1" src="http://b.sugarat.top:3000/2.html"></iframe>
    <script>
        document.domain = 'sugarat.top'
        var a = 666
    </script>
</body>


子页面


<body>
    <h1>子页面</h1>
    <script>
        document.domain = 'sugarat.top'
        console.log('get parent data a:', window.parent.a);
    </script>
</body>


运行结果


网络异常,图片无法展示
|


总结


上文只是介绍了常见的一些跨域方案,并配上了能直接复制粘贴运行的示例,方便读者理解与上手体验


在实际生产环境中需针对特定的场景进行方案的pick


面试中这也是一道经典考题,望能帮助读者加深理解


参考



原文首发于个人博客,专题系列会整理多篇,与大家一起学习,共同进步,如有错误,还请斧正


浏览器专题系列文章


相关文章
|
1月前
|
Web App开发 JSON 安全
Chrome浏览器的跨域问题
【10月更文挑战第6天】
|
2月前
|
Web App开发 存储 前端开发
Chrome浏览器的跨域问题
Chrome浏览器的跨域问题
|
3月前
|
Web App开发 JSON 数据格式
【Azure Developer】浏览器查看本地数据文件时遇见跨域问题(CORS)
【Azure Developer】浏览器查看本地数据文件时遇见跨域问题(CORS)
【Azure Developer】浏览器查看本地数据文件时遇见跨域问题(CORS)
|
3月前
|
Web App开发 JSON 安全
【跨域难题终结者】:一键解锁Chrome浏览器神秘设置,彻底告别开发阶段的跨域烦恼!
【8月更文挑战第20天】跨域是前端开发常遇难题,尤其在前后端分离项目中。浏览器因安全考量会阻止不同源间的请求。本文对比CORS、JSONP、代理服务器等解法,并介绍开发阶段通过调整Chrome设置来临时禁用跨域限制的方法,提供启动Chrome及使用`fetch`API示例,适合快速测试。但请注意这不适用于生产环境,存在一定安全风险。
886 1
|
4月前
|
移动开发 JSON JavaScript
浏览器跨域
浏览器跨域
|
5月前
|
Web App开发 JSON 数据格式
【Azure Developer】浏览器查看本地数据文件时遇见跨域问题(CORS)
Access to XMLHttpRequest at 'file:///C:/Users/.../failedrequests.json' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome-untrusted, https, edge. reportdata/failedrequests.json:1 Fail
|
5月前
|
安全 前端开发 JavaScript
CORS是W3C标准,解决浏览器同源策略限制的跨域数据访问。
【6月更文挑战第27天】CORS是W3C标准,解决浏览器同源策略限制的跨域数据访问。它通过服务器在HTTP响应头添加`Access-Control-Allow-*`字段允许特定源请求。简单请求无需预检,非简单请求会发OPTIONS预检请求。服务器配置CORS策略,客户端正常请求,浏览器自动处理。若未正确配置,浏览器将阻止响应,保障安全。
60 0
|
6月前
|
前端开发 Java 应用服务中间件
无域可安宁?浏览器跨域问题详解与应对
无域可安宁?浏览器跨域问题详解与应对
69 10
|
6月前
谷歌浏览器跨域设置都是127.0.0.1出现跨域
谷歌浏览器跨域设置都是127.0.0.1出现跨域
256 0
|
6月前
|
JSON 前端开发 安全
浏览器跨域限制:为什么浏览器不能跨域发送Ajax请求?
浏览器跨域限制:为什么浏览器不能跨域发送Ajax请求?
105 0