Files
mcn-short-video/project/短视频对话分析/dist/reference/oauth-device-binding/index.html
T

1072 lines
61 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WorkBuddy Skills MCP OAuth 鉴权与设备指纹绑定完整方案</title>
<style>
:root {
--bg: #f8fafc;
--card: #ffffff;
--border: #e2e8f0;
--text: #1e293b;
--text2: #475569;
--text3: #94a3b8;
--blue: #3b82f6;
--blue-bg: #eff6ff;
--purple: #8b5cf6;
--purple-bg: #f5f3ff;
--green: #22c55e;
--green-bg: #f0fdf4;
--red: #ef4444;
--red-bg: #fef2f2;
--orange: #f59e0b;
--orange-bg: #fffbeb;
--shadow: 0 1px 3px 0 rgba(0,0,0,0.06), 0 1px 2px -1px rgba(0,0,0,0.06);
--shadow-lg: 0 4px 6px -1px rgba(0,0,0,0.07), 0 2px 4px -2px rgba(0,0,0,0.05);
--radius: 10px;
--radius-sm: 6px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
padding: 24px;
max-width: 1100px;
margin: 0 auto;
}
/* ── Header ── */
.header {
background: linear-gradient(135deg, var(--blue), var(--purple));
color: #fff;
padding: 36px 40px;
border-radius: var(--radius);
margin-bottom: 28px;
box-shadow: var(--shadow-lg);
}
.header h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; letter-spacing: 0.02em; }
.header p { font-size: 14px; opacity: 0.88; max-width: 680px; }
/* ── Section ── */
.section { margin-bottom: 28px; }
.section-title {
font-size: 18px; font-weight: 700;
margin-bottom: 14px;
padding-bottom: 8px;
border-bottom: 2px solid var(--border);
display: flex; align-items: center; gap: 8px;
}
.section-title .ico { font-size: 20px; }
/* ── Card ── */
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
margin-bottom: 16px;
box-shadow: var(--shadow);
}
.card-title {
font-size: 15px; font-weight: 700; margin-bottom: 12px;
display: flex; align-items: center; gap: 8px;
}
/* ── Flow Diagram (SVG) ── */
.diagram-wrap {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
overflow-x: auto;
box-shadow: var(--shadow);
margin-bottom: 16px;
}
.diagram-wrap svg { display: block; margin: 0 auto; }
/* ── Code Block ── */
.code-block {
background: #1e293b;
color: #e2e8f0;
border-radius: var(--radius-sm);
padding: 16px 20px;
font-family: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace;
font-size: 12.5px;
line-height: 1.66;
overflow-x: auto;
white-space: pre;
margin-bottom: 12px;
box-shadow: var(--shadow);
}
.code-block .kw { color: #c084fc; }
.code-block .str { color: #86efac; }
.code-block .cm { color: #64748b; font-style: italic; }
.code-block .fn { color: #7dd3fc; }
.code-block .num { color: #fbbf24; }
.code-block .op { color: #f472b6; }
.code-inline {
background: var(--blue-bg);
color: var(--blue);
padding: 1px 6px;
border-radius: 3px;
font-family: Consolas, monospace;
font-size: 12px;
}
/* ── Table ── */
.info-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin-bottom: 12px;
}
.info-table th, .info-table td {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid var(--border);
}
.info-table th {
background: var(--blue-bg);
color: var(--blue);
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.info-table tr:last-child td { border-bottom: none; }
.info-table td:first-child { font-weight: 600; color: var(--text); }
.info-table .tag {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.tag-green { background: var(--green-bg); color: var(--green); }
.tag-red { background: var(--red-bg); color: var(--red); }
.tag-blue { background: var(--blue-bg); color: var(--blue); }
.tag-purple { background: var(--purple-bg); color: var(--purple); }
.tag-orange { background: var(--orange-bg); color: var(--orange); }
/* ── Steps ── */
.step-list { counter-reset: step; list-style: none; }
.step-list li {
counter-increment: step;
padding: 10px 10px 10px 44px;
position: relative;
border-left: 2px solid var(--border);
margin-left: 12px;
}
.step-list li::before {
content: counter(step);
position: absolute;
left: -14px;
top: 10px;
width: 26px; height: 26px;
background: var(--blue);
color: #fff;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 700;
}
.step-list li:last-child { border-left: 2px solid transparent; }
/* ── Warning/Callout ── */
.callout {
padding: 14px 18px;
border-left: 4px solid;
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
margin-bottom: 12px;
font-size: 13px;
background: var(--card);
}
.callout-warn { border-color: var(--orange); background: var(--orange-bg); }
.callout-info { border-color: var(--blue); background: var(--blue-bg); }
.callout-danger{ border-color: var(--red); background: var(--red-bg); }
.callout-green { border-color: var(--green); background: var(--green-bg); }
/* ── TOC ── */
.toc {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px 24px;
margin-bottom: 28px;
box-shadow: var(--shadow);
}
.toc-title { font-weight: 700; font-size: 15px; margin-bottom: 10px; }
.toc a {
display: block;
padding: 4px 0;
color: var(--blue);
text-decoration: none;
font-size: 13px;
transition: color 0.15s;
}
.toc a:hover { color: var(--purple); }
/* ── Footer ── */
.footer {
text-align: center;
color: var(--text3);
font-size: 12px;
padding: 20px;
border-top: 1px solid var(--border);
margin-top: 32px;
}
/* ── Comparison grid ── */
.comp-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
font-size: 13px;
}
.comp-item {
padding: 12px 16px;
border-radius: var(--radius-sm);
background: var(--card);
border: 1px solid var(--border);
}
.comp-item .label {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
font-weight: 600; color: var(--text3); margin-bottom: 4px;
}
.comp-item .val { font-weight: 700; font-family: Consolas, monospace; word-break: break-all; }
/* ── Tab system ── */
.tab-nav { display: flex; gap: 2px; margin-bottom: 0; }
.tab-btn {
padding: 8px 18px;
border: 1px solid var(--border);
border-bottom: none;
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
background: #f1f5f9;
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: var(--text2);
transition: all 0.15s;
}
.tab-btn.active { background: var(--card); color: var(--blue); font-weight: 600; }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ── Responsive ── */
@media (max-width: 768px) {
body { padding: 12px; }
.header { padding: 24px; }
.comp-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- ═══════════════ HEADER ═══════════════ -->
<div class="header">
<h1>WorkBuddy Skills → MCP/API<br>OAuth 2.0 鉴权 + 设备指纹绑定完整方案</h1>
<p>���盖 OAuth 2.0 授权码流程、设备指纹生成与绑定、安全存储、服务端校验、防 Token 复制攻击的全链路实施方案</p>
</div>
<!-- ═══════════════ TOC ═══════════════ -->
<div class="toc">
<div class="toc-title">目录</div>
<a href="#s1">一、OAuth 2.0 鉴权流程(泳道图)</a>
<a href="#s2">二、设备指纹绑定流程(架构图)</a>
<a href="#s3">三、硬件信号采集 → Python 实现</a>
<a href="#s4">四、设备指纹 + WorkBuddy 实例绑定</a>
<a href="#s5">五、OS 原生安全存储</a>
<a href="#s6">六、OAuth 客户端集成</a>
<a href="#s7">七、MCP 服务端校验中间件</a>
<a href="#s8">八、攻击场景与防护验证</a>
<a href="#s9">九、方案对比与推荐组合</a>
</div>
<!-- ═══════════════ S1: OAuth 泳道图 ═══════════════ -->
<div class="section" id="s1">
<div class="section-title"><span class="ico">🔐</span> 一、OAuth 2.0 鉴权流程(四泳道)</div>
<div class="card">
<div class="card-title">流程说明</div>
<p style="font-size:13px;color:var(--text2);margin-bottom:12px;">
四条泳道(用户 → WorkBuddy → Skills → MCP/API)自上而下描述完整的 OAuth 2.0 授权码鉴权流程。包含 <b style="color:var(--red);">Token 缺失走 OAuth</b> 和 <b style="color:var(--green);">Token 有效跳过 OAuth</b> 两条路径。
</p>
<!-- Tab: 流程图 / 详细说明 -->
<div class="tab-nav">
<div class="tab-btn active" onclick="switchTab(event,'tab-s1-flow')">流程图</div>
<div class="tab-btn" onclick="switchTab(event,'tab-s1-detail')">步骤详解</div>
</div>
<div class="tab-panel active" id="tab-s1-flow" style="margin-top:0; border:1px solid var(--border); border-radius:0 var(--radius-sm) var(--radius-sm) var(--radius-sm);">
<div class="diagram-wrap" style="border:none;margin:0;border-radius:0;">
<svg viewBox="0 0 1000 780" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="ar-gray" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#64748b"/>
</marker>
<marker id="ar-red" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#ef4444"/>
</marker>
<marker id="ar-green" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#22c55e"/>
</marker>
</defs>
<!-- Lane backgrounds -->
<rect x="0" y="0" width="1000" height="780" fill="#f8fafc" rx="8"/>
<rect x="10" y="48" width="235" height="724" fill="#eff6ff" rx="6" opacity="0.5"/>
<rect x="255" y="48" width="235" height="724" fill="#f5f3ff" rx="6" opacity="0.5"/>
<rect x="500" y="48" width="235" height="724" fill="#f0fdf4" rx="6" opacity="0.5"/>
<rect x="745" y="48" width="245" height="724" fill="#fffbeb" rx="6" opacity="0.5"/>
<!-- Lane headers -->
<rect x="10" y="10" width="235" height="34" fill="#3b82f6" rx="6"/>
<text x="127" y="32" text-anchor="middle" fill="#fff" font-size="13" font-weight="700">用户</text>
<rect x="255" y="10" width="235" height="34" fill="#8b5cf6" rx="6"/>
<text x="372" y="32" text-anchor="middle" fill="#fff" font-size="13" font-weight="700">WorkBuddy</text>
<rect x="500" y="10" width="235" height="34" fill="#22c55e" rx="6"/>
<text x="617" y="32" text-anchor="middle" fill="#fff" font-size="13" font-weight="700">Skills</text>
<rect x="745" y="10" width="245" height="34" fill="#f59e0b" rx="6"/>
<text x="867" y="32" text-anchor="middle" fill="#fff" font-size="13" font-weight="700">MCP / API</text>
<!-- Nodes -->
<!-- s01: 用户 -->
<rect x="50" y="64" width="155" height="48" fill="#fff" stroke="#93c5fd" stroke-width="1.5" rx="6"/>
<text x="127" y="83" text-anchor="middle" fill="#64748b" font-size="9" font-weight="600">步骤 01</text>
<text x="127" y="99" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="600">用户发起任务</text>
<!-- s02: WorkBuddy -->
<rect x="295" y="140" width="155" height="48" fill="#fff" stroke="#c4b5fd" stroke-width="1.5" rx="6"/>
<text x="372" y="159" text-anchor="middle" fill="#64748b" font-size="9" font-weight="600">步骤 02</text>
<text x="372" y="175" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="600">加载 Skill 解析意图</text>
<!-- s03: Skills -->
<rect x="540" y="216" width="155" height="48" fill="#fff" stroke="#86efac" stroke-width="1.5" rx="6"/>
<text x="617" y="235" text-anchor="middle" fill="#64748b" font-size="9" font-weight="600">步骤 03</text>
<text x="617" y="251" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="600">调用 MCP 工具</text>
<!-- s04: WorkBuddy Token check -->
<rect x="295" y="295" width="155" height="60" fill="#fff" stroke="#c4b5fd" stroke-width="1.5" rx="6"/>
<text x="372" y="314" text-anchor="middle" fill="#64748b" font-size="9" font-weight="600">步骤 04</text>
<text x="372" y="330" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="600">查询本地 Token</text>
<rect x="310" y="337" width="125" height="14" fill="#fef2f2" stroke="#fca5a5" stroke-width="1" rx="3"/>
<text x="372" y="348" text-anchor="middle" fill="#ef4444" font-size="9" font-weight="600">缺失 → 走 OAuth</text>
<!-- s05: 用户 OAuth -->
<rect x="50" y="390" width="155" height="60" fill="#fff" stroke="#93c5fd" stroke-width="1.5" rx="6"/>
<text x="127" y="409" text-anchor="middle" fill="#64748b" font-size="9" font-weight="600">步骤 05</text>
<text x="127" y="425" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="600">浏览器 OAuth 授权</text>
<rect x="65" y="432" width="125" height="14" fill="#eff6ff" stroke="#93c5fd" stroke-width="1" rx="3"/>
<text x="127" y="443" text-anchor="middle" fill="#3b82f6" font-size="9" font-weight="600">登录 &amp; 同意授权</text>
<!-- s06: WorkBuddy callback -->
<rect x="295" y="470" width="155" height="48" fill="#fff" stroke="#c4b5fd" stroke-width="1.5" rx="6"/>
<text x="372" y="489" text-anchor="middle" fill="#64748b" font-size="9" font-weight="600">步骤 06</text>
<text x="372" y="505" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="600">接收回调 code→Token</text>
<!-- s07: WorkBuddy store -->
<rect x="295" y="546" width="155" height="48" fill="#fff" stroke="#c4b5fd" stroke-width="1.5" rx="6"/>
<text x="372" y="565" text-anchor="middle" fill="#64748b" font-size="9" font-weight="600">步骤 07</text>
<text x="372" y="581" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="600">Token 持久化加密存储</text>
<!-- s08: MCP gateway -->
<rect x="785" y="650" width="165" height="48" fill="#fff" stroke="#fcd34d" stroke-width="1.5" rx="6"/>
<text x="867" y="669" text-anchor="middle" fill="#64748b" font-size="9" font-weight="600">步骤 08</text>
<text x="867" y="685" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="600">鉴权网关校验 Bearer</text>
<!-- s09: MCP business -->
<rect x="785" y="726" width="165" height="40" fill="#fff" stroke="#fcd34d" stroke-width="1.5" rx="6"/>
<text x="867" y="744" text-anchor="middle" fill="#64748b" font-size="9" font-weight="600">步骤 09</text>
<text x="867" y="758" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="600">执行业务逻辑 → 返回数据</text>
<!-- ── Orthogonal arrows ── -->
<g stroke-linejoin="round" fill="none">
<!-- 01→02: right→left, L-shape -->
<path d="M205,88 L215,88 L215,164 L295,164" stroke="#64748b" stroke-width="1.5" marker-end="url(#ar-gray)"/>
<!-- 02→03: right→left, L-shape -->
<path d="M450,164 L460,164 L460,240 L540,240" stroke="#64748b" stroke-width="1.5" marker-end="url(#ar-gray)"/>
<!-- 03→04: left→right (back), L-shape -->
<path d="M540,240 L530,240 L530,325 L450,325" stroke="#64748b" stroke-width="1.5" marker-end="url(#ar-gray)"/>
<!-- 04→05: left→right (back), RED -->
<path d="M295,325 L285,325 L285,420 L205,420" stroke="#ef4444" stroke-width="2" marker-end="url(#ar-red)"/>
<text x="262" y="380" text-anchor="middle" fill="#ef4444" font-size="10" font-weight="700">Token缺失</text>
<!-- 04→08: right→left, GREEN dash (skip OAuth) -->
<path d="M450,335 L460,335 L460,674 L785,674" stroke="#22c55e" stroke-width="2" stroke-dasharray="6 4" marker-end="url(#ar-green)"/>
<rect x="560" y="490" width="130" height="28" fill="#f0fdf4" stroke="#22c55e" stroke-width="1" rx="4"/>
<text x="625" y="503" text-anchor="middle" fill="#22c55e" font-size="10" font-weight="700">Token有效</text>
<text x="625" y="515" text-anchor="middle" fill="#22c55e" font-size="9" font-weight="600">跳过 OAuth</text>
<!-- 05→06: right→left, L-shape -->
<path d="M205,420 L215,420 L215,494 L295,494" stroke="#64748b" stroke-width="1.5" marker-end="url(#ar-gray)"/>
<!-- 06→07: same lane, top→bottom -->
<path d="M372,518 L372,546" stroke="#64748b" stroke-width="1.5" marker-end="url(#ar-gray)"/>
<!-- 07→08: right→left, L-shape -->
<path d="M450,570 L460,570 L460,674 L785,674" stroke="#64748b" stroke-width="1.5" marker-end="url(#ar-gray)"/>
<!-- 08→09: same lane -->
<path d="M867,698 L867,726" stroke="#64748b" stroke-width="1.5" marker-end="url(#ar-gray)"/>
</g>
<!-- Return flow bar -->
<rect x="745" y="4" width="245" height="4" fill="#94a3b8" rx="2" opacity="0"/>
<text x="500" y="775" text-anchor="middle" fill="#94a3b8" font-size="10">
← 结果逐层回传:MCP → Skills → WorkBuddy → 用户
</text>
</svg>
</div>
</div>
<div class="tab-panel" id="tab-s1-detail" style="border:1px solid var(--border); border-radius:0 var(--radius-sm) var(--radius-sm) var(--radius-sm);padding:16px;">
<ol class="step-list">
<li style="margin-bottom:6px"><b>用户发起任务</b> — 在 WorkBuddy 对话框中输入需要调用 MCP 服务的指令</li>
<li style="margin-bottom:6px"><b>WorkBuddy 加载 Skill</b> — 根据指令匹配并加载对应 Skill,解析用户意图</li>
<li style="margin-bottom:6px"><b>Skills 调用 MCP 工具</b> — Skill 执行业务逻辑,发起 <span class="code-inline">tools/call</span> 请求</li>
<li style="margin-bottom:6px"><b>查询本地 Token</b> ⚠️ <span style="color:var(--red);font-weight:600;">关键分叉点</span> — WorkBuddy 检查 <span class="code-inline">~/.workbuddy/oauth_tokens/</span> 是否有有效凭证</li>
<li style="margin-bottom:6px"><b>浏览器 OAuth 授权</b> — Token 缺失时打开浏览器,用户登录第三方服务并同意授权</li>
<li style="margin-bottom:6px"><b>接收回调 code → Token</b> — WorkBuddy 接收 <span class="code-inline">authorization_code</span>,向 Token 端点换取 <span class="code-inline">access_token</span> + <span class="code-inline">refresh_token</span></li>
<li style="margin-bottom:6px"><b>Token 持久化加密存储</b> — 将 Token 存入本地安全区(DPAPI/Keychain/Secret Service)</li>
<li style="margin-bottom:6px"><b>鉴权网关校验 Bearer Token</b> — MCP 服务端验证 JWT 签名、过期时间、设备绑定</li>
<li style="margin-bottom:6px"><b>执行业务逻辑</b> — 鉴权通过后执行 MCP 工具,返回结果逐层回传至用户</li>
</ol>
</div>
</div>
</div>
<!-- ═══════════════ S2: 设备指纹绑定流程 ═══════════════ -->
<div class="section" id="s2">
<div class="section-title"><span class="ico">🖥️</span> 二、设备指纹绑定流程</div>
<div class="card">
<p style="font-size:13px;color:var(--text2);margin-bottom:14px;">
设备指纹是防 Token 复制的核心机制。每次 OAuth 授权时,WorkBuddy 将本机硬件指纹写入 JWT 的 <span class="code-inline">cnf</span> (confirmation) 声明。后续 MCP 请求必须携带相同的指纹,否则服务端拒绝访问。
</p>
<div class="diagram-wrap" style="border:none;margin:0;">
<svg viewBox="0 0 960 1050" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="ab-gray" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#64748b"/>
</marker>
<marker id="ab-red" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#ef4444"/>
</marker>
</defs>
<!-- Background -->
<rect x="0" y="0" width="960" height="1050" fill="#f8fafc" rx="8"/>
<!-- Col 1: Hardware Layer -->
<rect x="16" y="56" width="290" height="980" fill="#eff6ff" rx="6" opacity="0.6"/>
<text x="161" y="38" text-anchor="middle" fill="#3b82f6" font-size="14" font-weight="700">硬件层 — 设备指纹生成</text>
<!-- Col 2: Binding Layer -->
<rect x="322" y="56" width="300" height="980" fill="#f5f3ff" rx="6" opacity="0.6"/>
<text x="472" y="38" text-anchor="middle" fill="#8b5cf6" font-size="14" font-weight="700">绑定层 — OAuth + JWT 签发</text>
<!-- Col 3: Verify Layer -->
<rect x="638" y="56" width="306" height="980" fill="#f0fdf4" rx="6" opacity="0.6"/>
<text x="791" y="38" text-anchor="middle" fill="#22c55e" font-size="14" font-weight="700">校验层 — 每次 MCP 请求</text>
<!-- ═══════════ Col 1: Hardware ═══════════ -->
<!-- Block 1: 采集硬件信号 -->
<rect x="36" y="84" width="250" height="230" fill="#fff" stroke="#bfdbfe" stroke-width="1.5" rx="6"/>
<text x="161" y="110" text-anchor="middle" fill="#1e293b" font-size="13" font-weight="700">采集硬件信号</text>
<line x1="56" y1="122" x2="266" y2="122" stroke="#e2e8f0" stroke-width="1"/>
<text x="56" y="146" fill="#475569" font-size="12">MachineGuid — 系统安装唯一ID</text>
<text x="56" y="172" fill="#475569" font-size="12">BIOS 序列号 — 固件烧录,不可导出</text>
<text x="56" y="198" fill="#475569" font-size="12">主板序列号 — 物理级唯一标识</text>
<text x="56" y="224" fill="#475569" font-size="12">CPU ID — 处理器唯一标识</text>
<text x="56" y="250" fill="#475569" font-size="12">MAC 地址 / 磁盘序列号 — 补充</text>
<text x="56" y="280" fill="#94a3b8" font-size="11">Windows: MachineGuid + PowerShell</text>
<text x="56" y="300" fill="#94a3b8" font-size="11">macOS: IOPlatformUUID + ioreg</text>
<!-- Arrow Block1→Block2 -->
<line x1="161" y1="318" x2="161" y2="370" stroke="#64748b" stroke-width="1.5" marker-end="url(#ab-gray)"/>
<!-- Block 2: SHA-256 -->
<rect x="51" y="378" width="220" height="80" fill="#fff" stroke="#bfdbfe" stroke-width="1.5" rx="6"/>
<text x="161" y="404" text-anchor="middle" fill="#1e293b" font-size="13" font-weight="700">SHA-256 哈希组合</text>
<text x="161" y="432" text-anchor="middle" fill="#475569" font-size="12">→ 64位设备指纹哈希值</text>
<text x="161" y="452" text-anchor="middle" fill="#94a3b8" font-size="10">组合后不可逆,不泄露原始信号</text>
<!-- Horizontal Arrow Col1→Col2 -->
<line x1="271" y1="418" x2="322" y2="418" stroke="#64748b" stroke-width="1.5" marker-end="url(#ab-gray)"/>
<!-- Block 3: 本机实测 -->
<rect x="40" y="500" width="242" height="92" fill="#f0fdf4" stroke="#86efac" stroke-width="1.5" rx="6"/>
<text x="161" y="524" text-anchor="middle" fill="#22c55e" font-size="12" font-weight="700">✓ 本机实测结果</text>
<text x="161" y="550" text-anchor="middle" fill="#475569" font-size="11">4/4 核心信号采集成功</text>
<text x="161" y="572" text-anchor="middle" fill="#94a3b8" font-size="11">XIAOMI TM2413 / Win11 / i5-13500H / 32GB</text>
<!-- ═══════════ Col 2: Binding ═══════════ -->
<!-- Block 1: 实例 ID -->
<rect x="342" y="84" width="260" height="82" fill="#fff" stroke="#ddd6fe" stroke-width="1.5" rx="6"/>
<text x="472" y="110" text-anchor="middle" fill="#1e293b" font-size="13" font-weight="700">WorkBuddy 实例 ID</text>
<text x="472" y="138" text-anchor="middle" fill="#475569" font-size="12">UUID v4 — 首次安装生成,永不变化</text>
<text x="472" y="156" text-anchor="middle" fill="#94a3b8" font-size="10">软件层第二因子:同型号设备也能区分</text>
<!-- Arrow Block1→Block2 -->
<line x1="472" y1="170" x2="472" y2="224" stroke="#64748b" stroke-width="1.5" marker-end="url(#ab-gray)"/>
<!-- Block 2: HMAC -->
<rect x="342" y="232" width="260" height="98" fill="#fff" stroke="#ddd6fe" stroke-width="1.5" rx="6"/>
<text x="472" y="258" text-anchor="middle" fill="#1e293b" font-size="13" font-weight="700">HMAC-SHA256 绑定密钥</text>
<line x1="362" y1="270" x2="582" y2="270" stroke="#e2e8f0" stroke-width="1"/>
<text x="472" y="292" text-anchor="middle" fill="#475569" font-size="12">key = 设备指纹 · msg = 实例ID</text>
<text x="472" y="314" text-anchor="middle" fill="#8b5cf6" font-size="11" font-weight="600">🔐 双因子绑定:硬件 + 软件 = 唯一密钥</text>
<!-- Arrow Block2→Block3 -->
<line x1="472" y1="334" x2="472" y2="390" stroke="#64748b" stroke-width="1.5" marker-end="url(#ab-gray)"/>
<!-- Block 3: OAuth 携带 -->
<rect x="342" y="398" width="260" height="128" fill="#fff" stroke="#ddd6fe" stroke-width="1.5" rx="6"/>
<text x="472" y="424" text-anchor="middle" fill="#1e293b" font-size="13" font-weight="700">OAuth 授权时携带绑定信息</text>
<line x1="362" y1="436" x2="582" y2="436" stroke="#e2e8f0" stroke-width="1"/>
<text x="362" y="460" fill="#475569" font-size="12">device_fingerprint = sha256_hash</text>
<text x="362" y="484" fill="#475569" font-size="12">wb_instance_id = uuid_v4_string</text>
<text x="362" y="508" fill="#475569" font-size="12">binding_key = hmac_result</text>
<text x="362" y="526" fill="#94a3b8" font-size="10">+ PKCE code_verifier(防授权劫持)</text>
<!-- Arrow Block3→Block4 -->
<line x1="472" y1="530" x2="472" y2="590" stroke="#64748b" stroke-width="1.5" marker-end="url(#ab-gray)"/>
<!-- Block 4: JWT -->
<rect x="342" y="598" width="260" height="150" fill="#fff" stroke="#c4b5fd" stroke-width="2" rx="6"/>
<text x="472" y="624" text-anchor="middle" fill="#1e293b" font-size="13" font-weight="700">服务端签发 JWT(含绑定声明)</text>
<line x1="362" y1="636" x2="582" y2="636" stroke="#e2e8f0" stroke-width="1"/>
<text x="362" y="660" fill="#8b5cf6" font-size="11" font-weight="600">"cnf": {</text>
<text x="382" y="682" fill="#475569" font-size="12">"device_fp": "sha256_hash_value",</text>
<text x="382" y="704" fill="#475569" font-size="12">"wb_instance": "uuid_v4_string",</text>
<text x="382" y="726" fill="#475569" font-size="12">"binding_key": "hmac_result"</text>
<text x="362" y="748" fill="#8b5cf6" font-size="11" font-weight="600">}</text>
<!-- Horizontal Arrow Col2→Col3 -->
<line x1="602" y1="670" x2="638" y2="670" stroke="#64748b" stroke-width="1.5" marker-end="url(#ab-gray)"/>
<!-- ═══════════ Col 3: Verify ═══════════ -->
<!-- Block 1: MCP 请求 -->
<rect x="658" y="84" width="266" height="118" fill="#fff" stroke="#bbf7d0" stroke-width="1.5" rx="6"/>
<text x="791" y="110" text-anchor="middle" fill="#1e293b" font-size="13" font-weight="700">每次 MCP 请求</text>
<line x1="678" y1="122" x2="904" y2="122" stroke="#e2e8f0" stroke-width="1"/>
<text x="678" y="144" fill="#475569" font-size="12">Authorization: Bearer &lt;jwt_token&gt;</text>
<text x="678" y="168" fill="#475569" font-size="12">X-Device-Fingerprint: &lt;current_fp&gt;</text>
<text x="678" y="192" fill="#475569" font-size="12">X-Binding-Key: &lt;current_bk&gt;</text>
<!-- Arrow Block1→Block2 -->
<line x1="791" y1="206" x2="791" y2="260" stroke="#64748b" stroke-width="1.5" marker-end="url(#ab-gray)"/>
<!-- Block 2: 校验比对 -->
<rect x="658" y="268" width="266" height="104" fill="#fff" stroke="#bbf7d0" stroke-width="1.5" rx="6"/>
<text x="791" y="294" text-anchor="middle" fill="#1e293b" font-size="13" font-weight="700">服务端逐项校验比对</text>
<line x1="678" y1="306" x2="904" y2="306" stroke="#e2e8f0" stroke-width="1"/>
<text x="678" y="330" fill="#475569" font-size="12">① JWT.cnf.device_fp == 请求头 device_fp ?</text>
<text x="678" y="354" fill="#475569" font-size="12">② JWT.cnf.binding_key == 请求头 binding_key ?</text>
<!-- Split: 匹配 ✓ → 200 OK -->
<line x1="791" y1="376" x2="791" y2="428" stroke="#22c55e" stroke-width="1.5" marker-end="url(#ab-gray)"/>
<text x="718" y="408" fill="#22c55e" font-size="11" font-weight="700">匹配 ✓</text>
<!-- Split: 不匹配 ✗ → L-shaped route to 403 below -->
<polyline points="924,372 954,372 954,597 924,597" fill="none" stroke="#ef4444" stroke-width="1.5" marker-end="url(#ab-red)"/>
<text x="940" y="488" fill="#ef4444" font-size="11" font-weight="700">不匹配 ✗</text>
<!-- Block 3a: 200 OK -->
<rect x="658" y="436" width="266" height="74" fill="#f0fdf4" stroke="#86efac" stroke-width="1.5" rx="6"/>
<text x="791" y="462" text-anchor="middle" fill="#22c55e" font-size="13" font-weight="700">200 OK · 正常执行业务逻辑</text>
<text x="791" y="486" text-anchor="middle" fill="#475569" font-size="11">身份确认 ✓ 设备合法 ✓ 绑定一致 ✓</text>
<text x="791" y="504" text-anchor="middle" fill="#94a3b8" font-size="10">继续执行 MCP 工具调用</text>
<!-- Block 3b: 403 (单独一行,在 200 OK 下方) -->
<rect x="658" y="560" width="266" height="74" fill="#fef2f2" stroke="#fca5a5" stroke-width="1.5" rx="6"/>
<text x="791" y="586" text-anchor="middle" fill="#ef4444" font-size="13" font-weight="700">403 Forbidden · 立即吊销 Token</text>
<text x="791" y="610" text-anchor="middle" fill="#475569" font-size="11">设备指纹不匹配 → 拒绝请求 → 吊销凭据</text>
<text x="791" y="628" text-anchor="middle" fill="#94a3b8" font-size="10">复制 Token 到其他机器无效</text>
<!-- ═══════════ Bottom: Secure Storage ═══════════ -->
<rect x="80" y="820" width="640" height="70" fill="#f1f5f9" stroke="#cbd5e1" stroke-width="1.5" stroke-dasharray="6 4" rx="6"/>
<text x="400" y="846" text-anchor="middle" fill="#1e293b" font-size="13" font-weight="700">🔒 安全存储层 — 密钥永不明文落盘</text>
<text x="400" y="870" text-anchor="middle" fill="#64748b" font-size="12">Windows: DPAPI(绑定用户SID+机器密钥)| macOS: Keychain(Secure Enclave 硬件级保护)| Linux: Secret Service(用户登录密码加密)</text>
<!-- Arrow HMAC → Secure Storage -->
<line x1="472" y1="820" x2="472" y2="840" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#ab-gray)"/>
<text x="488" y="833" fill="#94a3b8" font-size="10" font-weight="600">密钥写入平台安全区</text>
</svg>
</div>
</div>
</div>
<!-- ═══════════════ S3: 硬件信号采集 ═══════════════ -->
<div class="section" id="s3">
<div class="section-title"><span class="ico">📡</span> 三、硬件信号采集 — Python 实现</div>
<div class="card">
<p style="font-size:13px;color:var(--text2);margin-bottom:12px;">
跨平台硬件信号采集器,Windows / macOS / Linux 三端适配。信号哈希后不可逆,不泄露原始硬件信息。
</p>
<div class="code-block"><span class="kw">import</span> hashlib, platform, subprocess, uuid, json, os
<span class="kw">class</span> <span class="fn">DeviceFingerprint</span>:
<span class="cm">"""跨平台设备指纹生成器"""</span>
<span class="kw">@staticmethod</span>
<span class="kw">def</span> <span class="fn">_windows_signals</span>() -> dict:
signals = {}
<span class="cm"># 1) MachineGuid — Windows 安装实例唯一ID,重装系统才变</span>
<span class="kw">import</span> winreg
<span class="kw">with</span> winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
<span class="str">r"SOFTWARE\Microsoft\Cryptography"</span>) <span class="kw">as</span> key:
signals[<span class="str">'machine_guid'</span>] = winreg.QueryValueEx(key, <span class="str">"MachineGuid"</span>)[<span class="num">0</span>]
<span class="cm"># 2) BIOS 序列号 — 烧录在固件中,物理级不可复制</span>
r = subprocess.run(
[<span class="str">"powershell"</span>, <span class="str">"-Command"</span>,
<span class="str">"(Get-WmiObject Win32_BIOS).SerialNumber"</span>],
capture_output=<span class="kw">True</span>, text=<span class="kw">True</span>, timeout=<span class="num">5</span>)
signals[<span class="str">'bios_serial'</span>] = r.stdout.strip()
<span class="cm"># 3) 主板序列号 — 备用物理标识</span>
r = subprocess.run(
[<span class="str">"powershell"</span>, <span class="str">"-Command"</span>,
<span class="str">"(Get-WmiObject Win32_BaseBoard).SerialNumber"</span>],
capture_output=<span class="kw">True</span>, text=<span class="kw">True</span>, timeout=<span class="num">5</span>)
signals[<span class="str">'board_serial'</span>] = r.stdout.strip()
<span class="kw">return</span> signals
<span class="kw">@staticmethod</span>
<span class="kw">def</span> <span class="fn">_macos_signals</span>() -> dict:
signals = {}
r = subprocess.run([<span class="str">"ioreg"</span>, <span class="str">"-rd1"</span>, <span class="str">"-c"</span>, <span class="str">"IOPlatformExpertDevice"</span>],
capture_output=<span class="kw">True</span>, text=<span class="kw">True</span>, timeout=<span class="num">5</span>)
<span class="kw">for</span> line <span class="kw">in</span> r.stdout.split(<span class="str">'\n'</span>):
<span class="kw">if</span> <span class="str">"IOPlatformUUID"</span> <span class="kw">in</span> line:
signals[<span class="str">'platform_uuid'</span>] = line.split(<span class="str">'"'</span>)[-<span class="num">2</span>]
<span class="kw">if</span> <span class="str">"IOPlatformSerialNumber"</span> <span class="kw">in</span> line:
signals[<span class="str">'serial'</span>] = line.split(<span class="str">'"'</span>)[-<span class="num">2</span>]
<span class="kw">return</span> signals
<span class="kw">@staticmethod</span>
<span class="kw">def</span> <span class="fn">_linux_signals</span>() -> dict:
signals = {}
<span class="kw">with</span> open(<span class="str">'/etc/machine-id'</span>) <span class="kw">as</span> f:
signals[<span class="str">'machine_id'</span>] = f.read().strip()
<span class="kw">with</span> open(<span class="str">'/sys/class/dmi/id/board_serial'</span>) <span class="kw">as</span> f:
signals[<span class="str">'board_serial'</span>] = f.read().strip()
<span class="kw">return</span> signals
<span class="kw">@classmethod</span>
<span class="kw">def</span> <span class="fn">generate</span>(cls) -> str:
system = platform.system().lower()
signals = {<span class="str">'windows'</span>: cls._windows_signals,
<span class="str">'darwin'</span>: cls._macos_signals,
<span class="str">'linux'</span>: cls._linux_signals}[system]()
raw = json.dumps(signals, sort_keys=<span class="kw">True</span>)
<span class="kw">return</span> hashlib.sha256(raw.encode()).hexdigest()</div>
</div>
<div class="card">
<div class="card-title">本机实测信号值</div>
<table class="info-table">
<tr><th>信号</th><th>值</th><th>可靠性</th></tr>
<tr><td>MachineGuid</td><td style="font-family:Consolas,monospace;font-size:12px;">164ecf8e-d075-4349-8fbe-b2d4dd928efe</td><td><span class="tag tag-green">★★★★★</span></td></tr>
<tr><td>BIOS 序列号</td><td style="font-family:Consolas,monospace;font-size:12px;">67422/25SE01488</td><td><span class="tag tag-green">★★★★☆</span></td></tr>
<tr><td>主板序列号</td><td style="font-family:Consolas,monospace;font-size:12px;">525M59SRMP000559P02BS</td><td><span class="tag tag-green">★★★★☆</span></td></tr>
<tr><td>CPU ID</td><td style="font-family:Consolas,monospace;font-size:12px;">BFEBFBFF000B06A2</td><td><span class="tag tag-green">★★★★☆</span></td></tr>
<tr><td>设备指纹(SHA-256)</td><td style="font-family:Consolas,monospace;font-size:11px;word-break:break-all;">0712a0d153af628a68b972c6f878285ab094bbf97e64634332d4aa6b65292acc</td><td><span class="tag tag-blue">衍生值</span></td></tr>
</table>
<p style="font-size:12px;color:var(--text3);margin-top:8px;">测试设备: XIAOMI TM2413 / i5-13500H / 32GB / Windows 11 — 4/4 核心信号全部可用</p>
</div>
</div>
<!-- ═══════════════ S4: 设备指纹 + WorkBuddy 绑定 ═══════════════ -->
<div class="section" id="s4">
<div class="section-title"><span class="ico">🔗</span> 四、设备指纹 + WorkBuddy 实例绑定</div>
<div class="card">
<p style="font-size:13px;color:var(--text2);margin-bottom:12px;">
单靠硬件指纹不够——两台同型号批次的电脑可能有相同硬件信号。引入 WorkBuddy 实例 ID(软件层第二因子),双因子组合确保唯一性。
</p>
<div class="code-block"><span class="kw">class</span> <span class="fn">WorkBuddyBinding</span>:
<span class="cm">"""将设备指纹与 WorkBuddy 实例绑定 — 双因子防复制"""</span>
CONFIG_PATH = os.path.expanduser(<span class="str">'~/.workbuddy/config.json'</span>)
<span class="kw">@classmethod</span>
<span class="kw">def</span> <span class="fn">get_or_create_instance_id</span>(cls) -> str:
<span class="cm">"""获取或创建 WorkBuddy 实例 ID(首次安装生成,永不变化)"""</span>
config = {}
<span class="kw">if</span> os.path.exists(cls.CONFIG_PATH):
<span class="kw">with</span> open(cls.CONFIG_PATH) <span class="kw">as</span> f:
config = json.load(f)
<span class="kw">if</span> <span class="str">'instance_id'</span> not <span class="kw">in</span> config:
config[<span class="str">'instance_id'</span>] = str(uuid.uuid4()) <span class="cm"># 首次生成</span>
os.makedirs(os.path.dirname(cls.CONFIG_PATH), exist_ok=<span class="kw">True</span>)
<span class="kw">with</span> open(cls.CONFIG_PATH, <span class="str">'w'</span>) <span class="kw">as</span> f:
json.dump(config, f, indent=<span class="num">2</span>)
<span class="kw">return</span> config[<span class="str">'instance_id'</span>]
<span class="kw">@classmethod</span>
<span class="kw">def</span> <span class="fn">generate_binding_key</span>(cls) -> str:
<span class="cm">"""
双因子绑定密钥 = HMAC-SHA256(设备指纹, 实例ID)
- 硬件不同 → 指纹不同 → 密钥不同
- 实例不同 → ID 不同 → 密钥不同
"""</span>
fingerprint = DeviceFingerprint.generate()
instance_id = cls.get_or_create_instance_id()
<span class="kw">return</span> hmac.new(
fingerprint.encode(), instance_id.encode(), hashlib.sha256
).hexdigest()
<span class="kw">@classmethod</span>
<span class="kw">def</span> <span class="fn">get_binding_proof</span>(cls) -> dict:
<span class="cm">"""返回完整绑定证明,用于 OAuth 请求"""</span>
<span class="kw">return</span> {
<span class="str">'device_fingerprint'</span>: DeviceFingerprint.generate(),
<span class="str">'wb_instance_id'</span>: cls.get_or_create_instance_id(),
<span class="str">'binding_key'</span>: cls.generate_binding_key(),
}</div>
<div class="callout callout-info">
<b>双因子原理:</b>两台完全相同的硬件(如公司采购的同一批次笔记本),设备指纹可能相同。但每台机器上 WorkBuddy 安装时生成的实例 ID 不同 → HMAC 结果不同 → 绑定密钥不同 → 无法冒充。
</div>
</div>
</div>
<!-- ═══════════════ S5: OS 安全存储 ═══════════════ -->
<div class="section" id="s5">
<div class="section-title"><span class="ico">🔒</span> 五、OS 原生安全存储</div>
<div class="card">
<p style="font-size:13px;color:var(--text2);margin-bottom:12px;">
绑定密钥不能明文存储在文件里(可被复制)。必须存入操作系统原生加密区:Windows DPAPI / macOS Keychain / Linux Secret Service。
</p>
<div class="code-block"><span class="kw">class</span> <span class="fn">SecureStorage</span>:
<span class="cm">"""OS 原生安全存储 — 密钥绑定到当前用户+当前机器"""</span>
<span class="kw">@staticmethod</span>
<span class="kw">def</span> <span class="fn">store</span>(key_name: str, value: str):
system = platform.system().lower()
<span class="kw">if</span> system == <span class="str">'windows'</span>:
<span class="cm"># DPAPI — 绑定当前 Windows 用户 SID + 机器密钥</span>
<span class="cm"># 同一用户同一台机器才能解密,复制文件到其他机器无效</span>
script = (
<span class="str">f'$val = ConvertTo-SecureString "{value}" '</span>
<span class="str">f'-AsPlainText -Force; '
f'ConvertFrom-SecureString $val | '</span>
<span class="str">f'Set-Content "$env:USERPROFILE\\.workbuddy\\.device_key"'</span>
)
subprocess.run([<span class="str">"powershell"</span>, <span class="str">"-Command"</span>, script],
check=<span class="kw">True</span>, capture_output=<span class="kw">True</span>)
<span class="kw">elif</span> system == <span class="str">'darwin'</span>:
<span class="cm"># macOS Keychain — Apple Silicon 上使用 Secure Enclave</span>
subprocess.run([<span class="str">"security"</span>, <span class="str">"add-generic-password"</span>,
<span class="str">"-a"</span>, os.getenv(<span class="str">'USER'</span>, <span class="str">'workbuddy'</span>),
<span class="str">"-s"</span>, key_name, <span class="str">"-w"</span>, value, <span class="str">"-U"</span>], check=<span class="kw">True</span>)
<span class="kw">elif</span> system == <span class="str">'linux'</span>:
<span class="cm"># Secret Service — GNOME Keyring / KWallet</span>
<span class="kw">try</span>:
subprocess.run([<span class="str">"secret-tool"</span>, <span class="str">"store"</span>,
<span class="str">"--label"</span>, key_name,
<span class="str">"service"</span>, <span class="str">"workbuddy"</span>,
<span class="str">"account"</span>, key_name],
input=value, text=<span class="kw">True</span>, check=<span class="kw">True</span>)
<span class="kw">except</span> FileNotFoundError:
<span class="cm"># 降级:文件 + 0600 权限(安全性不如原生)</span>
path = os.path.expanduser(<span class="str">'~/.workbuddy/.device_key'</span>)
<span class="kw">with</span> open(path, <span class="str">'w'</span>) <span class="kw">as</span> f: f.write(value)
os.chmod(path, <span class="num">0o600</span>)
<span class="kw">@staticmethod</span>
<span class="kw">def</span> <span class="fn">retrieve</span>(key_name: str) -> str:
system = platform.system().lower()
<span class="kw">if</span> system == <span class="str">'windows'</span>:
<span class="cm"># 解密:只有同一用户同一台机器才能成功</span>
script = (
<span class="str">f'$enc = Get-Content "$env:USERPROFILE\\.workbuddy\\.device_key" '
f'| ConvertTo-SecureString; '</span>
<span class="str">'[Runtime.InteropServices.Marshal]::PtrToStringAuto('
'[Runtime.InteropServices.Marshal]::SecureStringToBSTR($enc))'</span>
)
r = subprocess.run([<span class="str">"powershell"</span>, <span class="str">"-Command"</span>, script],
capture_output=<span class="kw">True</span>, text=<span class="kw">True</span>)
<span class="kw">return</span> r.stdout.strip()
<span class="kw">elif</span> system == <span class="str">'darwin'</span>:
r = subprocess.run([<span class="str">"security"</span>, <span class="str">"find-generic-password"</span>,
<span class="str">"-a"</span>, os.getenv(<span class="str">'USER'</span>, <span class="str">'workbuddy'</span>),
<span class="str">"-s"</span>, key_name, <span class="str">"-w"</span>],
capture_output=<span class="kw">True</span>, text=<span class="kw">True</span>)
<span class="kw">return</span> r.stdout.strip() <span class="kw">if</span> r.returncode == <span class="num">0</span> <span class="kw">else</span> <span class="kw">None</span>
<span class="kw">elif</span> system == <span class="str">'linux'</span>:
<span class="kw">try</span>:
r = subprocess.run([<span class="str">"secret-tool"</span>, <span class="str">"lookup"</span>,
<span class="str">"service"</span>, <span class="str">"workbuddy"</span>,
<span class="str">"account"</span>, key_name],
capture_output=<span class="kw">True</span>, text=<span class="kw">True</span>)
<span class="kw">return</span> r.stdout.strip()
<span class="kw">except</span> Exception:
path = os.path.expanduser(<span class="str">'~/.workbuddy/.device_key'</span>)
<span class="kw">return</span> open(path).read().strip() <span class="kw">if</span> os.path.exists(path) <span class="kw">else</span> <span class="kw">None</span></div>
<table class="info-table" style="margin-top:16px;">
<tr><th>平台</th><th>机制</th><th>复制到其他机器可用?</th><th>原因</th></tr>
<tr><td>Windows</td><td>DPAPI</td><td><span class="tag tag-red">✗ 不可用</span></td><td>加密时绑定当前用户 SID + 机器密钥</td></tr>
<tr><td>macOS</td><td>Keychain</td><td><span class="tag tag-red">✗ 不可用</span></td><td>密钥受登录密码保护,Apple Silicon 走 Secure Enclave</td></tr>
<tr><td>Linux</td><td>Secret Service</td><td><span class="tag tag-red">✗ 不可用</span></td><td>密钥环用用户登录密码加密</td></tr>
</table>
</div>
</div>
<!-- ═══════════════ S6: OAuth 客户端集成 ═══════════════ -->
<div class="section" id="s6">
<div class="section-title"><span class="ico">🔐</span> 六、OAuth 客户端集成</div>
<div class="card">
<p style="font-size:13px;color:var(--text2);margin-bottom:12px;">
OAuth 授权时在 Token 交换请求和每次 MCP 调用中注入设备绑定信息。
</p>
<div class="code-block"><span class="kw">class</span> <span class="fn">OAuthClient</span>:
<span class="cm">"""
OAuth 2.0 授权码流程 + 设备绑定
在标准 PKCE 基础上增加 device_fingerprint + binding_key
"""</span>
<span class="kw">@staticmethod</span>
<span class="kw">def</span> <span class="fn">build_authorization_url</span>() -> str:
<span class="cm">"""构建授权页面 URL(含 PKCE challenge)"""</span>
code_verifier = secrets.token_urlsafe(<span class="num">32</span>)
code_challenge = base64url(hashlib.sha256(
code_verifier.encode()
).digest())
<span class="cm"># 将 code_verifier 存入内存(不落盘),回调时使用</span>
SecureStorage.store(<span class="str">'oauth_code_verifier'</span>, code_verifier)
params = {
<span class="str">'response_type'</span>: <span class="str">'code'</span>,
<span class="str">'client_id'</span>: CLIENT_ID,
<span class="str">'redirect_uri'</span>: REDIRECT_URI,
<span class="str">'code_challenge'</span>: code_challenge,
<span class="str">'code_challenge_method'</span>: <span class="str">'S256'</span>,
<span class="str">'scope'</span>: <span class="str">'mcp:tools mcp:resources'</span>,
}
<span class="kw">return</span> AUTHORIZE_ENDPOINT + <span class="str">'?'</span> + urllib.parse.urlencode(params)
<span class="kw">@staticmethod</span>
<span class="kw">def</span> <span class="fn">exchange_code_for_token</span>(auth_code: str) -> dict:
<span class="cm">"""用 code 换 Token,携带设备绑定信息"""</span>
code_verifier = SecureStorage.retrieve(<span class="str">'oauth_code_verifier'</span>)
proof = WorkBuddyBinding.get_binding_proof()
response = requests.post(TOKEN_ENDPOINT, data={
<span class="str">'grant_type'</span>: <span class="str">'authorization_code'</span>,
<span class="str">'code'</span>: auth_code,
<span class="str">'code_verifier'</span>: code_verifier,
<span class="str">'client_id'</span>: CLIENT_ID,
<span class="str">'redirect_uri'</span>: REDIRECT_URI,
<span class="cm"># ↓ 设备绑定信息 ↓</span>
<span class="str">'device_fingerprint'</span>: proof[<span class="str">'device_fingerprint'</span>],
<span class="str">'wb_instance_id'</span>: proof[<span class="str">'wb_instance_id'</span>],
<span class="str">'binding_key'</span>: proof[<span class="str">'binding_key'</span>],
})
token_data = response.json()
<span class="cm"># 将 Token 存入安全区(不是明文文件!)</span>
SecureStorage.store(<span class="str">'mcp_access_token'</span>, token_data[<span class="str">'access_token'</span>])
SecureStorage.store(<span class="str">'mcp_refresh_token'</span>, token_data[<span class="str">'refresh_token'</span>])
<span class="kw">return</span> token_data
<span class="kw">@staticmethod</span>
<span class="kw">def</span> <span class="fn">call_mcp_tool</span>(tool_name: str, params: dict) -> dict:
<span class="cm">"""每次 MCP 调用携带设备绑定头"""</span>
token = SecureStorage.retrieve(<span class="str">'mcp_access_token'</span>)
proof = WorkBuddyBinding.get_binding_proof()
headers = {
<span class="str">'Authorization'</span>: <span class="str">f'Bearer {token}'</span>,
<span class="str">'X-Device-Fingerprint'</span>: proof[<span class="str">'device_fingerprint'</span>],
<span class="str">'X-Binding-Key'</span>: proof[<span class="str">'binding_key'</span>],
<span class="str">'Content-Type'</span>: <span class="str">'application/json'</span>,
}
<span class="kw">return</span> requests.post(
<span class="str">f'{MCP_BASE_URL}/tools/call'</span>,
json={<span class="str">'name'</span>: tool_name, <span class="str">'arguments'</span>: params},
headers=headers
).json()</div>
<div class="callout callout-green">
<b>PKCE (RFC 7636) 防护:</b><span class="code-inline">code_verifier</span> 仅存于发起授权的机器内存中。即使另一台机器拦截到 <span class="code-inline">authorization_code</span>,没有 <span class="code-inline">code_verifier</span> 也无法换取 Token。
</div>
</div>
</div>
<!-- ═══════════════ S7: MCP 服务端校验 ═══════════════ -->
<div class="section" id="s7">
<div class="section-title"><span class="ico">🛡️</span> 七、MCP 服务端校验中间件</div>
<div class="card">
<p style="font-size:13px;color:var(--text2);margin-bottom:12px;">
服务端在签发 JWT 时将设备绑定写入声明,每次 MCP 请求时校验三层:签名 → 过期 → 设备绑定。
</p>
<div class="code-block"><span class="cm"># ── 授权服务器:签发绑定了设备的 JWT ──</span>
<span class="kw">def</span> <span class="fn">issue_token</span>(user_id: str, token_request: dict) -> str:
payload = {
<span class="str">'iss'</span>: <span class="str">'mcp-auth-server'</span>,
<span class="str">'sub'</span>: user_id,
<span class="str">'iat'</span>: int(time.time()),
<span class="str">'exp'</span>: int(time.time()) + <span class="num">900</span>, <span class="cm"># 15 分钟短效</span>
<span class="str">'cnf'</span>: { <span class="cm"># confirmation claim</span>
<span class="str">'device_fp'</span>: token_request[<span class="str">'device_fingerprint'</span>],
<span class="str">'wb_instance'</span>: token_request[<span class="str">'wb_instance_id'</span>],
<span class="str">'binding_key'</span>: token_request[<span class="str">'binding_key'</span>],
}
}
<span class="kw">return</span> jwt.encode(payload, PRIVATE_KEY, algorithm=<span class="str">'RS256'</span>)
<span class="cm"># ── MCP 网关:设备绑定校验中间件 ──</span>
<span class="kw">from</span> functools <span class="kw">import</span> wraps
<span class="kw">def</span> <span class="fn">verify_device_binding</span>(handler):
<span class="kw">@wraps</span>(handler)
<span class="kw">def</span> <span class="fn">wrapper</span>(request, *args, **kwargs):
token = extract_bearer_token(request.headers)
payload = jwt.decode(token, PUBLIC_KEY, algorithms=[<span class="str">'RS256'</span>])
cnf = payload.get(<span class="str">'cnf'</span>, {})
token_fp = cnf.get(<span class="str">'device_fp'</span>)
token_bk = cnf.get(<span class="str">'binding_key'</span>)
request_fp = request.headers.get(<span class="str">'X-Device-Fingerprint'</span>)
request_bk = request.headers.get(<span class="str">'X-Binding-Key'</span>)
<span class="cm"># 校验 1:设备指纹</span>
<span class="kw">if</span> not token_fp <span class="kw">or</span> token_fp != request_fp:
revoke_token(token) <span class="cm"># 立即吊销可疑 Token</span>
log_alert(<span class="str">f"device_fp mismatch for user {payload['sub']}"</span>)
<span class="kw">return</span> error(<span class="num">403</span>, <span class="str">'Device fingerprint mismatch — token revoked'</span>)
<span class="cm"># 校验 2:绑定密钥(硬件+软件双因子)</span>
<span class="kw">if</span> not token_bk <span class="kw">or</span> token_bk != request_bk:
revoke_token(token)
log_alert(<span class="str">f"binding_key mismatch for user {payload['sub']}"</span>)
<span class="kw">return</span> error(<span class="num">403</span>, <span class="str">'Binding key mismatch — token revoked'</span>)
<span class="kw">return</span> handler(request, *args, **kwargs)
<span class="kw">return</span> wrapper
<span class="cm"># ── 使用示例:将中间件挂载到 MCP 路由 ──</span>
<span class="kw">@app</span>.route(<span class="str">'/tools/call'</span>, methods=[<span class="str">'POST'</span>])
<span class="kw">@verify_device_binding</span> <span class="cm"># ← 设备绑定校验</span>
<span class="kw">def</span> <span class="fn">handle_tool_call</span>(request):
payload = request.json
result = execute_mcp_tool(payload[<span class="str">'name'</span>], payload[<span class="str">'arguments'</span>])
<span class="kw">return</span> {<span class="str">'status'</span>: <span class="str">'ok'</span>, <span class="str">'result'</span>: result}</div>
<div class="callout callout-danger">
<b>关键设计:</b>校验失败立即 <b>吊销 Token</b> 并告警 —— 不仅拒绝本次请求,还要让被复制的 Token 彻底失效,防止攻击者反复尝试。
</div>
</div>
</div>
<!-- ═══════════════ S8: 攻击场景 ═══════════════ -->
<div class="section" id="s8">
<div class="section-title"><span class="ico">🛑</span> 八、攻击场景与防护验证</div>
<div class="card">
<table class="info-table">
<tr><th>攻击者做了什么</th><th>结果</th><th>失败原因</th></tr>
<tr><td>只复制 <span class="code-inline">oauth_tokens/</span> 目录</td><td><span class="tag tag-red">403 拒绝</span></td><td>请求头中的设备指纹 ≠ JWT 中绑定的指纹</td></tr>
<tr><td>复制 <span class="code-inline">oauth_tokens/</span> + <span class="code-inline">config.json</span></td><td><span class="tag tag-red">403 拒绝</span></td><td>硬件不同 → 指纹不同 → 绑定密钥不同</td></tr>
<tr><td>复制全部文件 + 伪造设备指纹</td><td><span class="tag tag-red">403 拒绝</span></td><td>不知道原始硬件信号值,无法生成相同哈希</td></tr>
<tr><td>同一台机器复制到另一个 WorkBuddy 安装</td><td><span class="tag tag-red">403 拒绝</span></td><td>实例 ID 不同 → 绑定密钥不同</td></tr>
<tr><td>同一台机器 + 同一 WorkBuddy 安装</td><td><span class="tag tag-green">通过</span></td><td>本质是同一用户在同一机器使用(非攻击场景)</td></tr>
<tr><td>DPAPI 加密文件复制到其他机器</td><td><span class="tag tag-red">解密失败</span></td><td>DPAPI 绑定当前用户 SID + 机器密钥,异机无法解密</td></tr>
<tr><td>拦截 <span class="code-inline">authorization_code</span> 后伪造请求</td><td><span class="tag tag-red">换 Token 失败</span></td><td>缺少 PKCE <span class="code-inline">code_verifier</span>(仅存于原始机器安全区)</td></tr>
</table>
<div class="callout callout-warn" style="margin-top:16px;">
<b>唯一无法防护的场景:</b>攻击者有物理访问权限,在同一台机器上登录同一 Windows 用户账户 → 此时 DPAPI 可解密、设备指纹相同 → 这是操作系统级别的问题,超出应用层防护范围。
</div>
</div>
</div>
<!-- ═══════════════ S9: 方案对比 ═══════════════ -->
<div class="section" id="s9">
<div class="section-title"><span class="ico">📊</span> 九、方案对比与推荐组合</div>
<div class="card">
<table class="info-table">
<tr><th>方案</th><th>防复制强度</th><th>实现复杂度</th><th>标准化</th><th>推荐度</th></tr>
<tr><td>短效 Token + Refresh 轮转</td><td><span class="tag tag-orange">★★☆</span></td><td>低</td><td>OAuth 2.0 标准</td><td>辅助</td></tr>
<tr><td>PKCE</td><td><span class="tag tag-orange">★★☆</span></td><td>低</td><td>RFC 7636</td><td>标配</td></tr>
<tr><td><b>设备指纹绑定</b></td><td><b><span class="tag tag-green">★★★</span></b></td><td><b>中</b></td><td>自定义扩展</td><td><b>⭐ 核心</b></td></tr>
<tr><td>DPoP (RFC 9449)</td><td><span class="tag tag-green">★★★★</span></td><td>高</td><td>RFC 9449</td><td>进阶</td></tr>
<tr><td>mTLS (RFC 8705)</td><td><span class="tag tag-green">★★★★</span></td><td>高</td><td>RFC 8705</td><td>企业级</td></tr>
<tr><td>Introspection + 风控</td><td><span class="tag tag-green">★★★</span></td><td>中</td><td>RFC 7662</td><td>辅助</td></tr>
</table>
<div class="callout callout-green" style="margin-top:16px;">
<b>推荐组合(三层纵深防御):</b><br>
<span class="tag tag-blue">PKCE</span>(防授权码劫持)+ <span class="tag tag-green">设备指纹绑定</span>(防 Token 跨机复制)+ <span class="tag tag-orange">短效 Token + Refresh 轮转</span>(缩小攻击窗口)= 覆盖全链路的纵深防护体系
</div>
</div>
</div>
<!-- ═══════════════ FOOTER ═══════════════ -->
<div class="footer">
WorkBuddy Skills MCP OAuth 鉴权与设备指纹绑定方案 &copy; 2026 | 基于 Python 3.13 + JWT RS256 + DPAPI/Keychain 实现
</div>
<!-- ═══════════════ Tab Switch JS ═══════════════ -->
<script>
function switchTab(event, panelId) {
const container = event.target.closest('.card');
// Deactivate all tabs in this card
container.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
container.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
// Activate clicked tab
event.target.classList.add('active');
document.getElementById(panelId).classList.add('active');
}
</script>
</body>
</html>