feat(appendix): 添加多个交互式演示组件,完善 AI/Infra 等章节内容
- 新增 Vibe Coding 全栈相关演示组件 (DeveloperSkillShift, FrontendTriad, BackendCore 等) - 新增 RAG 相关组件 (RAGPipeline, ChunkingStrategy, Retrieval 等) - 新增 Embedding & Vector 相关组件 (EmbeddingConcept, VectorSimilarity 等) - 新增 AI Native App 设计组件 (AINativeArch, PromptDesign 等) - 新增 Infrastructure as Code 组件 (IaCConcept, TerraformWorkflow 等) - 新增 DNS & HTTPS 演示组件 (DnsResolution, HttpsHandshake 等) - 新增 Model Finetuning 组件 (FinetuningPipeline 等) - 更新多个章节的 markdown 内容,集成交互式演示
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<div class="cert-chain-demo">
|
||||
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
|
||||
🔗 证书信任链可视化
|
||||
</h4>
|
||||
<p class="intro-text">
|
||||
点击每一层证书,查看它的详细信息和在信任链中的角色。
|
||||
</p>
|
||||
|
||||
<div class="chain-container">
|
||||
<div
|
||||
v-for="(cert, idx) in certs"
|
||||
:key="idx"
|
||||
class="cert-node"
|
||||
:class="{ selected: selectedIdx === idx }"
|
||||
:style="{ '--level-color': cert.color }"
|
||||
@click="selectedIdx = idx"
|
||||
>
|
||||
<div class="cert-icon">{{ cert.icon }}</div>
|
||||
<div class="cert-title">{{ cert.title }}</div>
|
||||
<div class="cert-subtitle">{{ cert.subtitle }}</div>
|
||||
<div v-if="idx < certs.length - 1" class="chain-arrow">
|
||||
<span class="arrow-text">签发</span>
|
||||
<span class="arrow-symbol">↓</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedIdx >= 0" class="detail-panel">
|
||||
<div
|
||||
class="detail-header"
|
||||
:style="{ borderColor: certs[selectedIdx].color }"
|
||||
>
|
||||
<span class="detail-icon">{{ certs[selectedIdx].icon }}</span>
|
||||
<span class="detail-name">{{ certs[selectedIdx].title }}</span>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<div class="detail-row" v-for="(item, i) in certs[selectedIdx].details" :key="i">
|
||||
<span class="detail-label">{{ item.label }}</span>
|
||||
<span class="detail-value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-explain">
|
||||
{{ certs[selectedIdx].explain }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verify-box">
|
||||
<div class="verify-title">🔍 浏览器验证流程</div>
|
||||
<div class="verify-steps">
|
||||
<div v-for="(s, i) in verifySteps" :key="i" class="verify-step">
|
||||
<span class="verify-num">{{ i + 1 }}</span>
|
||||
<span class="verify-text">{{ s }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const selectedIdx = ref(0)
|
||||
|
||||
const certs = [
|
||||
{
|
||||
icon: '🏛️',
|
||||
title: '根证书(Root CA)',
|
||||
subtitle: '信任的起点',
|
||||
color: '#c62828',
|
||||
explain:
|
||||
'根证书是整个信任链的锚点。它由根证书颁发机构自签名,预装在操作系统和浏览器中。全球只有少数几十个根 CA,它们的安全性由严格的审计和物理安全措施保障。根 CA 的私钥通常存储在离线的硬件安全模块(HSM)中。',
|
||||
details: [
|
||||
{ label: '签发者', value: 'DigiCert Global Root G2(自签名)' },
|
||||
{ label: '有效期', value: '25 年(2013 - 2038)' },
|
||||
{ label: '密钥长度', value: 'RSA 2048 位' },
|
||||
{ label: '存储位置', value: '操作系统 / 浏览器内置信任库' },
|
||||
{ label: '数量级', value: '全球约 150 个受信根证书' }
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: '🏢',
|
||||
title: '中间证书(Intermediate CA)',
|
||||
subtitle: '信任的桥梁',
|
||||
color: '#e65100',
|
||||
explain:
|
||||
'中间证书由根 CA 签发,作为根证书和服务器证书之间的桥梁。这种分层设计的好处是:即使中间证书被泄露,也可以单独吊销它而不影响根证书。中间 CA 负责日常的证书签发工作,根 CA 的私钥因此可以保持离线状态。',
|
||||
details: [
|
||||
{ label: '签发者', value: 'DigiCert Global Root G2' },
|
||||
{ label: '持有者', value: 'DigiCert SHA2 Extended Validation Server CA' },
|
||||
{ label: '有效期', value: '10 年' },
|
||||
{ label: '用途', value: '签发终端实体(服务器)证书' },
|
||||
{ label: '可吊销', value: '是(通过 CRL 或 OCSP)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: '🌐',
|
||||
title: '服务器证书(Server Certificate)',
|
||||
subtitle: '网站的身份证',
|
||||
color: '#1565c0',
|
||||
explain:
|
||||
'服务器证书是网站向浏览器证明自己身份的凭证。它由中间 CA 签发,包含网站的域名、公钥和有效期等信息。当浏览器收到这张证书后,会沿着信任链向上验证,直到找到一个已经信任的根证书为止。',
|
||||
details: [
|
||||
{ label: '签发者', value: 'DigiCert SHA2 Extended Validation Server CA' },
|
||||
{ label: '持有者', value: 'www.example.com' },
|
||||
{ label: '有效期', value: '1 年(行业标准)' },
|
||||
{ label: '包含公钥', value: 'ECDSA P-256 公钥' },
|
||||
{ label: '验证级别', value: 'EV(扩展验证)/ DV(域名验证)' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const verifySteps = [
|
||||
'浏览器收到服务器证书,读取其签发者信息',
|
||||
'找到中间证书,用中间 CA 的公钥验证服务器证书的签名',
|
||||
'再用根 CA 的公钥验证中间证书的签名',
|
||||
'确认根证书在本地信任库中 → 整条链验证通过'
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cert-chain-demo {
|
||||
background: linear-gradient(135deg, #fce4ec 0%, #fff3e0 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.chain-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.cert-node {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 14px 24px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
width: 280px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cert-node:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.cert-node.selected {
|
||||
border-color: var(--level-color);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.cert-icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.cert-title {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: #1a1a2e;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.cert-subtitle {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.chain-arrow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.arrow-text {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.arrow-symbol {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 3px solid;
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #888;
|
||||
min-width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-explain {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
line-height: 1.7;
|
||||
background: #f5f5f5;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.verify-box {
|
||||
background: #e8f5e9;
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
.verify-title {
|
||||
font-weight: 700;
|
||||
color: #2e7d32;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.verify-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.verify-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.verify-num {
|
||||
background: #4caf50;
|
||||
color: #fff;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.verify-text {
|
||||
line-height: 1.5;
|
||||
padding-top: 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,391 @@
|
||||
<template>
|
||||
<div class="comparison-demo">
|
||||
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
|
||||
🔐 HTTP vs HTTPS 数据传输对比
|
||||
</h4>
|
||||
<div class="control-row">
|
||||
<button
|
||||
class="mode-btn"
|
||||
:class="{ active: mode === 'http' }"
|
||||
@click="mode = 'http'"
|
||||
>
|
||||
HTTP(明文)
|
||||
</button>
|
||||
<button
|
||||
class="mode-btn https"
|
||||
:class="{ active: mode === 'https' }"
|
||||
@click="mode = 'https'"
|
||||
>
|
||||
HTTPS(加密)
|
||||
</button>
|
||||
<button class="send-btn" :disabled="isSending" @click="sendData">
|
||||
{{ isSending ? '传输中...' : '发送数据' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flow-area">
|
||||
<div class="endpoint">
|
||||
<div class="ep-icon">💻</div>
|
||||
<div class="ep-label">浏览器</div>
|
||||
<div class="ep-data original">
|
||||
<div class="data-title">原始数据</div>
|
||||
<code>{{ originalData }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="transmission">
|
||||
<div class="wire" :class="mode">
|
||||
<div class="wire-label">
|
||||
{{ mode === 'http' ? '🔓 明文传输' : '🔒 加密传输' }}
|
||||
</div>
|
||||
<div
|
||||
class="packet"
|
||||
:class="{ moving: isSending, done: sendDone }"
|
||||
>
|
||||
<code class="packet-text">{{ transmittedData }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mode === 'http'" class="hacker-box">
|
||||
<div class="hacker-icon">🕵️</div>
|
||||
<div class="hacker-label">中间人可窃听</div>
|
||||
<div v-if="sendDone" class="hacker-sees">
|
||||
<code>{{ originalData }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="hacker-box blocked">
|
||||
<div class="hacker-icon">🕵️</div>
|
||||
<div class="hacker-label">中间人无法解密</div>
|
||||
<div v-if="sendDone" class="hacker-sees encrypted">
|
||||
<code>{{ encryptedData }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="ep-icon">🖥️</div>
|
||||
<div class="ep-label">服务器</div>
|
||||
<div v-if="sendDone" class="ep-data received">
|
||||
<div class="data-title">收到数据</div>
|
||||
<code>{{ originalData }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="compare-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>对比项</th>
|
||||
<th>HTTP</th>
|
||||
<th>HTTPS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in compareRows" :key="i">
|
||||
<td class="row-label">{{ row.label }}</td>
|
||||
<td class="http-cell">{{ row.http }}</td>
|
||||
<td class="https-cell">{{ row.https }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const mode = ref('http')
|
||||
const isSending = ref(false)
|
||||
const sendDone = ref(false)
|
||||
|
||||
const originalData = 'password=MySecret123&user=zhangsan'
|
||||
const encryptedData = 'a7f2c9...3b8e1d(密文)'
|
||||
|
||||
const transmittedData = ref('')
|
||||
|
||||
const compareRows = [
|
||||
{ label: '端口', http: '80', https: '443' },
|
||||
{ label: '数据加密', http: '无(明文传输)', https: 'TLS 对称加密' },
|
||||
{ label: '身份验证', http: '无', https: 'CA 证书验证服务器身份' },
|
||||
{ label: '数据完整性', http: '无保障', https: 'MAC 校验防篡改' },
|
||||
{ label: 'SEO 影响', http: '搜索引擎降权', https: '搜索引擎优先收录' },
|
||||
{ label: '性能开销', http: '无额外开销', https: 'TLS 握手增加约 1-2 RTT' }
|
||||
]
|
||||
|
||||
async function sendData() {
|
||||
if (isSending.value) return
|
||||
isSending.value = true
|
||||
sendDone.value = false
|
||||
|
||||
if (mode.value === 'http') {
|
||||
transmittedData.value = originalData
|
||||
} else {
|
||||
transmittedData.value = encryptedData
|
||||
}
|
||||
|
||||
await sleep(1500)
|
||||
sendDone.value = true
|
||||
isSending.value = false
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comparison-demo {
|
||||
background: linear-gradient(135deg, #e8eaf6 0%, #fce4ec 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 18px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
padding: 7px 18px;
|
||||
border: 2px solid #ef5350;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #c62828;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mode-btn.https {
|
||||
border-color: #43a047;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
background: #c62828;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mode-btn.https.active {
|
||||
background: #2e7d32;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 7px 18px;
|
||||
background: #1565c0;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.flow-area {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.ep-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.ep-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
|
||||
.ep-data {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 11px;
|
||||
max-width: 160px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ep-data.original {
|
||||
border: 1px solid #90caf9;
|
||||
}
|
||||
|
||||
.ep-data.received {
|
||||
border: 1px solid #a5d6a7;
|
||||
}
|
||||
|
||||
.data-title {
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.transmission {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wire {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.wire.http {
|
||||
background: #ffebee;
|
||||
border: 2px dashed #ef5350;
|
||||
}
|
||||
|
||||
.wire.https {
|
||||
background: #e8f5e9;
|
||||
border: 2px solid #43a047;
|
||||
}
|
||||
|
||||
.wire-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.packet {
|
||||
font-size: 11px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.packet.moving {
|
||||
opacity: 1;
|
||||
animation: slide 1.2s ease-in-out;
|
||||
}
|
||||
|
||||
.packet.done {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
0% {
|
||||
transform: translateX(-30px);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.packet-text {
|
||||
font-size: 10px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.hacker-box {
|
||||
background: #fff3e0;
|
||||
border: 1px solid #ffcc80;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hacker-box.blocked {
|
||||
background: #f1f8e9;
|
||||
border-color: #aed581;
|
||||
}
|
||||
|
||||
.hacker-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.hacker-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.hacker-box.blocked .hacker-label {
|
||||
color: #558b2f;
|
||||
}
|
||||
|
||||
.hacker-sees {
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
color: #c62828;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.hacker-sees.encrypted {
|
||||
color: #558b2f;
|
||||
}
|
||||
|
||||
.compare-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.compare-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.compare-table th {
|
||||
background: #37474f;
|
||||
color: #fff;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.compare-table td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.http-cell {
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.https-cell {
|
||||
color: #2e7d32;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div class="dns-record-demo">
|
||||
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
|
||||
📋 DNS 记录类型速查
|
||||
</h4>
|
||||
<div class="tab-row">
|
||||
<button
|
||||
v-for="rec in records"
|
||||
:key="rec.type"
|
||||
class="tab-btn"
|
||||
:class="{ active: selected === rec.type }"
|
||||
@click="selected = rec.type"
|
||||
>
|
||||
{{ rec.type }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="detail-card">
|
||||
<div class="detail-header">
|
||||
<span class="type-badge">{{ current.type }}</span>
|
||||
<span class="type-name">{{ current.name }}</span>
|
||||
</div>
|
||||
<p class="type-desc">{{ current.desc }}</p>
|
||||
|
||||
<div class="example-block">
|
||||
<div class="example-title">示例记录</div>
|
||||
<code class="example-code">{{ current.example }}</code>
|
||||
</div>
|
||||
|
||||
<div class="usage-block">
|
||||
<div class="usage-title">常见用途</div>
|
||||
<ul class="usage-list">
|
||||
<li v-for="(u, i) in current.usages" :key="i">{{ u }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>小贴士:</strong>
|
||||
DNS 不只是把域名翻译成 IP,它还承载了邮件路由、域名验证、负载均衡等多种功能,全靠不同的记录类型来实现。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const selected = ref('A')
|
||||
|
||||
const records = [
|
||||
{
|
||||
type: 'A',
|
||||
name: 'Address 记录',
|
||||
desc: '将域名映射到一个 IPv4 地址。这是最常见的 DNS 记录类型,浏览器访问网站时最终需要的就是这条记录。',
|
||||
example: 'example.com. IN A 93.184.216.34',
|
||||
usages: [
|
||||
'网站域名指向服务器 IP',
|
||||
'子域名指向不同的服务器',
|
||||
'配合负载均衡返回多个 IP'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'AAAA',
|
||||
name: 'IPv6 Address 记录',
|
||||
desc: '将域名映射到一个 IPv6 地址。随着 IPv4 地址耗尽,AAAA 记录变得越来越重要。',
|
||||
example: 'example.com. IN AAAA 2606:2800:220:1:248:1893:25c8:1946',
|
||||
usages: [
|
||||
'支持 IPv6 网络的设备访问',
|
||||
'双栈部署(同时配置 A 和 AAAA)',
|
||||
'面向未来的网络架构'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'CNAME',
|
||||
name: 'Canonical Name 记录',
|
||||
desc: '将一个域名指向另一个域名(别名)。浏览器会继续解析目标域名,直到找到 A 记录。',
|
||||
example: 'www.example.com. IN CNAME example.com.',
|
||||
usages: [
|
||||
'www 子域名指向主域名',
|
||||
'CDN 加速(指向 CDN 提供商域名)',
|
||||
'多个域名指向同一服务'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'MX',
|
||||
name: 'Mail Exchange 记录',
|
||||
desc: '指定负责接收该域名邮件的邮件服务器地址和优先级。数字越小优先级越高。',
|
||||
example: 'example.com. IN MX 10 mail.example.com.',
|
||||
usages: [
|
||||
'配置企业邮箱(如 Gmail、Outlook)',
|
||||
'设置邮件服务器优先级',
|
||||
'邮件备份和容灾'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'TXT',
|
||||
name: 'Text 记录',
|
||||
desc: '存储任意文本信息。常用于域名所有权验证、邮件安全策略(SPF/DKIM/DMARC)等场景。',
|
||||
example: 'example.com. IN TXT "v=spf1 include:_spf.google.com ~all"',
|
||||
usages: [
|
||||
'SPF 记录防止邮件伪造',
|
||||
'SSL 证书申请时的域名验证',
|
||||
'第三方服务的域名所有权确认'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'NS',
|
||||
name: 'Name Server 记录',
|
||||
desc: '指定该域名由哪些 DNS 服务器负责解析。这是 DNS 委派机制的核心。',
|
||||
example: 'example.com. IN NS ns1.exampledns.com.',
|
||||
usages: [
|
||||
'将域名托管到指定 DNS 服务商',
|
||||
'子域名委派给不同团队管理',
|
||||
'DNS 服务迁移'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => records.find((r) => r.type === selected.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dns-record-demo {
|
||||
background: linear-gradient(135deg, #f3e5f5 0%, #ede7f6 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.tab-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 6px 16px;
|
||||
border: 2px solid #ce93d8;
|
||||
border-radius: 20px;
|
||||
background: #fff;
|
||||
color: #7b1fa2;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: #7b1fa2;
|
||||
color: #fff;
|
||||
border-color: #7b1fa2;
|
||||
}
|
||||
|
||||
.tab-btn:hover:not(.active) {
|
||||
background: #f3e5f5;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 18px;
|
||||
margin-bottom: 14px;
|
||||
border: 1px solid #e1bee7;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
background: #7b1fa2;
|
||||
color: #fff;
|
||||
padding: 3px 12px;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
font-size: 15px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.type-desc {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 14px 0;
|
||||
}
|
||||
|
||||
.example-block {
|
||||
background: #263238;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.example-title {
|
||||
font-size: 11px;
|
||||
color: #80cbc4;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.example-code {
|
||||
color: #e0f7fa;
|
||||
font-size: 13px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.usage-block {
|
||||
background: #f3e5f5;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.usage-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #7b1fa2;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.usage-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
font-size: 13px;
|
||||
color: #444;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
margin-top: 14px;
|
||||
padding: 10px 14px;
|
||||
background: #fff3e0;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: #5d4037;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<div class="dns-resolution-demo">
|
||||
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
|
||||
🔍 DNS 解析过程模拟器
|
||||
</h4>
|
||||
<div class="input-row">
|
||||
<input
|
||||
v-model="domain"
|
||||
type="text"
|
||||
placeholder="输入域名,如 www.example.com"
|
||||
class="domain-input"
|
||||
@keyup.enter="startResolve"
|
||||
/>
|
||||
<button class="resolve-btn" :disabled="isResolving" @click="startResolve">
|
||||
{{ isResolving ? '解析中...' : '开始解析' }}
|
||||
</button>
|
||||
<button class="reset-btn" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="resolve-flow">
|
||||
<div
|
||||
v-for="(step, idx) in steps"
|
||||
:key="idx"
|
||||
class="step-card"
|
||||
:class="{
|
||||
active: currentStep === idx,
|
||||
done: currentStep > idx,
|
||||
pending: currentStep < idx
|
||||
}"
|
||||
>
|
||||
<div class="step-icon">{{ step.icon }}</div>
|
||||
<div class="step-label">{{ step.label }}</div>
|
||||
<div v-if="currentStep > idx" class="step-result">
|
||||
{{ step.result }}
|
||||
</div>
|
||||
<div v-if="currentStep === idx && isResolving" class="step-spinner">
|
||||
⏳
|
||||
</div>
|
||||
<div
|
||||
v-if="idx < steps.length - 1"
|
||||
class="arrow"
|
||||
:class="{ 'arrow-active': currentStep > idx }"
|
||||
>
|
||||
→
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="resolved" class="result-box">
|
||||
<div class="result-title">✅ 解析完成</div>
|
||||
<div class="result-detail">
|
||||
<span class="result-domain">{{ domain }}</span>
|
||||
→
|
||||
<span class="result-ip">{{ resolvedIp }}</span>
|
||||
</div>
|
||||
<div class="result-time">总耗时:约 {{ totalTime }}ms(模拟)</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>解析流程说明:</strong>
|
||||
浏览器访问网站时,需要先将域名翻译成 IP
|
||||
地址。这个过程会依次查询多级缓存和服务器,直到找到对应的 IP。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
const domain = ref('www.example.com')
|
||||
const isResolving = ref(false)
|
||||
const resolved = ref(false)
|
||||
const currentStep = ref(-1)
|
||||
const resolvedIp = ref('')
|
||||
const totalTime = ref(0)
|
||||
|
||||
const steps = reactive([
|
||||
{
|
||||
icon: '🌐',
|
||||
label: '浏览器缓存',
|
||||
result: '未命中,继续查询...'
|
||||
},
|
||||
{
|
||||
icon: '💻',
|
||||
label: '操作系统缓存',
|
||||
result: '未命中,继续查询...'
|
||||
},
|
||||
{
|
||||
icon: '🔄',
|
||||
label: '递归解析器',
|
||||
result: '向根服务器发起查询...'
|
||||
},
|
||||
{
|
||||
icon: '🌍',
|
||||
label: '根域名服务器',
|
||||
result: '返回 .com TLD 服务器地址'
|
||||
},
|
||||
{
|
||||
icon: '📂',
|
||||
label: 'TLD 服务器',
|
||||
result: '返回权威服务器地址'
|
||||
},
|
||||
{
|
||||
icon: '🏠',
|
||||
label: '权威 DNS 服务器',
|
||||
result: ''
|
||||
}
|
||||
])
|
||||
|
||||
function generateIp() {
|
||||
const a = 93 + Math.floor(Math.random() * 60)
|
||||
const b = Math.floor(Math.random() * 256)
|
||||
const c = Math.floor(Math.random() * 256)
|
||||
const d = 1 + Math.floor(Math.random() * 254)
|
||||
return `${a}.${b}.${c}.${d}`
|
||||
}
|
||||
|
||||
async function startResolve() {
|
||||
if (isResolving.value || !domain.value.trim()) return
|
||||
isResolving.value = true
|
||||
resolved.value = false
|
||||
currentStep.value = -1
|
||||
const ip = generateIp()
|
||||
resolvedIp.value = ip
|
||||
steps[5].result = `找到记录!IP = ${ip}`
|
||||
|
||||
const delays = [200, 300, 400, 500, 400, 300]
|
||||
let total = 0
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
currentStep.value = i
|
||||
await sleep(delays[i])
|
||||
total += delays[i]
|
||||
currentStep.value = i + 1
|
||||
}
|
||||
|
||||
totalTime.value = total
|
||||
resolved.value = true
|
||||
isResolving.value = false
|
||||
}
|
||||
|
||||
function reset() {
|
||||
isResolving.value = false
|
||||
resolved.value = false
|
||||
currentStep.value = -1
|
||||
resolvedIp.value = ''
|
||||
totalTime.value = 0
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dns-resolution-demo {
|
||||
background: linear-gradient(135deg, #f0f4ff 0%, #e8f0fe 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.domain-input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 8px 14px;
|
||||
border: 2px solid #c5cae9;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.domain-input:focus {
|
||||
border-color: #5c6bc0;
|
||||
}
|
||||
|
||||
.resolve-btn {
|
||||
padding: 8px 20px;
|
||||
background: #5c6bc0;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.resolve-btn:hover:not(:disabled) {
|
||||
background: #3f51b5;
|
||||
}
|
||||
|
||||
.resolve-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
padding: 8px 16px;
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
background: #bdbdbd;
|
||||
}
|
||||
|
||||
.resolve-flow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
padding: 10px 0;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.step-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 10px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
min-width: 100px;
|
||||
max-width: 120px;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
border: 2px solid transparent;
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.step-card.active {
|
||||
border-color: #ff9800;
|
||||
background: #fff8e1;
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3);
|
||||
}
|
||||
|
||||
.step-card.done {
|
||||
border-color: #4caf50;
|
||||
background: #e8f5e9;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.step-card.pending {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.step-result {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.step-spinner {
|
||||
font-size: 20px;
|
||||
animation: pulse 0.8s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
right: -14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 18px;
|
||||
color: #ccc;
|
||||
font-weight: bold;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.arrow-active {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
margin-top: 16px;
|
||||
padding: 14px 18px;
|
||||
background: #e8f5e9;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #a5d6a7;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-weight: 700;
|
||||
color: #2e7d32;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.result-detail {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-domain {
|
||||
color: #5c6bc0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-ip {
|
||||
color: #e65100;
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.result-time {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
margin-top: 14px;
|
||||
padding: 10px 14px;
|
||||
background: #fff3e0;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: #5d4037;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div class="https-handshake-demo">
|
||||
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
|
||||
🤝 TLS 握手过程演示
|
||||
</h4>
|
||||
<div class="control-row">
|
||||
<button class="start-btn" :disabled="isRunning" @click="startHandshake">
|
||||
{{ isRunning ? '握手进行中...' : '开始 TLS 握手' }}
|
||||
</button>
|
||||
<button class="reset-btn" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="handshake-area">
|
||||
<div class="side client-side">
|
||||
<div class="side-icon">💻</div>
|
||||
<div class="side-label">客户端(浏览器)</div>
|
||||
</div>
|
||||
|
||||
<div class="message-lane">
|
||||
<div
|
||||
v-for="(msg, idx) in messages"
|
||||
:key="idx"
|
||||
class="msg-row"
|
||||
:class="{
|
||||
active: currentStep === idx,
|
||||
done: currentStep > idx,
|
||||
pending: currentStep < idx
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="msg-arrow"
|
||||
:class="msg.direction === 'right' ? 'arrow-right' : 'arrow-left'"
|
||||
>
|
||||
<span class="arrow-line"></span>
|
||||
<span class="arrow-head">{{ msg.direction === 'right' ? '→' : '←' }}</span>
|
||||
</div>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name">{{ msg.name }}</div>
|
||||
<div class="msg-desc">{{ msg.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side server-side">
|
||||
<div class="side-icon">🖥️</div>
|
||||
<div class="side-label">服务器</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep >= 0 && currentStep < messages.length" class="detail-box">
|
||||
<div class="detail-title">
|
||||
{{ messages[currentStep].name }}
|
||||
</div>
|
||||
<div class="detail-text">
|
||||
{{ messages[currentStep].detail }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="handshakeDone" class="success-box">
|
||||
✅ TLS 握手完成!后续所有 HTTP 数据都将通过对称加密传输,第三方无法窃听。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isRunning = ref(false)
|
||||
const currentStep = ref(-1)
|
||||
const handshakeDone = ref(false)
|
||||
|
||||
const messages = [
|
||||
{
|
||||
name: 'Client Hello',
|
||||
direction: 'right',
|
||||
desc: '发送支持的 TLS 版本、加密套件列表、随机数',
|
||||
detail:
|
||||
'浏览器向服务器发起连接请求,告知自己支持的 TLS 版本(如 TLS 1.3)、可用的加密算法列表(如 AES-256-GCM)以及一个客户端随机数(Client Random)。这就像自我介绍:"我会这些加密方式,你选一个吧。"'
|
||||
},
|
||||
{
|
||||
name: 'Server Hello',
|
||||
direction: 'left',
|
||||
desc: '选定 TLS 版本、加密套件、服务器随机数',
|
||||
detail:
|
||||
'服务器从客户端提供的列表中选择一个最优的加密套件,并返回自己的随机数(Server Random)。相当于回应:"好的,我们就用 TLS 1.3 + AES-256-GCM 来通信。"'
|
||||
},
|
||||
{
|
||||
name: 'Certificate',
|
||||
direction: 'left',
|
||||
desc: '服务器发送数字证书(含公钥)',
|
||||
detail:
|
||||
'服务器将自己的数字证书发送给浏览器。证书中包含服务器的公钥、域名信息以及 CA 的签名。浏览器会验证证书是否由受信任的 CA 签发、是否过期、域名是否匹配。'
|
||||
},
|
||||
{
|
||||
name: 'Key Exchange',
|
||||
direction: 'right',
|
||||
desc: '双方协商生成会话密钥',
|
||||
detail:
|
||||
'在 TLS 1.3 中,客户端和服务器通过 ECDHE(椭圆曲线 Diffie-Hellman)算法交换密钥材料。双方各自生成临时密钥对,交换公钥后独立计算出相同的"预主密钥",再结合之前的随机数推导出最终的对称会话密钥。'
|
||||
},
|
||||
{
|
||||
name: 'Finished',
|
||||
direction: 'right',
|
||||
desc: '双方确认握手成功,开始加密通信',
|
||||
detail:
|
||||
'双方各自发送 Finished 消息,其中包含之前所有握手消息的摘要(用刚协商好的密钥加密)。如果对方能正确解密并验证,说明密钥协商成功,后续所有数据都将使用对称加密传输。'
|
||||
}
|
||||
]
|
||||
|
||||
async function startHandshake() {
|
||||
if (isRunning.value) return
|
||||
isRunning.value = true
|
||||
handshakeDone.value = false
|
||||
currentStep.value = -1
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
currentStep.value = i
|
||||
await sleep(1200)
|
||||
}
|
||||
|
||||
handshakeDone.value = true
|
||||
isRunning.value = false
|
||||
}
|
||||
|
||||
function reset() {
|
||||
isRunning.value = false
|
||||
currentStep.value = -1
|
||||
handshakeDone.value = false
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.https-handshake-demo {
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #e8eaf6 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
padding: 8px 20px;
|
||||
background: #1565c0;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.start-btn:hover:not(:disabled) {
|
||||
background: #0d47a1;
|
||||
}
|
||||
|
||||
.start-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
padding: 8px 16px;
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.handshake-area {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 80px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.side-icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.side-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-top: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-lane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.msg-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 2px solid transparent;
|
||||
opacity: 0.35;
|
||||
transition: all 0.4s;
|
||||
}
|
||||
|
||||
.msg-row.active {
|
||||
opacity: 1;
|
||||
border-color: #ff9800;
|
||||
background: #fff8e1;
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 3px 10px rgba(255, 152, 0, 0.2);
|
||||
}
|
||||
|
||||
.msg-row.done {
|
||||
opacity: 1;
|
||||
border-color: #4caf50;
|
||||
background: #e8f5e9;
|
||||
}
|
||||
|
||||
.msg-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 36px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.arrow-right {
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.arrow-left {
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.msg-name {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.msg-desc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.detail-box {
|
||||
margin-top: 14px;
|
||||
padding: 14px 18px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border-left: 4px solid #1565c0;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-weight: 700;
|
||||
color: #1565c0;
|
||||
margin-bottom: 6px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 13px;
|
||||
color: #444;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.success-box {
|
||||
margin-top: 14px;
|
||||
padding: 12px 18px;
|
||||
background: #e8f5e9;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #a5d6a7;
|
||||
color: #2e7d32;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user