同源策略:是浏览器的一个安全策略。
主要是为了防范我们的恶意网站读取我们的数据
- 跨域DOM操作:直接读取iframe银行网站的DOM。
js
<!-- 尝试嵌入银行网站的 iframe -->
<iframe id="bankFrame" src="https://bank.com/login"></iframe>
<script>
// 尝试读取 iframe 中的 DOM
const iframe = document.getElementById('bankFrame');
try {
// 这里会抛出安全错误
const bankInput = iframe.contentDocument.getElementById('password');
console.log('窃取到密码输入框:', bankInput); // 永远执行不到这里
} catch (e) {
console.error('同源策略阻止了DOM访问:', e.message);
// 输出:SecurityError: Blocked a frame with origin "http://evil.com" from accessing a cross-origin frame.
}
</script>
- 阻止js读取fetch响应内容。
js
// 尝试请求银行API
fetch('https://bank.com/api/balance')
.then(response => {
// 这里永远无法读取到实际响应
console.log('响应状态:', response.status); // 可能显示200
return response.json();
})
.then(data => {
console.log('窃取到余额数据:', data); // 永远不会执行到这里
})
.catch(error => {
console.error('同源策略阻止了响应读取:', error);
// 输出:TypeError: Failed to fetch (实际响应已被浏览器屏蔽)
});
其实我们的请求会到达服务器,只是在浏览器端进行了跨域拦截。 因此仍然会存在一些问题,例如CSRF攻击。 因此在发出一些复杂请求(非简单)时,会预先发出预检Option请求,来获取跨域操作信息,决定能否对服务器进行发出请求。
简单请求有如下: Get请求 Head请求 post请求(不含自定义请求头, content-Type为 application/x-www-unloadedcoded multipateform-data text/plain)
Option请求
预检请求有缓存机制 Access-Control-Max-Age 存在内存中(关闭即清除) 隐私模式不进行缓存。
当预检请求通过,才会发送复杂请求,不通过报同源错误。
解决跨域的方案
jsonP
首先我们会在客户端定义一个函数,接着服务器返回的js中调用这个回调函数,需要后端配合。
js
res.end('callback('+data+')');
js
//前端
funstion jsonp(url,back){
const script = document.createElement('script');
script.src =url;
document.appendChild(script);
window.callaback = callback;
}
jsonp('http://localhost:8080',function(data){
console.log(data);
}
);
CORS
后端可以给出响应头,告诉哪些东西能够访问。
当遇到简单请求头,不需要预检。当遇到复杂请求头,需要设置服务器哪些请求头允许访问。
js
app.use((req, res, next) => {
console.log('中间件来了')
console.log(req.headers)
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5503')
next()
})
//处理OPTIONS预检请求
app.options('*', (req, res) => {
res.setHeader('Access-Control-Max-Age', '86400')// 设置缓存
res.setHeader('Access-Control-Allow-Methods', 'POST') // 设置允许方式,针对复杂请求
res.setHeader('Access-Control-Allow-Headers', 'Content-Type') // 设置响应头,针对复杂响应头
res.sendStatus(204)
})
app.get('/', (req, res) => {
res.send('hello world')
})
app.post('/',(req,res)=>{
console.log('Post来了')
res.send('hello world')
})
注意: 我们的 Access-Control-Allow-Header Access-Control-Allow-Methods 只要设置在Option请求即可,发现没,他是专门为我们的复杂数据预检准备的,并且只需要在这里设置之后,在post的接收中就不必再设置了。
postMessage
他解决的并不是浏览器与服务器之间的跨域通信,而是不同浏览器窗口(iframe,tab)的跨域通信问题。
- postMessage 完全在浏览器内部工作,用于不同窗口、iframe 或标签页之间的通信
- 通信过程中的数据不会经过服务器,也不需要服务器参与或配置
- 即使是跨域通信,也只是浏览器绕过了同源策略的限制,允许不同源的窗口互相发送消息
- 所有消息传递都在客户端完成,数据不会通过网络发送到服务器
js
// 在domainA.com页面中
const otherTab = window.open('https://domainB.com/page');
// 等待新页面加载完成后发送消息
setTimeout(() => {
otherTab.postMessage('来自domainA的消息', 'https://domainB.com');
}, 1000);
// 在domainB.com页面中
window.addEventListener('message', (event) => {
// 检查消息来源
console.log('消息来源:', event.origin);
console.log('消息内容:', event.data);
// 回复消息到源页面
if (event.origin === 'https://domainA.com') {
event.source.postMessage('已收到你的消息', event.origin);
}
});
js
<!-- 在domainA.com -->
<iframe id="communicator" src="https://domainB.com/bridge.html" style="display:none;"></iframe>
<script>
const iframe = document.getElementById('communicator');
// 确保iframe加载完成
iframe.onload = function() {
// 向iframe发送跨域消息
iframe.contentWindow.postMessage('发送到domainB的消息', 'https://domainB.com');
};
// 接收来自iframe的消息
window.addEventListener('message', function(event) {
if (event.origin === 'https://domainB.com') {
console.log('收到来自domainB的回复:', event.data);
}
});
</script>
<!-- 在domainB.com/bridge.html -->
<script>
// 监听来自父页面的消息
window.addEventListener('message', function(event) {
if (event.origin === 'https://domainA.com') {
console.log('收到来自domainA的消息:', event.data);
// 回复消息
event.source.postMessage('domainB已收到消息', event.origin);
// 如果需要,还可以转发消息到domainB的其他页面
// 比如通过localStorage或其他方式
}
});
</script>
js
// 在被打开的页面(domainB.com)中
if (window.opener) {
window.opener.postMessage('这是来自新标签页的消息', 'https://domainA.com');
}
// 接收来自opener的消息
window.addEventListener('message', (event) => {
if (event.origin === 'https://domainA.com') {
console.log('收到opener消息:', event.data);
}
});
注意:他必须得获取窗口的引用,比如通过window.open , iframe标签 得到引用才能进行通信
代理配置 vite Nginx反向代理
vite代理和Nginx都是同样原理,我们的前端项目部署之后。我们的请求发送到同域名,接着做一个转发,这样就不会有跨域问题
Nginx反向代理,之所以叫反向代理,是他不暴露出服务器的网址给客户端。他还支持负载均衡,提供Http服务。错误日志,Http缓存。
websocket
TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。
浏览器对localhost有特殊处理,对于同一主机名下的不同端口之间的通信,通常会采用较为宽松的跨域策略。您的前端页面可能是从 http://localhost的某个端口访问的,连接到ws://localhost:8080,这种情况下某些浏览器会允许连接。
js
const express = require('express');
const app = express();
let WebSocketServer = require('ws').Server;
let wss = new WebSocketServer({port:8080});
wss.on('connection',function(ws){
console.log('有新的连接');
ws.on('message',function(msg){
console.log(msg);
})
})
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
let ws = new WebSocket('ws://localhost:8080');
ws.onopen = function(){
console.log('连接成功');
}
ws.onmessage = function(event){
console.log(event.data);
}
</script>
</body>
</html>
iframe + document.domain
- 当两个窗口(或iframe)属于同一个父域的不同子域时(如a.example.com和b.example.com)
- 通过在两个页面中都设置document.domain = 'example.com'
- 浏览器会认为它们是"同源"的,从而解除跨域限制
- 两个窗口就可以相互访问DOM、调用对方的方法、读写对方的变量等
js
<!DOCTYPE html>
<html>
<head>
<title>主页面 - www.example.com</title>
</head>
<body>
<h1>这是主域名页面</h1>
<!-- 加载跨子域的iframe -->
<iframe id="myFrame" src="http://api.example.com/b.html" style="width:100%;height:200px;"></iframe>
<script>
// 设置document.domain为共同的父域
document.domain = 'example.com';
// 等待iframe加载完成
window.onload = function() {
var frame = document.getElementById('myFrame');
// 尝试访问iframe中的内容
try {
console.log('iframe中的标题是:', frame.contentWindow.document.title);
// 调用iframe中的方法
frame.contentWindow.sayHello('从父页面发来的消息');
// 添加按钮来测试通信
var btn = document.createElement('button');
btn.innerHTML = '调用iframe中的方法';
btn.onclick = function() {
frame.contentWindow.sayHello('按钮点击时发送的消息');
};
document.body.appendChild(btn);
} catch(e) {
console.error('访问iframe失败:', e);
}
};
</script>
</body>
</html>
js
<!DOCTYPE html>
<html>
<head>
<title>iframe页面 - api.example.com</title>
</head>
<body>
<h2>这是子域名中的iframe</h2>
<div id="message"></div>
<script>
// 设置document.domain为共同的父域
document.domain = 'example.com';
// 定义可被父页面调用的函数
function sayHello(msg) {
document.getElementById('message').innerHTML = '收到消息: ' + msg;
// 可以访问父页面的方法和属性
window.parent.document.title = '标题已被iframe修改';
return '来自iframe的回应';
}
// 也可以直接调用父页面的方法或修改属性
try {
console.log('父页面的URL是:', window.parent.location.href);
} catch(e) {
console.error('访问父页面失败:', e);
}
</script>
</body>
</html>
添加启动命令
js
chrome.exe --disable-web-security
CSRF 攻击
即使有了同源策略,仍然无法阻止CSRF攻击
举一个例子,假如我们的一个Tab中打开了银行网站,另一个Tab中打开了恶意网站。假如我们恶意网站中使用了 img , script , link 标签的src属性,他们会自动携带目标网址的cookie(登录凭证),直接进行转账操作。
解决措施:
1. CSRF Token(最有效的方法)
- 原理:
- 生成:服务器为每个用户会话生成一个唯一、随机的token
- 存储:服务器将此token存储在用户的会话中
- 分发:服务器将token嵌入到页面中(表单隐藏字段、JavaScript变量等)
- 提交:用户提交请求时,token随请求一起发送
- 验证:服务器验证接收到的token是否与存储的匹配
注意: src属性总是会携带cookie fetch要手动指定是否携带cokie axios默认携带同源请求cookie,跨域携带cookie需要设置。表单提交会自动携带cookie。
2. SameSite Cookie 属性
- 设置:Set-Cookie: session=123; SameSite=Strict(或Lax)
- 效果:限制第三方网站发送的请求携带Cookie
- 模式:
- Strict:仅同站点请求携带Cookie (从外部链接返回可能导致未登录态)
- Lax:导航到目标网站的链接(
<a href>
)且为安全方法(head,option,get)可以携带Cookie - None:需要配合Secure使用,都会携带cookie。
3. 检查请求来源
3.1 验证Referer头:确保请求来自合法域名
优势:实现简单,大多数浏览器都会发送该头
局限性:
- 用户可能已禁用Referer头(出于隐私考虑)
- 某些代理可能会移除或修改此头
- 攻击者可能在某些情况下伪造Referer
- 验证Origin头:更可靠,不受修改
3.2 验证Origin头
- 优势:
- 比Referer更可靠,内容更简洁
- 用户无法通过浏览器设置修改
- 即使在HTTPS到HTTP的请求中也会发送(而Referer可能不会)
- 不包含路径信息,更好地保护隐私
4. 双重Cookie验证
- 服务器设置一个cookie返回给前端
- 在Cookie和请求参数中都包含相同的随机值
- 服务器检查两者是否匹配
5. 关键操作二次验证
- 敏感操作要求重新验证身份(密码、手机验证码)
- 添加验证码(CAPTCHA)防止自动提交
总结: 不同防护机制的共同本质
所有CSRF防护机制的核心是:利用同源策略限制,依赖攻击者无法获取/设置的信息进行请求验证。
让我们看看各种CSRF防护机制如何体现这一本质:
1. 传统CSRF Token
- 关键信息:服务器生成的随机token
- 存储位置:服务器端会话 + 客户端页面(非Cookie)
- 验证原理:恶意站点无法获取用户在目标网站上的token
2. 双Cookie验证
- 关键信息:随机生成的值
- 存储位置:Cookie + 手动添加的请求组件
- 验证原理:恶意站点虽然能触发带Cookie的请求,但无法读取Cookie值
3. 检查请求来源
- 关键信息:请求的Referer/Origin头
- 存储位置:浏览器自动生成
- 验证原理:恶意站点无法伪造合法的来源头
4. JWT(特定实现方式)
- 关键信息:包含用户信息的签名令牌
- 存储位置:localStorage/sessionStorage(非Cookie)
- 验证原理:恶意站点无法访问其他域的存储
XSS攻击
XSS (Cross-Site Scripting) 跨站脚本攻击是一种常见的网络安全漏洞,攻击者通过在网页中注入恶意脚本代码,当用户浏览该页面时,恶意脚本会在用户的浏览器中执行,从而实现各种攻击目的。
反射型XSS (Reflected XSS)
- 特点:不存储在服务器上,通常通过URL参数等方式传递
- 过程:
- 攻击者构造包含恶意脚本的链接
- 用户点击链接后,服务器将恶意代码作为响应的一部分返回
- 用户浏览器执行这段恶意代码
2. 存储型XSS (Stored XSS)
- 特点:恶意代码被存储在服务器数据库中
- 过程:
- 攻击者在论坛、评论区等位置提交包含恶意代码的内容
- 服务器将内容存储在数据库中
- 当其他用户访问包含该内容的页面时,恶意代码被执行
- 危害:影响范围广,所有访问页面的用户都可能受到攻击
3. DOM型XSS (DOM-based XSS)
- 特点:完全在客户端执行,服务器不参与处理恶意数据
- 过程:
- 恶意代码通过URL片段(#)等方式被传入
- 客户端JavaScript读取并处理这些数据
- 不恰当的处理导致恶意代码被执行
解决方案:
1. CSP
他是一个浏览器端执行的安全策略,其核心工作原理:
- 识别资源来源:浏览器会检查每个资源(脚本、样式、图片等)的来源
- 对照白名单:将资源来源与CSP策略中允许的来源列表进行比对
- 阻止非法资源:如果资源来自未经授权的源,浏览器会直接阻止该资源加载或执行
CSP通过HTTP响应头或HTML元标签启用:
HTTP头方式(推荐)
js
Content-Security-Policy: default-src 'self';
元标签方式
js
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
可以控制的源有很多,script,图片, css等。
2.代码转义。
将含有 <script>
这样的脚本进行转义,
js
<script>
console.log("这是示例代码");
</script>
3.设置cookie为HttpOnly
这样不允许js去拿到cookie