feat: add interactive demos for AI history, Auth design, and Git intro

This commit is contained in:
sanbuphy
2026-01-19 11:25:10 +08:00
parent bb28f010e3
commit 7d86ba9504
55 changed files with 12984 additions and 5776 deletions
@@ -1,772 +1,294 @@
<!--
CSRFDefenseDemo.vue
CSRF 御演示
CSRF 手动推进 + 怎么做清单
-->
<template>
<div class="csrf-defense-demo">
<div class="csrf-demo">
<div class="header">
<div class="title">CSRF 攻击与防御</div>
<div class="subtitle">Cross-Site Request Forgery 跨站请求伪造</div>
</div>
<div class="attack-demo">
<div class="demo-title">CSRF 攻击演示</div>
<div class="attack-scenario">
<div class="scenario-box good-site">
<div class="box-header">
<span class="box-icon">🏦</span>
<span class="box-title">银行网站 bank.com</span>
</div>
<div class="box-content">
<div class="login-status" :class="{ logged: isLoggedIn }">
{{ isLoggedIn ? '✅ 已登录' : '❌ 未登录' }}
</div>
<button
v-if="isLoggedIn"
class="action-btn transfer"
@click="performTransfer"
>
💰 转账
</button>
<button v-else class="action-btn login" @click="isLoggedIn = true">
🔑 登录银行
</button>
<div class="cookie-info" v-if="isLoggedIn">
<div class="info-title">浏览器 Cookie</div>
<div class="cookie-item">
<span class="cookie-key">session_id:</span>
<span class="cookie-value">bank_session_xyz</span>
</div>
</div>
</div>
</div>
<div class="scenario-arrow"> 用户同时访问</div>
<div class="scenario-box evil-site">
<div class="box-header">
<span class="box-icon">😈</span>
<span class="box-title">恶意网站 evil.com</span>
</div>
<div class="box-content">
<div class="evil-content">
<p>🎣 欢迎来到抽奖活动</p>
<button class="action-btn evil-btn" @click="triggerAttack">
🎁 点击抽奖
</button>
<div class="evil-code" v-if="attackTriggered">
<div class="code-title">恶意代码隐藏</div>
<pre class="code-block">
&lt;img src="https://bank.com/api/transfer?to=attacker&amount=10000" /&gt;</pre
>
</div>
</div>
</div>
</div>
</div>
<div class="attack-result" v-if="attackResult">
<div class="result-box" :class="attackResult.type">
<div class="result-icon">{{ attackResult.icon }}</div>
<div class="result-text">{{ attackResult.text }}</div>
</div>
<div class="title">🛡 CSRF为什么自动带 Cookie会出事</div>
<div class="subtitle">
手动推进一个最小攻击链再看 3 个最常用防护手段SameSite / CSRF Token /
双重提交
</div>
</div>
<div class="defense-mechanisms">
<div class="mechanisms-title">防御措施</div>
<div class="controls">
<button class="btn primary" @click="start" :disabled="step !== 0">
开始
</button>
<button class="btn" @click="prev" :disabled="step <= 1">上一步</button>
<button
class="btn primary"
@click="next"
:disabled="step === 0 || step >= maxStep"
>
下一步
</button>
<button class="btn" @click="reset">重置</button>
</div>
<div class="mechanism-tabs">
<button
v-for="mechanism in mechanisms"
:key="mechanism.key"
class="tab-btn"
:class="{ active: selectedMechanism === mechanism.key }"
@click="selectedMechanism = mechanism.key"
>
<span class="tab-icon">{{ mechanism.icon }}</span>
<span class="tab-label">{{ mechanism.name }}</span>
</button>
<div v-if="step > 0" class="progress">
Step {{ step }} / {{ maxStep }} · {{ steps[step - 1]?.title }}
</div>
<div class="grid">
<div class="card">
<div class="card-title">场景</div>
<div class="desc">
假设你登录了 <strong>bank.com</strong>Cookie
已存在你又打开了一个恶意网站
<strong>evil.com</strong>它偷偷发起转账请求
</div>
<div class="box">
<div class="box-title">你的 Cookie浏览器会自动带</div>
<code class="mono">Cookie: session_id=abc123</code>
</div>
</div>
<div class="mechanism-content" v-if="currentMechanism">
<div class="mechanism-header">
<div class="header-title">{{ currentMechanism.title }}</div>
<div class="header-subtitle">{{ currentMechanism.subtitle }}</div>
</div>
<div class="mechanism-demo">
<div class="demo-flow">
<div class="flow-steps">
<div
v-for="(step, index) in currentMechanism.steps"
:key="index"
class="flow-step"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">{{ step }}</div>
</div>
</div>
</div>
<div class="code-example" v-if="currentMechanism.code">
<div class="code-title">代码示例</div>
<pre class="code-block">{{ currentMechanism.code }}</pre>
</div>
</div>
<div class="mechanism-pros-cons">
<div class="pros">
<div class="list-title">优点</div>
<ul>
<li v-for="(pro, index) in currentMechanism.pros" :key="index">
{{ pro }}
</li>
</ul>
</div>
<div class="cons">
<div class="list-title">注意事项</div>
<ul>
<li v-for="(con, index) in currentMechanism.cons" :key="index">
{{ con }}
</li>
</ul>
</div>
</div>
<div class="card">
<div class="card-title">本步请求</div>
<pre class="code"><code>{{ requestText }}</code></pre>
<div class="desc">{{ steps[step - 1]?.desc }}</div>
</div>
</div>
<div class="comparison-table">
<div class="table-title">CSRF vs XSS 对比</div>
<table>
<thead>
<tr>
<th>特性</th>
<th>CSRF</th>
<th>XSS</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>攻击方式</strong></td>
<td>冒用用户身份发送请求</td>
<td>在网页注入恶意脚本</td>
</tr>
<tr>
<td><strong>攻击目标</strong></td>
<td>trusted 网站</td>
<td>网站的其他用户</td>
</tr>
<tr>
<td><strong>利用点</strong></td>
<td>浏览器自动带 Cookie</td>
<td>网站未过滤用户输入</td>
</tr>
<tr>
<td><strong>防御重点</strong></td>
<td>验证请求来源</td>
<td>输出转义CSP</td>
</tr>
</tbody>
</table>
<div class="card">
<div class="card-title">防护怎么选优先顺序</div>
<ol class="list">
<li>
<strong>SameSite Cookie</strong
>对大多数跨站表单/图片请求非常有效Lax/Strict
</li>
<li>
<strong>CSRF Token</strong>在表单/请求头里带
token服务端校验对复杂场景最稳
</li>
<li>
<strong>双重提交 Cookie</strong>Cookie + Header 同时带
token服务端比较一致性
</li>
</ol>
<div class="warn">
<div class="warn-title">注意</div>
<div class="warn-text">
CSRF 主要针对Cookie 自动携带的场景若你用 Authorization:
Bearer不自动发送CSRF 风险会显著降低但仍要考虑 XSS/Token
泄露等问题
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { computed, ref } from 'vue'
const isLoggedIn = ref(false)
const attackTriggered = ref(false)
const attackResult = ref(null)
const selectedMechanism = ref('csrf-token')
const maxStep = 4
const step = ref(0)
const mechanisms = [
const steps = [
{
key: 'csrf-token',
icon: '🎫',
name: 'CSRF Token',
title: 'CSRF Token 验证',
subtitle: '在每个请求中添加随机 Token,服务端验证',
steps: [
'用户访问页面时,服务端生成随机 CSRF Token',
'Token 存储在 Session 中,并返回给前端',
'前端在表单中加入隐藏字段:&lt;input type="hidden" name="csrf_token" value="..."&gt;',
'提交表单时,服务端验证 Token 是否匹配',
'Token 只能用一次,验证后立即失效'
],
code: `// 后端生成 Token
app.get('/form', (req, res) => {
const token = generateRandomToken()
req.session.csrf_token = token
res.render('form', { csrf_token: token })
})
// 验证 Token
app.post('/transfer', (req, res) => {
if (req.body.csrf_token !== req.session.csrf_token) {
return res.status(403).send('CSRF Token 无效')
}
// 执行转账
})`,
pros: [
'✅ 最有效的 CSRF 防御方法',
'✅ Token 随机生成,攻击者无法预测',
'✅ 每次请求验证,安全性高'
],
cons: ['⚠️ 需要在每个表单中添加 Token', '⚠️ 增加开发和维护成本']
title: '1) 恶意站点发起跨站请求',
desc: 'evil.com 诱导你点击按钮/加载图片/提交表单,目标是 bank.com 的转账接口。'
},
{
key: 'samesite',
icon: '🍪',
name: 'SameSite Cookie',
title: 'SameSite Cookie 属性',
subtitle: '限制 Cookie 在跨站请求时发送',
steps: [
'设置 Cookie 的 SameSite 属性',
'SameSite=Strict:只在同一站点请求时发送',
'SameSite=Lax:允许安全的跨站请求(如链接跳转)',
'浏览器自动阻止跨站请求携带 Cookie',
'无需修改应用代码'
],
code: `// 设置 SameSite Cookie
app.use(session({
secret: 'your-secret',
cookie: {
sameSite: 'strict', // 或 'lax'
secure: true, // 只在 HTTPS 下传输
httpOnly: true // 防止 JavaScript 读取
}
}))`,
pros: [
'✅ 简单易用,只需设置 Cookie 属性',
'✅ 浏览器原生支持,无需修改应用逻辑',
'✅ 与其他防御方法兼容'
],
cons: ['⚠️ 老版本浏览器不支持', '⚠️ 可能影响某些合法的跨站请求']
title: '2) 浏览器自动带上 bank.com 的 Cookie',
desc: '关键点:Cookie 是“按域名自动携带”的,evil.com 不需要知道你的 session_id。'
},
{
key: 'jwt',
icon: '🎫',
name: '使用 JWT',
title: 'JWT 替代 Cookie',
subtitle: '将 Token 存储在 localStorage,不使用 Cookie',
steps: [
'用户登录后,服务端生成 JWT',
'前端将 JWT 存储在 localStorage',
'每次请求在 Header 中携带:Authorization: Bearer &lt;token&gt;',
'localStorage 的内容不会自动发送',
'天然防 CSRF 攻击'
],
code: `// 前端存储 JWT
localStorage.setItem('token', jwt_token)
// 发送请求时携带
fetch('/api/data', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})`,
pros: [
'✅ 天然防 CSRFCookie 不自动携带',
'✅ 适合前后端分离和移动端',
'✅ 易于实现'
],
cons: [
'⚠️ 容易受到 XSS 攻击',
'⚠️ 需要额外防范 XSSHttpOnly Cookie 无法用)'
]
title: '3) 服务端如果只靠 Cookie 识别用户,会误以为是你本人操作',
desc: '如果 bank.com 没做 CSRF 防护,转账可能被执行。'
},
{
title: '4) 加上 CSRF 防护后,请求会被拒绝',
desc: 'SameSite/CSRF Token 等会阻断这类跨站伪造请求。'
}
]
const currentMechanism = computed(() => {
return mechanisms.find((m) => m.key === selectedMechanism.value)
const requestText = computed(() => {
if (step.value === 0) return '(点击开始)'
if (step.value === 1) {
return `POST https://bank.com/api/transfer
Origin: https://evil.com
Content-Type: application/x-www-form-urlencoded
to=attacker&amount=1000`
}
if (step.value === 2) {
return `POST /api/transfer
Origin: https://evil.com
Cookie: session_id=abc123
to=attacker&amount=1000`
}
if (step.value === 3) {
return `(如果服务端只校验 Cookie:可能返回 200 OK 并执行转账)`
}
return `POST /api/transfer
Origin: https://evil.com
Cookie: session_id=abc123
X-CSRF-Token: <missing or invalid>
→ 403 Forbidden`
})
const performTransfer = () => {
if (!isLoggedIn.value) return
alert('正常转账:转账成功')
const start = () => {
step.value = 1
}
const triggerAttack = () => {
attackTriggered.value = true
const next = () => {
step.value = Math.min(maxStep, step.value + 1)
}
if (isLoggedIn.value) {
attackResult.value = {
type: 'danger',
icon: '⚠️',
text: 'CSRF 攻击成功!浏览器自动带上了银行的 Cookie,转账请求被发送。'
}
} else {
attackResult.value = {
type: 'warning',
icon: '🛡️',
text: '攻击失败:用户未登录银行网站。'
}
}
const prev = () => {
step.value = Math.max(1, step.value - 1)
}
const reset = () => {
step.value = 0
}
</script>
<style scoped>
.csrf-defense-demo {
.csrf-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
border-radius: 8px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
margin: 1rem 0;
}
.header {
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.3rem;
font-weight: 800;
color: var(--vp-c-text-1);
}
.subtitle {
margin-top: 0.25rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.attack-demo {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1.5rem;
}
.demo-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
.attack-scenario {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
align-items: stretch;
margin-bottom: 1rem;
}
.scenario-box {
background: var(--vp-c-bg-soft);
border-radius: 10px;
padding: 1rem;
border: 2px solid var(--vp-c-divider);
.controls {
display: flex;
flex-direction: column;
}
.scenario-box.good-site {
border-color: #3b82f6;
}
.scenario-box.evil-site {
border-color: #ef4444;
}
.box-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
flex-wrap: wrap;
margin-bottom: 0.75rem;
}
.box-icon {
font-size: 1.5rem;
.btn {
padding: 0.5rem 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
cursor: pointer;
font-weight: 700;
font-size: 0.875rem;
}
.btn.primary {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: var(--vp-c-bg);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.progress {
color: var(--vp-c-text-2);
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
}
.card-title {
font-weight: 800;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
}
.desc {
color: var(--vp-c-text-2);
line-height: 1.75;
}
.box {
margin-top: 0.75rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
border-radius: 8px;
padding: 0.75rem;
}
.box-title {
font-weight: 600;
font-size: 0.9rem;
font-weight: 800;
margin-bottom: 0.35rem;
color: var(--vp-c-text-1);
}
.box-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.75rem;
.mono {
font-family: var(--vp-font-family-mono);
}
.login-status {
padding: 0.5rem;
border-radius: 6px;
text-align: center;
font-weight: 600;
font-size: 0.85rem;
background: #fef3c7;
color: #92400e;
}
.login-status.logged {
background: #d1fae5;
color: #065f46;
}
.action-btn {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.2s ease;
}
.action-btn.login {
background: #3b82f6;
color: white;
}
.action-btn.transfer {
background: #22c55e;
color: white;
}
.action-btn.evil-btn {
background: #ef4444;
color: white;
}
.cookie-info {
background: white;
border-radius: 6px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.info-title {
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
}
.cookie-item {
font-family: 'Courier New', monospace;
font-size: 0.75rem;
display: flex;
gap: 0.5rem;
}
.cookie-key {
color: #3b82f6;
font-weight: 600;
}
.cookie-value {
color: var(--vp-c-text-2);
word-break: break-all;
}
.scenario-arrow {
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: 700;
color: var(--vp-c-text-2);
writing-mode: vertical-rl;
text-orientation: mixed;
}
.evil-content {
text-align: center;
}
.evil-content p {
font-size: 0.9rem;
margin-bottom: 1rem;
}
.evil-code {
background: #1e293b;
border-radius: 6px;
padding: 0.75rem;
margin-top: 1rem;
text-align: left;
}
.code-title {
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
margin-bottom: 0.5rem;
}
.code-block {
font-family: 'Courier New', monospace;
font-size: 0.75rem;
color: #e2e8f0;
.code {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
.attack-result {
margin-top: 1rem;
}
.result-box {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
border-radius: 8px;
font-weight: 600;
}
.result-box.danger {
background: rgba(239, 68, 68, 0.1);
border: 1px solid #ef4444;
color: #dc2626;
}
.result-box.warning {
background: rgba(245, 158, 11, 0.1);
border: 1px solid #f59e0b;
color: #d97706;
}
.result-icon {
font-size: 1.5rem;
}
.defense-mechanisms {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
padding: 0.75rem;
border-radius: 6px;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
margin-bottom: 1.5rem;
overflow-x: auto;
color: var(--vp-c-text-1);
}
.mechanisms-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
.mechanism-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.tab-btn {
flex: 1;
min-width: 120px;
padding: 0.75rem 1rem;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.tab-btn:hover {
border-color: var(--vp-c-brand);
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.tab-icon {
font-size: 1.2rem;
}
.mechanism-content {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.mechanism-header {
margin-bottom: 1.5rem;
}
.header-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.3rem;
}
.header-subtitle {
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.mechanism-demo {
margin-bottom: 1.5rem;
}
.demo-flow {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.flow-step {
display: flex;
gap: 0.75rem;
align-items: flex-start;
}
.step-number {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.85rem;
flex-shrink: 0;
}
.step-content {
flex: 1;
font-size: 0.85rem;
line-height: 1.5;
padding-top: 0.25rem;
}
.code-example {
background: #1e293b;
border-radius: 8px;
padding: 1rem;
}
.code-example .code-title {
font-size: 0.8rem;
font-weight: 600;
color: #94a3b8;
margin-bottom: 0.75rem;
}
.code-example .code-block {
font-size: 0.8rem;
line-height: 1.6;
}
.mechanism-pros-cons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.pros,
.cons {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1rem;
}
.list-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.pros ul,
.cons ul {
.list {
margin: 0;
padding-left: 1.25rem;
padding-left: 1.2rem;
color: var(--vp-c-text-2);
line-height: 1.75;
}
.pros li,
.cons li {
font-size: 0.85rem;
margin-bottom: 0.5rem;
line-height: 1.5;
}
.comparison-table {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.table-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
.warn {
margin-top: 0.75rem;
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.18);
background: rgba(var(--vp-c-brand-rgb), 0.06);
border-radius: 8px;
padding: 0.75rem;
text-align: left;
font-weight: 700;
font-size: 0.85rem;
border-bottom: 2px solid var(--vp-c-divider);
}
tbody td {
padding: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
.warn-title {
font-weight: 800;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
tbody tr:last-child td {
border-bottom: none;
.warn-text {
color: var(--vp-c-text-2);
line-height: 1.7;
}
@media (max-width: 768px) {
.attack-scenario {
grid-template-columns: 1fr;
}
.scenario-arrow {
writing-mode: horizontal-tb;
transform: rotate(90deg);
}
.mechanism-tabs {
flex-direction: column;
}
.mechanism-pros-cons {
@media (max-width: 720px) {
.grid {
grid-template-columns: 1fr;
}
}