1071 lines
61 KiB
HTML
1071 lines
61 KiB
HTML
<!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">登录 & 同意授权</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 <jwt_token></text>
|
|||
|
|
<text x="678" y="168" fill="#475569" font-size="12">X-Device-Fingerprint: <current_fp></text>
|
|||
|
|
<text x="678" y="192" fill="#475569" font-size="12">X-Binding-Key: <current_bk></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 鉴权与设备指纹绑定方案 © 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>
|