【计算机网络】【HTTP】跨域
一、什么是跨域
域:域既是 window 网络操作系统的逻辑组织单元,也是Internet的逻辑组织单元,他是安全边界
只有域的所有者才能访问管理域内部的资源,若其它的域要访问或者管理,则需要该域赋予其它域相关权限
跨域,全称跨源资源共享,英文名称 CORS(Cross-Origin Resource Sharing),跨域既是从其它域获取资源的操作。
二、为什么会跨域
跨域出现的原因是浏览器的同源策略引起的
2.1 什么是源
web内容的源由用于访问它的URL的方案(协议),主机(域名)和端口定义。只有当方案,主机和端口都匹配时,两个对象具有相同的起源
所谓同源指的是域名、协议、端口相同。若有其中一个不同,浏览器将会认为非同源,也就是跨域。
2.2 URL结构
URL 代表着是统一资源定位符(Uniform Resource Locator)。URL无非就是一个给定的独特资源在 web 上的地址
URL结构组成:
-
Schme 或者 Protocol
-
Domain Name 也叫做host域名
-
port 端口号
-
Parameters参数
-
Anchor 锚点,一般用于定位
2.3 同源不同源举例
- 同源栗子:
- 相同的 scheme http 和host:example.com/app1/index.… 和 example.com/app2/index.…
- http 默认80端口所以同源: Example.com:80 和 example.com
- 不同源栗子:
- 协议不同:example.com/app1 和 example.com/app2
- host不同:example.com 和 myapp.example.com
- 端口不同:example.com 和 example.com:8080
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-Methods
和Access-Control-Allow-Headers
,用于控制允许的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");
资料来源:
转载自:https://juejin.cn/post/7304538199148920895