同源策略:是浏览器的一个安全策略。
主要是为了防范我们的恶意网站读取我们的数据
协议+端口+域名都一样才叫同源。
同源策略主要限制的是一个源(Origin)的文档或脚本,对另一个源的资源进行“读”和“写”的操作。最典型的场景就是 AJAX 请求。
- 跨域DOM操作:直接读取iframe银行网站的DOM。
<!-- 尝试嵌入银行网站的 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响应内容。
// 尝试请求银行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中调用这个回调函数,需要后端配合。
res.end('callback('+data+')');
//前端
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
后端可以给出响应头,告诉哪些东西能够访问。
当遇到简单请求头,不需要预检。当遇到复杂请求头,需要设置服务器哪些请求头允许访问。
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 或标签页之间的通信
- 通信过程中的数据不会经过服务器,也不需要服务器参与或配置
- 即使是跨域通信,也只是浏览器绕过了同源策略的限制,允许不同源的窗口互相发送消息
- 所有消息传递都在客户端完成,数据不会通过网络发送到服务器
// 在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);
}
});
<!-- 在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>
// 在被打开的页面(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,这种情况下某些浏览器会允许连接。
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);
})
})
<!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、调用对方的方法、读写对方的变量等
<!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>
<!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>
添加启动命令
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 (从外部链接返回可能导致未登录态) (同站指顶级域名+1相同)
- Lax:顶级导航(用户点击链接,地址栏输入URL)且为安全方法(head,option,get) 顶级窗口的GET表单提交,预加载请求 允许携带cookie
- None:设置为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头方式(推荐)
Content-Security-Policy: default-src 'self';
元标签方式
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
可以控制的源有很多,script,图片, css等。
2.代码转义。
将含有 <script>这样的脚本进行转义,
<script>
console.log("这是示例代码");
</script>
3.设置cookie为HttpOnly
这样不允许js去拿到cookie
总结:
同源策略是浏览器端的一个安全策略,它可以防止恶意网站获取我们的信息。他是只有当一个源如何与另一个源进行资源交互。http协议,url端口三者都相同时,才能获取到我们的信息。
例如我们对银行网站进行了登录,访问恶意网站他通过请求去携带我们的cookie获取数据。 例如网站嵌套iframe子窗口的时候,可能进行支付时候的一个弹窗,他可以阻止页面获取另一个的页面数据,不让隐私信息泄露。
首先我们生产环境下的解决方案主要有 jsonp,cors,nginx反向代理。 jsonp 是比较老的一种解决跨域问题的方案,他主要通过script标签的src属性是不受同源策略的影响的,我们的前端首先要定义一个回调函数挂载到全局,接着后端返回一段js脚本,调用这个回调函数,将传递的数据通过这个回调函数进行传参。 但是他有一些缺点,例如:只支持get请求,script标签加载阻塞页面,不好处理http访问失败的错误。
现在主要通过CORS进行跨域问题的处理。 我们可以通过配置CORS响应头来进行一些处理。当我们是简单请求的时候,也就是HEAD,GET,POST请求,并且不含自定义请求头,contenttype为plain/text,x-www-uncoloded,multipate/form-Date。他会直接发送到我们的服务端,接着我们可以设置一下哪些源允许接收数据。当一些复杂请求发送,比如会修改我们服务器的状态的方式,首先他会发送一个预检请求,判断一下可不可以发送。我们可以指定哪个源的数据允许访问,哪些方法,请求头,是否携带cookie,预检缓存时间等。不过这里的预检缓存只针对同样的请求,比如你添加了头信息,即使同样的get请求,也需要再次预检。
我们还有像postmessage方法,它允许我们的两个tab或者之前描述的那种iframe窗口之间的通信,他们之间的通信是不经过服务器的。他是一个订阅发布系统的模式,一个通过页面引用来调用postmessage发送方法,另一个窗口通过监听message事件来接收数据。或者可以通过document.domain。他首先必须得是父域相同的两个子域。通过设置decument.domain设置 相同的父域名。也是通过引用,但是它可以直接调用另一个窗口的方法或访问数据。
我们还可以使用反向代理,例如使用nginx,或者开发期间使用vite的代理配置。或者我们开发期间之间用启动参数去关闭浏览器的跨域措施。
websocket也是不受同源策略的影响的。
CSRF攻击: 它是恶意网站伪造我们的身份信息进行信息窃取。例如我们都知道script标签的src属性是不受同源策略影响,并且还会自动携带目标网站的cookie。那恶意网站趁我们登录,可能用这个特性向服务器发起请求,伪造身份获取信息。
最简单的措施是验证一下referer和origin的值判断一下是否是目标网站。 现在主要的防范措施就是通过攻击者无法获取和设置的信息进行判断。 最常用的方法是CSRF token,我们将一个token放置在表单之中,接着发起请求的时候必须携带这个token,我们知道src属性是设置不了请求头的。或者进行双cookie验证。用户登录后的token我们不仅放在cookie中,还要放在请求头里,接着通过这两个进行比对来判断。不同则是恶意网站。 或者将jwt存储在localstorage里,我们的攻击者是访问不到其他域的存储的。 或者设置一下samesite-cookie为lax。
XSS攻击:
这是攻击者通过脚本注入的方式进行攻击。有反射型XSS,恶意脚本通过服务器响应反射回来,存储型XSS,他是存储在服务器中,例如你在发布的博客里面写一段js脚本,所有看见这篇博客的人都会被攻击,比如把cookie泄露出去。DOM型CSS,完全不经过服务器,在处理DOM时候生成,比如hash值。
防范措施有设置cookie为httponly,将标签进行转义,csp:他可以通过http响应头或者元标签启用。他会将要加载的资源如脚本,图片与白名单中的资源进行对比,成功放行,否则阻止加载或执行。
问题:
Strict:仅同站点请求携带Cookie (从外部链接返回可能导致未登录态) (同站指顶级域名+1相同)
- Lax:顶级导航(用户点击链接,地址栏输入URL)且为安全方法(head,option,get) 顶级窗口的GET表单提交,预加载请求 允许携带cookie。
- None:设置为none必须设置Secure使用,任何请求都会携带cookie。
- SameSite 如何防范 CSRF 攻击?它能完全替代 CSRF token 吗?
限制请求的cookie携带。 例如状态修改的GET请求,同站不同子域攻击。兼容性也不好。 所以高安全场景使用CSRF Token + sameSite
- 使用 SameSite=None 时,为什么必须同时设置 Secure 属性?不这样做会有什么后果?
HTTPS 确保cooike在传输过程不泄露。 Cookie被拒绝:现代浏览器会直接拒绝设置这样的Cookie,出现SameSiteNoneInsecure错误。
- 从攻击者角度,如何绕过 SameSite 的保护机制?
利用顶级导航允许携带构建诱导性链接执行GET请求。
- 在处理子域名时,SameSite 策略如何工作?比如 sub.example.com 和 example.com 是否被视为同站?
不同协议视为不同站。公共后缀前面加一个域名段(.com .cn)。