<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>ZapcoMan</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>http://example.com/</id>
  <link href="http://example.com/" rel="alternate"/>
  <link href="http://example.com/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, ZapcoMan</rights>
  <title>ZapcoMan's world</title>
  <updated>2026-08-14T16:19:54.139Z</updated>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="网络安全" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="社会工程学" scheme="http://example.com/tags/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="漏洞利用" scheme="http://example.com/tags/%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8/"/>
    <content>
      <![CDATA[<blockquote><p>一款月活近9亿的国民级电商App，被曝利用50+个Android系统漏洞提权、窃取隐私、监控竞品、阻止卸载。这是移动安全史上最令人震惊的”主流App恶意化”事件。</p></blockquote><hr><h2 id="事件概览"><a href="#事件概览" class="headerlink" title="事件概览"></a>事件概览</h2><p><strong>2023年3月</strong>，中国电商巨头拼多多（PDD Holdings旗下）的Android应用被Google从Play Store紧急下架，标记为”有害应用”。随后，卡巴斯基实验室、Lookout、WithSecure等多家顶级安全机构独立确认：</p><blockquote><p><strong>拼多多部分版本包含精心设计的恶意代码，利用Android系统漏洞进行权限提升、后门安装和数据窃取。</strong></p></blockquote><p>这不是某个黑客组织的杰作，而是一家纳斯达克上市公司（NASDAQ: PDD）的<strong>官方产品行为</strong>。</p><table><thead><tr><th>关键数据</th><th>详情</th></tr></thead><tbody><tr><td>受影响版本</td><td>v6.49.0及以下（通过中国第三方应用商店分发）</td></tr><tr><td>利用漏洞数</td><td>约 <strong>50个</strong> Android系统级漏洞</td></tr><tr><td>核心漏洞</td><td><strong>CVE-2023-20963</strong>（CVSS 7.8 高危）</td></tr><tr><td>恶意模块</td><td>超过 <strong>30个</strong> DEX文件</td></tr><tr><td>权限请求</td><td>高达 <strong>83项</strong> 敏感权限</td></tr><tr><td>安全检测标识</td><td><code>HEUR:Backdoor.AndroidOS.Pinduo.a</code></td></tr><tr><td>潜在受害用户</td><td><strong>数亿级别</strong>（中国Android用户占比约75%）</td></tr></tbody></table><hr><h2 id="时间线：从秘密组建到全球曝光"><a href="#时间线：从秘密组建到全球曝光" class="headerlink" title="时间线：从秘密组建到全球曝光"></a>时间线：从秘密组建到全球曝光</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">2020 ────── 2022.11 ──── 2023.02 ──── 2023.03 ──── 2023.04+</span><br><span class="line"> │           │            │            │            │</span><br><span class="line"> ▼           ▼            ▼            ▼            ▼</span><br><span class="line">成立百人   Project Zero  DarkNavy     Google下架   CNN深度</span><br><span class="line">漏洞团队   警告三星漏洞  发布报告      卡巴斯基确认  法律诉讼</span><br><span class="line">                        BreachForums  团队解散      CISA列入KEV</span><br><span class="line">                        确认身份</span><br></pre></td></tr></table></figure><h3 id="📅-详细时间轴"><a href="#📅-详细时间轴" class="headerlink" title="📅 详细时间轴"></a>📅 详细时间轴</h3><h4 id="第一幕：暗处布局（2020-2022）"><a href="#第一幕：暗处布局（2020-2022）" class="headerlink" title="第一幕：暗处布局（2020-2022）"></a>第一幕：暗处布局（2020-2022）</h4><p><strong>2020年 — 百人团队的诞生</strong></p><p>据CNN采访的多位拼多多现任&#x2F;前任员工透露，拼多多母公司PDD Holdings在这一年做出了一个惊人的决定：</p><ul><li>组建约 <strong>100名工程师和产品经理</strong> 的专项团队</li><li><strong>唯一任务</strong>：挖掘Android手机系统漏洞，开发exploit，”将其转化为利润”</li><li>团队运作高度保密，内部代号为特定项目名</li><li><strong>战略选择</strong>：初期只针对<strong>农村和小城镇用户</strong>，刻意避开北京、上海等一线城市的安全研究人员</li><li>目标：通过收集海量用户活动数据 → 构建精准画像 → 优化推荐算法 → 提升转化率</li></ul><blockquote><p>“目标是为了降低被暴露的风险。” —— 匿名拼多多员工，CNN采访</p></blockquote><p><strong>2022年11月 — Google Project Zero的预警</strong></p><p>Google旗下的顶尖安全团队Project Zero发布报告，警告针对三星手机的复杂攻击链：</p><ul><li>串联 <strong>3个安全漏洞</strong>（三星已于2021年3月修复）</li><li>攻击者可读写设备上的任意文件</li><li>归因于”商业监控供应商”，<strong>未点名具体App</strong></li><li>这为后来的拼多多事件埋下了伏笔</li></ul><h4 id="第二幕：曝光与确认（2023年2-3月）"><a href="#第二幕：曝光与确认（2023年2-3月）" class="headerlink" title="第二幕：曝光与确认（2023年2-3月）"></a>第二幕：曝光与确认（2023年2-3月）</h4><p><strong>2023年2月28日 — 深蓝洞察(DarkNavy)引爆炸弹</strong></p><p>中国安全研究机构DarkNavy发布年度重磅报告：</p><blockquote><p><strong>《”深蓝洞察”2022年度最”不可赦”漏洞》</strong></p></blockquote><p>报告披露了一家”大型互联网公司”的App存在以下恶意行为（报告中多处代码截图被打码处理）：</p><ol><li>✅ 利用各Android OEM厂商系统的 <strong>0Day漏洞</strong> 进行提权</li><li>✅ 窃取用户隐私数据</li><li>✅ 攻击竞争对手App</li><li>✅ 使应用<strong>无法被用户卸载</strong></li><li>✅ 虚假提升日活&#x2F;月活数据（静默安装、后台运行）</li></ol><p>报告发布后迅速在中文安全技术社区引发震动。</p><p><strong>2023年3月3日 — 黑客论坛”点名”</strong></p><p>已关闭的黑客论坛BreachForums上，有用户将DarkNavy报告中的打码代码与公开可下载的APK进行对比，<strong>确认涉事App就是拼多多</strong>。</p><p><strong>2023年3月4日 — 微博爆料（后被删）</strong></p><p>电商专家刘华芳在微博公开发文，指认拼多多利用安全漏洞获取市场份额、窃取竞品数据。该帖子<strong>很快被删除</strong>。</p><p><strong>2023年3月5日 — 紧急”消毒”</strong></p><p>拼多多发布 <strong>v6.50.0版本更新</strong>，移除了所有漏洞利用代码。</p><p>但安全专家后来指出：<strong>底层框架代码仍在，可随时重新激活攻击能力</strong>。</p><p><strong>2023年3月7日 — 团队解散与清洗</strong></p><p>在v6.50.0发布两天后：</p><ul><li>百人漏洞挖掘团队被<strong>正式解散</strong></li><li>团队成员发现：<ul><li>内部通讯工具 <strong>Knock被锁定</strong></li><li>失去访问内网文件的权限</li><li>大数据&#x2F;数据表&#x2F;日志系统的访问权限被撤销</li></ul></li><li><strong>大部分成员被转移到Temu</strong>（拼多多2022年推出的跨境电商平台）</li><li>约20名核心网络安全工程师保留在拼多多</li></ul><p><strong>2023年3月20-21日 — Google重拳出击</strong></p><ul><li>Google从Play Store<strong>下架拼多多</strong></li><li>向用户发出警告：”该应用可能有害，可能导致对设备或数据的未经授权访问”</li><li>这是Google对主流App采取的最严厉措施之一</li></ul><p><strong>2023年3月28日 — 三方独立验证同日发布</strong></p><table><thead><tr><th>机构</th><th>发现</th></tr></thead><tbody><tr><td><strong>卡巴斯基实验室</strong></td><td>确认恶意代码存在，检测名为<code>HEUR:Backdoor.AndroidOS.Pinduo.a</code>；利用已知Android漏洞提权，下载执行额外恶意模块</td></tr><tr><td><strong>Lookout</strong></td><td>分析两个非Play版本（SHA256: <code>3c5a0eba...</code>和<code>234aa7a4...</code>），均含CVE-2023-20963利用代码；使用与Play版相同的签名密钥</td></tr></tbody></table><h4 id="第三幕：全球关注（2023年4月及以后）"><a href="#第三幕：全球关注（2023年4月及以后）" class="headerlink" title="第三幕：全球关注（2023年4月及以后）"></a>第三幕：全球关注（2023年4月及以后）</h4><p><strong>2023年4月2日 — CNN深度调查报道</strong></p><p>CNN发表独家调查，采访了：</p><ul><li><strong>6家网络安全团队</strong>（美国、欧洲、亚洲）</li><li><strong>多位拼多多现任&#x2F;前任员工</strong></li></ul><p>核心发现震惊全球安全界。</p><p><strong>2023年4月后 — 持续发酵</strong></p><ul><li><strong>CISA</strong>（美国网络安全和基础设施安全局）将CVE-2023-20963列入<strong>已知利用漏洞(KEV)目录</strong>，要求联邦机构在2023年5月4日前修复</li><li><strong>美国多州</strong>对Temu提起集体诉讼（详见<a href="#%E5%90%8E%E7%BB%AD%E5%BD%B1%E5%93%8D%E4%B8%8E%E6%B3%95%E5%BE%8B%E9%A3%8E%E6%9A%B4">后续影响</a>章节）</li><li><strong>芬兰WithSecure</strong>等机构持续跟踪分析</li></ul><hr><h2 id="技术深挖：EvilParcel攻击链如何工作"><a href="#技术深挖：EvilParcel攻击链如何工作" class="headerlink" title="技术深挖：EvilParcel攻击链如何工作"></a>技术深挖：EvilParcel攻击链如何工作</h2><h3 id="核心漏洞：CVE-2023-20963"><a href="#核心漏洞：CVE-2023-20963" class="headerlink" title="核心漏洞：CVE-2023-20963"></a>核心漏洞：CVE-2023-20963</h3><table><thead><tr><th>属性</th><th>值</th></tr></thead><tbody><tr><td>CVE编号</td><td>CVE-2023-20963</td></tr><tr><td>漏洞类型</td><td>Android Framework 权限提升(EoP)</td></tr><tr><td>CVSS评分</td><td><strong>7.8 &#x2F; 高危</strong></td></tr><tr><td>影响版本</td><td>Android 11 &#x2F; 12 &#x2F; 12L &#x2F; 13</td></tr><tr><td>利用条件</td><td><strong>无需用户交互</strong></td></tr><tr><td>Google公开日期</td><td>2023年3月6日</td></tr><tr><td>补丁推送</td><td>公开后约2周才到达终端用户</td></tr><tr><td>拼多多利用时间</td><td><strong>早于Google公开日期</strong>（零日利用）</td></tr></tbody></table><h3 id="EvilParcel攻击流程"><a href="#EvilParcel攻击流程" class="headerlink" title="EvilParcel攻击流程"></a>EvilParcel攻击流程</h3><p>Lookout安全公司的逆向工程师Christoph Hebeisen及其团队花费两天时间分析了拼多多APK样本，还原了完整的攻击链：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line">┌─────────────────────────────────────────────────────────┐</span><br><span class="line">│ 阶段1：入口                                              │</span><br><span class="line">│                                                          │</span><br><span class="line">│ 拼多多APP (v6.49.0及以下)                                │</span><br><span class="line">│ 通过华为/小米/OPPO等第三方应用商店安装                   │</span><br><span class="line">│ 与Google Play版使用相同数字签名密钥 ✓                    │</span><br><span class="line">└────────────────────────┬────────────────────────────────┘</span><br><span class="line">                         │</span><br><span class="line">                         ▼</span><br><span class="line">┌─────────────────────────────────────────────────────────┐</span><br><span class="line">│ 阶段2：漏洞触发                                          │</span><br><span class="line">│                                                          │</span><br><span class="line">│ EvilParcel Exploit                                      │</span><br><span class="line">│ ├─ 构造恶意Bundle对象                                    │</span><br><span class="line">│ │  └─ 利用Parcel序列化/反序列化不一致性                  │</span><br><span class="line">│ ├─ 隐藏Intent payload                                   │</span><br><span class="line">│ │  └─ 首次反序列化不可见，再次出现时激活                 │</span><br><span class="line">│ └─ 触发android.os.WorkSource类                           │</span><br><span class="line">│    └─ CVE-2023-20963                                    │</span><br><span class="line">│       ★ 无需任何用户交互 ★                               │</span><br><span class="line">└────────────────────────┬────────────────────────────────┘</span><br><span class="line">                         │</span><br><span class="line">                         ▼</span><br><span class="line">┌─────────────────────────────────────────────────────────┐</span><br><span class="line">│ 阶段3：权限提升                                          │</span><br><span class="line">│                                                          │</span><br><span class="line">│ LaunchAnywhere Attack                                   │</span><br><span class="line">│ 通过Account Chooser Activity触发                          │</span><br><span class="line">│ 获得 system 级权限                                      │</span><br><span class="line">│ 绕过 Android Sandbox 完全隔离                            │</span><br><span class="line">└────────────────────────┬────────────────────────────────┘</span><br><span class="line">                         │</span><br><span class="line">              ┌──────────┼──────────┐</span><br><span class="line">              ▼          ▼          ▼</span><br><span class="line">┌──────────────┐ ┌──────────┐ ┌──────────────┐</span><br><span class="line">│ 阶段4A：窃取  │ │阶段4B：  │ │ 阶段4C：持久化│</span><br><span class="line">│              │ │后门部署  │ │              │</span><br><span class="line">│ ·通知内容    │ │ ·从C2服务器│ │ ·阻止卸载    │</span><br><span class="line">│ ·用户文件    │ │ 下载DEX  │ │ ·后台常驻    │</span><br><span class="line">│ ·位置/Wi-Fi  │ │ ·动态执行│ │ ·自启动      │</span><br><span class="line">│ ·联系人/日历 │ │ (30+模块)│ │ ·虚增MAU     │</span><br><span class="line">│ ·相册        │ │          │ │              │</span><br><span class="line">└──────────────┘ └──────────┘ └──────────────┘</span><br></pre></td></tr></table></figure><h3 id="技术细节亮点"><a href="#技术细节亮点" class="headerlink" title="技术细节亮点"></a>技术细节亮点</h3><h4 id="1-Bundle-Feng-Shui（Bundle风水）"><a href="#1-Bundle-Feng-Shui（Bundle风水）" class="headerlink" title="1. Bundle Feng Shui（Bundle风水）"></a>1. Bundle Feng Shui（Bundle风水）</h4><p>DarkNavy报告中的术语，描述了一种精巧的序列化攻击：</p><ul><li>构造一个Bundle对象，使其<strong>序列化和反序列化的结果不同</strong></li><li>隐藏的Intent在首次反序列化时不可见</li><li>在第二次反序列化时”复活”</li><li>这种”走私”机制绕过了多项安全检查</li></ul><h4 id="2-LuciferStrategy字符串"><a href="#2-LuciferStrategy字符串" class="headerlink" title="2. LuciferStrategy字符串"></a>2. LuciferStrategy字符串</h4><p>Lookout在分析的样本中发现了与DarkNavy报告完全一致的字符串标识：<code>LuciferStrategy</code>——这直接证实了两者描述的是同一套攻击体系。</p><h4 id="3-多OEM适配"><a href="#3-多OEM适配" class="headerlink" title="3. 多OEM适配"></a>3. 多OEM适配</h4><p>Oversecured创始人Sergey Toshin的分析显示：</p><ul><li><strong>约50个漏洞</strong>中，大部分针对<strong>OEM定制代码</strong></li><li>主要目标厂商：<strong>三星、华为、小米、OPPO</strong></li><li>OEM代码审计频率远低于AOSP主线，因此更容易存在未修复漏洞</li><li>这意味着拼多多针对不同品牌手机开发了<strong>定制化的exploit包</strong></li></ul><h4 id="4-代码伪装技术"><a href="#4-代码伪装技术" class="headerlink" title="4. 代码伪装技术"></a>4. 代码伪装技术</h4><p>Check Point Research发现的反检测手段：</p><ul><li>将恶意组件隐藏在<strong>合法文件名下</strong>（如伪装成Google服务）</li><li>这是专业恶意软件开发者的常用手法</li><li>热更新机制可<strong>绕过应用商店审核</strong>，动态加载新功能</li></ul><h4 id="5-第二阶段Payload"><a href="#5-第二阶段Payload" class="headerlink" title="5. 第二阶段Payload"></a>5. 第二阶段Payload</h4><p>Lookout注意到一个关键细节：</p><ul><li>拼多多会从互联网<strong>下载第二阶段载荷</strong></li><li>该载荷<strong>未进行数字签名</strong></li><li>Lookout无法将其归因于PDD Holdings</li><li>这意味着攻击能力可以<strong>动态扩展</strong>，不仅限于预置的功能</li></ul><hr><h2 id="恶意行为全清单：你的手机经历了什么"><a href="#恶意行为全清单：你的手机经历了什么" class="headerlink" title="恶意行为全清单：你的手机经历了什么"></a>恶意行为全清单：你的手机经历了什么</h2><p>基于CNN联合6家安全团队的综合分析，以下是拼多多恶意代码的完整行为清单：</p><h3 id="🔓-隐私数据窃取"><a href="#🔓-隐私数据窃取" class="headerlink" title="🔓 隐私数据窃取"></a>🔓 隐私数据窃取</h3><table><thead><tr><th>数据类型</th><th>访问方式</th><th>用户知情？</th></tr></thead><tbody><tr><td>GPS定位信息</td><td>系统级直接读取</td><td>❌ 不需要权限弹窗</td></tr><tr><td>联系人列表</td><td>绕过通讯录权限</td><td>❌</td></tr><tr><td>日历事件</td><td>直接访问</td><td>❌</td></tr><tr><td>通知栏内容</td><td>监听NotificationService</td><td>❌</td></tr><tr><td>相册照片</td><td>读取存储分区</td><td>❌</td></tr><tr><td>Wi-Fi网络信息</td><td>获取SSID和BSSID</td><td>❌</td></tr><tr><td>社交媒体账号</td><td>凭证或Session劫持</td><td>❌</td></tr><tr><td>聊天记录</td><td>读取其他App数据库</td><td>❌</td></tr><tr><td>生物识别数据</td><td>访问指纹&#x2F;面部特征(部分报告)</td><td>❌</td></tr></tbody></table><h3 id="👁️-竞品监控"><a href="#👁️-竞品监控" class="headerlink" title="👁️ 竞品监控"></a>👁️ 竞品监控</h3><ul><li><strong>追踪用户在其他购物App的活动</strong>（淘宝、京东等）</li><li><strong>提取竞品的商品浏览记录和购买意向</strong></li><li>用于优化自身推荐算法和营销策略</li><li>违反了Apple和Google的应用商店政策</li></ul><h3 id="🛡️-系统控制"><a href="#🛡️-系统控制" class="headerlink" title="🛡️ 系统控制"></a>🛡️ 系统控制</h3><table><thead><tr><th>行为</th><th>描述</th><th>危害等级</th></tr></thead><tbody><tr><td>修改系统设置</td><td>更改壁纸、铃声等</td><td>⚠️ 中</td></tr><tr><td>静默安装应用</td><td>无通知下载安装其他App</td><td>🔴 高</td></tr><tr><td>阻止卸载</td><td>利用系统漏洞锁定自身</td><td>🔴 高</td></tr><tr><td>后台常驻进程</td><td>即使关闭也继续运行</td><td>🔴 高</td></tr><tr><td>开机自启动</td><td>注册系统级Receiver</td><td>⚠️ 中</td></tr></tbody></table><h3 id="📊-数据造假"><a href="#📊-数据造假" class="headerlink" title="📊 数据造假"></a>📊 数据造假</h3><ul><li><strong>虚假安装</strong>：在用户不知情的情况下安装到其他设备</li><li><strong>虚增DAU&#x2F;MAU</strong>：后台运行算作活跃用户</li><li><strong>刷量行为</strong>：自动化操作模拟真实用户行为</li></ul><hr><h2 id="百人团队：拼多多的”网络武器”计划"><a href="#百人团队：拼多多的”网络武器”计划" class="headerlink" title="百人团队：拼多多的”网络武器”计划"></a>百人团队：拼多多的”网络武器”计划</h2><h3 id="组织架构"><a href="#组织架构" class="headerlink" title="组织架构"></a>组织架构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">PDD Holdings (拼多多母公司)</span><br><span class="line">└── Android安全团队 (2020年组建, ~100人)</span><br><span class="line">    ├── 漏洞研究员 (~30人)</span><br><span class="line">    │   ├── OEM代码审计组</span><br><span class="line">    │   ├── AOSP漏洞挖掘组</span><br><span class="line">    │   └── 0Day储备管理组</span><br><span class="line">    ├── Exploit开发组 (~40人)</span><br><span class="line">    │   ├── Samsung适配组</span><br><span class="line">    │   ├── Huawei/Xiaomi/OPPO适配组</span><br><span class="line">    │   └─ 通用Exploit框架组</span><br><span class="line">    ├── 产品/运营接口人 (~15人)</span><br><span class="line">    │   └── 将能力转化为业务指标</span><br><span class="line">    └── 反检测/隐蔽组 (~15人)</span><br><span class="line">        ├── 代码混淆</span><br><span class="line">        ├── 签名管理</span><br><span class="line">        └── 热更新架构</span><br></pre></td></tr></table></figure><h3 id="运作模式"><a href="#运作模式" class="headerlink" title="运作模式"></a>运作模式</h3><h4 id="目标选择策略"><a href="#目标选择策略" class="headerlink" title="目标选择策略"></a>目标选择策略</h4><blockquote><p>初期只针对<strong>农村和小城镇用户</strong>，避开北京上海等大城市。</p><p>—— CNN引用匿名员工说法</p></blockquote><p>这种策略背后的逻辑：</p><ol><li><strong>低风险</strong>：小城镇用户较少安装安全软件，不易被发现</li><li><strong>高价值</strong>：下沉市场是拼多多的核心用户群，数据价值高</li><li><strong>低技术素养</strong>：即使出现异常行为，用户也不易察觉</li></ol><h4 id="商业化路径"><a href="#商业化路径" class="headerlink" title="商业化路径"></a>商业化路径</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">漏洞利用 → 数据采集 → 用户画像 → 算法优化 → 精准推送 → 转化率提升 → 收入增长</span><br></pre></td></tr></table></figure><p>据CNN报道，通过收集用户活动的全面数据，拼多多能够：</p><ul><li>创建用户习惯、兴趣和偏好的<strong>完整画像</strong></li><li>改进机器学习模型</li><li>提供<strong>更个性化的推送通知和广告</strong></li><li>吸引用户打开App并下单</li></ul><h3 id="团队解散后的去向"><a href="#团队解散后的去向" class="headerlink" title="团队解散后的去向"></a>团队解散后的去向</h3><table><thead><tr><th>去向</th><th>人数</th><th>新岗位</th></tr></thead><tbody><tr><td><strong>Temu（跨境版拼多多）</strong></td><td>~70人</td><td>营销部门、推送通知开发等</td></tr><tr><td><strong>拼多多留任</strong></td><td>~20人</td><td>核心网络安全工程师</td></tr><tr><td><strong>离职&#x2F;未知</strong></td><td>~10人</td><td>不详</td></tr></tbody></table><p><strong>关键风险</strong>：开发恶意软件的同批工程师现在负责Temu——这款2023年美国下载量第一的App。</p><hr><h2 id="各方反应与博弈"><a href="#各方反应与博弈" class="headerlink" title="各方反应与博弈"></a>各方反应与博弈</h2><h3 id="🔴-拼多多官方回应"><a href="#🔴-拼多多官方回应" class="headerlink" title="🔴 拼多多官方回应"></a>🔴 拼多多官方回应</h3><p><strong>声明要点</strong>：</p><blockquote><p>“我们强烈反对某些匿名研究员和Google非结论性回应关于拼多多APP是恶意的推测和指控。”<br>—— 拼多多发言人Kong Ho（回复TechCrunch）</p></blockquote><p><strong>其他说辞</strong>：</p><ul><li>Google Play称”当前版本不符合政策”，但<strong>未分享更多细节</strong></li><li>“同时被下架的有多个App”（暗示并非单独针对拼多多）</li><li><strong>拒绝CNN多次采访请求</strong></li></ul><h3 id="🔵-Google的立场"><a href="#🔵-Google的立场" class="headerlink" title="🔵 Google的立场"></a>🔵 Google的立场</h3><ul><li>3月21日下架拼多多，标记为**”有害应用(Harmful)”**</li><li>向用户发出警告：可能导致<strong>未经授权的设备&#x2F;数据访问</strong></li><li>未透露更多调查细节（通常做法）</li><li>此举在TikTok被美国议员质疑国家安全的大背景下发生</li></ul><h3 id="🟣-安全社区的共识"><a href="#🟣-安全社区的共识" class="headerlink" title="🟣 安全社区的共识"></a>🟣 安全社区的共识</h3><p><strong>一致结论</strong>（来自6+家独立安全机构）：</p><table><thead><tr><th>机构</th><th>核心观点</th></tr></thead><tbody><tr><td><strong>DarkNavy（深蓝洞察）</strong></td><td>首次曝光，提供详细技术证据</td></tr><tr><td><strong>Lookout</strong></td><td>独立验证，确认CVE-2023-20963利用</td></tr><tr><td><strong>卡巴斯基</strong></td><td>检测出后门程序，给出正式命名</td></tr><tr><td><strong>WithSecure</strong></td><td>“非常不寻常”，从未见主流App如此做</td></tr><tr><td><strong>Check Point Research</strong></td><td>发现反检测和热更新机制</td></tr><tr><td><strong>Oversecured</strong></td><td>“最危险的恶意软件”，50+漏洞利用</td></tr><tr><td><strong>Johannes Kepler大学</strong></td><td>请求83项权限，远超购物App需求</td></tr></tbody></table><h3 id="🟢-中国监管层"><a href="#🟢-中国监管层" class="headerlink" title="🟢 中国监管层"></a>🟢 中国监管层</h3><p><strong>工信部（MIIT）</strong>：</p><ul><li><strong>未主动发现</strong>此问题</li><li>被Trivium China科技政策专家Kendra Schaefer批评：<blockquote><p>“这会让工信部很尴尬，因为这是他们的工作。他们本应该检查拼多多，而他们没发现任何问题，这对监管机构来说很尴尬。”</p></blockquote></li></ul><p><strong>法律层面风险</strong>：</p><ul><li>违反《个人信息保护法》(2021)：禁止非法收集、处理、传输个人信息</li><li>禁止利用互联网相关安全漏洞</li><li>禁止从事危害网络安全的行为</li></ul><h3 id="🟡-公关操作（据多方报道）"><a href="#🟡-公关操作（据多方报道）" class="headerlink" title="🟡 公关操作（据多方报道）"></a>🟡 公关操作（据多方报道）</h3><p>蓝点网等媒体披露了拼多多事件后的公关动作：</p><ol><li>✅ 新版本删除恶意代码</li><li>⚠️ 出动公关团队<strong>要求删除网络报道</strong></li><li>⚠️ 在微信公众平台<strong>投诉发布相关内容的公众号</strong></li><li>🔴 即使公众号运营者不承认侵权、微信审核也不认为侵权，<strong>重复发起投诉</strong></li><li>🔴 试图诱导&#x2F;逼迫公众号所有者删除文章</li></ol><hr><h2 id="后续影响与法律风暴"><a href="#后续影响与法律风暴" class="headerlink" title="后续影响与法律风暴"></a>后续影响与法律风暴</h2><h3 id="法律诉讼进展"><a href="#法律诉讼进展" class="headerlink" title="法律诉讼进展"></a>法律诉讼进展</h3><p>截至2025年，美国多个州对Temu&#x2F;PDD Holdings提起集体诉讼：</p><h4 id="肯塔基州诉案（2025年7月）"><a href="#肯塔基州诉案（2025年7月）" class="headerlink" title="肯塔基州诉案（2025年7月）"></a>肯塔基州诉案（2025年7月）</h4><p><strong>案件编号</strong>：Civil Action (Kentucky Circuit Court)</p><p><strong>核心指控</strong>：</p><ul><li>Temu继承了拼多多的恶意代码传统</li><li>PDD将开发恶意软件的团队转移到Temu</li><li>持续侵犯用户隐私权</li><li>拼多多被VirusTotal等多个安全引擎<strong>持续标记为”恶意”</strong></li></ul><h4 id="阿肯色州诉案（2024年6月）"><a href="#阿肯色州诉案（2024年6月）" class="headerlink" title="阿肯色州诉案（2024年6月）"></a>阿肯色州诉案（2024年6月）</h4><p><strong>法院</strong>：Cleburne County Circuit Court</p><p><strong>关键论点</strong>（摘自起诉书原文）：</p><blockquote><p>“PDD招募并雇佣了一支由100名程序员组成的团队，用于查找和利用Android的OEM定制化漏洞……意图利用那些审计频率低于主线的漏洞（估计有超过50个此类漏洞被作为目标）。”</p></blockquote><blockquote><p>“开发出’主流App中发现的最危险恶意软件’的同一批软件工程师和产品经理，在Temu进入市场后的一年内就被调往Temu工作。”</p></blockquote><h3 id="CISA-KEV目录收录"><a href="#CISA-KEV目录收录" class="headerlink" title="CISA KEV目录收录"></a>CISA KEV目录收录</h3><p><strong>2023年4月</strong>，美国网络安全和基础设施安全局(CISA)将CVE-2023-20963加入**Known Exploited Vulnerabilities (KEV)**目录：</p><ul><li>要求<strong>联邦民事行政部门(FCEB)机构</strong>在<strong>2023年5月4日前</strong>完成修复</li><li>基于约束性操作指令(BOD 22-01)</li><li>虽然仅强制联邦机构，但建议<strong>所有组织</strong>优先修复</li></ul><h3 id="行业影响"><a href="#行业影响" class="headerlink" title="行业影响"></a>行业影响</h3><table><thead><tr><th>影响维度</th><th>具体表现</th></tr></thead><tbody><tr><td><strong>用户信任</strong></td><td>中国用户对国产App的数据安全信任度下降</td></tr><tr><td><strong>开发者生态</strong></td><td>引发对”成功是否可以不择手段”的讨论</td></tr><tr><td><strong>应用商店审核</strong></td><td>各大商店加强了对权限请求和native代码的审查</td></tr><tr><td><strong>OEM厂商安全</strong></td><td>三星&#x2F;小米&#x2F;OPPO等加速修补定制代码漏洞</td></tr><tr><td><strong>国际形象</strong></td><td>成为中国App数据安全的负面案例，被外媒广泛引用</td></tr><tr><td><strong>监管政策</strong></td><td>推动中国《 App违法违规收集使用个人信息认定方法》升级</td></tr></tbody></table><h3 id="Temu的连带风险"><a href="#Temu的连带风险" class="headerlink" title="Temu的连带风险"></a>Temu的连带风险</h3><p>由于大部分团队成员转入Temu，安全界普遍担忧：</p><ol><li><strong>人才延续</strong>：具备开发国家级恶意软件能力的工程师现在负责Temu</li><li><strong>文化延续</strong>：同一管理层可能默许类似手段</li><li><strong>代码复用</strong>：底层框架仍在，可快速重新激活</li><li><strong>市场转移</strong>：从中国市场转向全球市场（Temu主要市场在美国）</li></ol><blockquote><p>截至2023年，Temu是美国下载量最高的App之一，这意味着潜在的受影响用户范围从中国扩展到了全球。</p></blockquote><hr><h2 id="深层思考：这件事改变了什么"><a href="#深层思考：这件事改变了什么" class="headerlink" title="深层思考：这件事改变了什么"></a>深层思考：这件事改变了什么</h2><h3 id="1-“主流App恶意化”的先例"><a href="#1-“主流App恶意化”的先例" class="headerlink" title="1. “主流App恶意化”的先例"></a>1. “主流App恶意化”的先例</h3><p>在此之前，恶意软件通常来自：</p><ul><li>小型犯罪团伙</li><li>国家级APT组织</li><li>偶尔失控的广告SDK</li></ul><p>拼多多事件开创了一个<strong>危险的先例</strong>：<strong>大型商业公司</strong>在其主流产品中集成<strong>专业级别的漏洞利用代码</strong>。这打破了”正规公司不会做这种事”的假设。</p><h3 id="2-OEM安全的盲区"><a href="#2-OEM安全的盲区" class="headerlink" title="2. OEM安全的盲区"></a>2. OEM安全的盲区</h3><p>Android生态系统的一个结构性弱点被暴露：</p><ul><li><strong>AOSP主线</strong>：审计严格，漏洞修复快</li><li><strong>OEM定制代码</strong>：审计松散，漏洞存留时间长</li><li>拼多多正是利用了这个<strong>盲区</strong>，针对各厂商定制代码开发专用exploit</li></ul><p>这对整个Android生态链提出了警示：<strong>安全性不能只依赖Google的主线维护</strong>。</p><h3 id="3-数字签名信任危机"><a href="#3-数字签名信任危机" class="headerlink" title="3. 数字签名信任危机"></a>3. 数字签名信任危机</h3><p>一个令人不安的事实：</p><blockquote><p>恶意版本的拼多多APK与Google Play上的正版使用了<strong>相同的数字签名密钥</strong>。</p></blockquote><p>这意味着：</p><ul><li>用户无法通过签名判断版本是否安全</li><li>开发者可以在”干净”和”恶意”模式间<strong>自由切换</strong></li><li>应用商店的分发渠道验证形同虚设</li></ul><h3 id="4-监管与创新的张力"><a href="#4-监管与创新的张力" class="headerlink" title="4. 监管与创新的张力"></a>4. 监管与创新的张力</h3><p>拼多多的事件暴露了中国科技监管的一个困境：</p><ul><li><strong>法规完备</strong>：《个人信息保护法》《网络安全法》等法律框架已经建立</li><li><strong>执行滞后</strong>：如此大规模的恶意行为未被主动发现</li><li><strong>事后追责</strong>：依赖外部安全研究者曝光后才介入</li></ul><p>如何在鼓励创新和保护用户之间找到平衡，仍是未解之题。</p><h3 id="5-沉默的共谋？"><a href="#5-沉默的共谋？" class="headerlink" title="5. 沉默的共谋？"></a>5. 沉默的共谋？</h3><p>一些值得深思的问题：</p><ul><li><strong>2020-2023年间</strong>，为何没有安全软件检测到？</li><li>国内杀毒&#x2F;安全App（如腾讯手机管家、360手机卫士）为何沉默？</li><li>应用商店（华为&#x2F;小米&#x2F;OPPO等）的审核机制是否存在漏洞？</li><li>是否存在某种”本土保护”心态导致审查放松？</li></ul><p>这些问题没有公开答案，但值得每一个从业者反思。</p><hr><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>拼多多恶意代码事件不是一起普通的”App违规收集信息”案例。它是一次<strong>有组织、有预谋、规模化</strong>的网络攻击行动——只不过发起者是一家市值千亿美元的上市公司，目标是它自己的用户。</p><p>当我们在谈论”数据安全””隐私保护”这些概念时，这个事件提供了一个极端的注脚：<strong>最危险的威胁有时就藏在那些你每天打开、最信任的图标后面</strong>。</p><p>对于普通用户，这件事留下的实用建议：</p><ol><li><strong>优先从官方应用商店下载App</strong>（Google Play &#x2F; Apple App Store）</li><li><strong>定期检查App权限</strong>，对异常请求保持警惕</li><li><strong>及时更新系统和App</strong>（补丁可以修复已知漏洞）</li><li><strong>对”过度索取权限”的App说不</strong></li></ol><p>对于行业从业者，这件事提醒我们：</p><blockquote><p>技术能力本身是中性的，但使用技术的方式定义了你是建设者还是破坏者。当一家公司决定用50个系统漏洞来提升月活数据时，它已经越过了那条线。</p></blockquote><hr><h2 id="参考来源"><a href="#参考来源" class="headerlink" title="参考来源"></a>参考来源</h2><ol><li><a href="https://arstechnica.com/?p=1926914">Ars Technica - Android app executed 0-day exploit on millions of devices</a></li><li><a href="https://edition.cnn.com/2023/04/02/tech/china-pinduoduo-malware-cybersecurity-analysis-intl-hnk/index.html">CNN - Chinese shopping app spying on users with ‘most dangerous malware’</a></li><li><a href="https://candid.technology/pinduoduo-app-malware-unauthorised-access/">Candid Technology - Kaspersky confirms Pinduoduo malware</a></li><li><a href="https://www.techradar.com/news/the-pinduoduo-malware-executed-a-dangerous-zero-day-against-millions-of-android-devices">TechRadar - The Pinduoduo malware zero-day</a></li><li><a href="https://www.solidot.org/story?sid=74503">Solidot - Lookout确认拼多多的非Play版本包含恶意代码</a></li><li><a href="https://www.solidot.org/story?sid=74563">Solidot - 拼多多解散了在应用中加入恶意功能的团队</a></li><li><a href="https://www.computing.co.uk/news/4112203/cisa-adds-android-zero-day-kev-catalogue">Computing.co.uk - CISA adds Android zero-day to KEV catalogue</a></li><li><a href="https://www.ag.ky.gov/">Kentucky AG - Temu Complaint (2025)</a></li><li><a href="https://reason.com/wp-content/uploads/2024/06/2024-06-25-temu-complaint.pdf">Arkansas Circuit Court - Temu Complaint (2024)</a></li><li><a href="https://zh.wikipedia.org/wiki/%E6%8B%BC%E5%A4%9A%E5%A4%9A">Wikipedia - 拼多多&#x2F;恶意代码条目</a></li></ol><hr><p><em>本文基于公开资料整理编写，仅供技术研究与安全意识教育用途。</em></p><p><strong>最后更新</strong>：2026年8月14日</p><hr><p>© 2026 codervibe’s Blog. All rights reserved.</p>]]>
    </content>
    <id>http://example.com/2026/08/15/20260815/2026081501/</id>
    <link href="http://example.com/2026/08/15/20260815/2026081501/"/>
    <published>2026-08-14T16:02:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>一款月活近9亿的国民级电商App，被曝利用50+个Android系统漏洞提权、窃取隐私、监控竞品、阻止卸载。这是移动安全史上最令人震惊的”主流App恶意化”事件。</p>
</blockquote>
<hr>
<h2 id="事件概览"><a hr]]>
    </summary>
    <title>拼多多APP恶意代码事件：当9亿用户的手机成为&quot;肉鸡&quot;</title>
    <updated>2026-08-14T16:19:54.139Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="网络安全" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="社会工程学" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="社会工程学" scheme="http://example.com/tags/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="黑客组织" scheme="http://example.com/tags/%E9%BB%91%E5%AE%A2%E7%BB%84%E7%BB%87/"/>
    <content>
      <![CDATA[<blockquote><p>一提到黑客组织，往往就想到大规模、有计划、有组织的网络攻击。在深入了解这两个组织之前，我们必须先理解一个核心概念——<strong>APT</strong>。</p></blockquote><hr><h2 id="一、什么是-APT？"><a href="#一、什么是-APT？" class="headerlink" title="一、什么是 APT？"></a>一、什么是 APT？</h2><p><strong>APT</strong> 是 <strong>Advanced Persistent Threat</strong>（高级持续性威胁）的首字母缩写，也称为定向威胁攻击。这类攻击具有三个显著特征：</p><ul><li><strong>高级（Advanced）</strong>：使用复杂的攻击技术和定制化恶意软件，远超普通脚本小子的能力范围。</li><li><strong>持续（Persistent）</strong>：攻击者会在目标网络中长期潜伏，持续数月甚至数年，不断收集情报。</li><li><strong>威胁（Threat）</strong>：这类攻击往往不是个人或小众的自发行为，而是由规模较大的群体或组织有计划、有组织地统一发起。</li></ul><p>正因为如此级别的攻击背后往往有国家力量的支撑，黑客组织才逐渐浮出水面，成为国际安全博弈的焦点。</p><p>在众多 APT 组织中，俄罗斯的两个组织尤为引人注目——它们被称为**”双熊”**：<strong>舒适熊（Cozy Bear &#x2F; APT29）</strong> 和 <strong>奇幻熊（Fancy Bear &#x2F; APT28）</strong>。两者均被认为与俄罗斯情报机构存在关联，但分工不同、风格迥异——一个擅长隐蔽渗透，一个偏好高调出击。</p><hr><h2 id="二、舒适熊-Cozy-Bear（APT29）"><a href="#二、舒适熊-Cozy-Bear（APT29）" class="headerlink" title="二、舒适熊 Cozy Bear（APT29）"></a>二、舒适熊 Cozy Bear（APT29）</h2><h3 id="2-1-组织概况"><a href="#2-1-组织概况" class="headerlink" title="2.1 组织概况"></a>2.1 组织概况</h3><table><thead><tr><th>属性</th><th>信息</th></tr></thead><tbody><tr><td><strong>代号</strong></td><td>APT29</td></tr><tr><td><strong>别名</strong></td><td><strong>核心代号</strong>：Cozy Bear、APT29<br><strong>Duke 系列</strong>：The Dukes、CozyDuke、CozyCar、Dark Halo<br><strong>微软命名</strong>：NOBELIUM（Nobelium）、Midnight Blizzard（午夜暴风雪）<br><strong>其他别名</strong>：Office Monkeys、StellarParticle、UNC2452、Yttrium、Iron Hemlock、Minidionis、CloudLook、Group 100<br><strong>合并称呼</strong>：Grizzly Steppe（与 APT28 合称时）</td></tr><tr><td><strong>最早活动时间</strong></td><td>可追溯至 2008 年（部分报告称 2010 年起）</td></tr><tr><td><strong>关联机构</strong></td><td><strong>俄罗斯对外情报局（SVR）</strong>——荷兰 AIVD 和美国情报部门于 2014 年通过入侵其办公室监控摄像头直接确认；CrowdStrike 和爱沙尼亚情报机构同时报告与 FSB 的可能联系<br><strong>商业关联（2023 年披露）</strong>：俄罗斯 IT 公司 NTC Vulkan（《Der Spiegel》文件显示其与 Cozy Bear 行动有关联）</td></tr><tr><td><strong>主要目标</strong></td><td>全球范围：美国（最频繁目标）、西欧 NATO 成员国、五眼联盟国家、亚洲（中国、韩国）、南美洲；行业覆盖政府&#x2F;外交&#x2F;国防&#x2F;情报机构、政党（DNC&#x2F;RNC）、智库&#x2F;NGO、关键基础设施（金融&#x2F;能源&#x2F;电信&#x2F;制造）、医疗卫生（COVID-19 疫苗研发）、科技公司（Microsoft&#x2F;FireEye&#x2F;SolarWinds&#x2F;TeamViewer）</td></tr><tr><td><strong>攻击风格</strong></td><td>隐蔽渗透、供应链攻击、长期潜伏、鱼叉式钓鱼、密码喷洒</td></tr></tbody></table><p>舒适熊由荷兰安全情报部门（AIVD）的情报显示，其由俄罗斯对外情报局（SVR）领导。网络安全公司 CrowdStrike 先前曾建议将其与俄罗斯联邦安全局（FSB）或 SVR 相关联。</p><p>F-Secure 对其的评价是：</p><blockquote><p><strong>“APT29 是一个资源丰富、高度专业和有组织的网络间谍组织，至少从 2008 年开始就为俄罗斯联邦工作，以收集情报支持外交和安全政策的决策。”</strong></p></blockquote><p>舒适熊拥有不同的项目，并且有不同的攻击目标。其中 <strong>“Nemesis Gemina” 项目</strong>重点关注军事、政府、能源、外交和电信部门。有证据表明，其目标涵盖荷兰、挪威、德国、乌兹别克斯坦、韩国和美国的商业实体和政府组织，如美国国务院和白宫。</p><h3 id="2-1-1-荷兰-AIVD-的突破性发现（2014）"><a href="#2-1-1-荷兰-AIVD-的突破性发现（2014）" class="headerlink" title="2.1.1 荷兰 AIVD 的突破性发现（2014）"></a>2.1.1 荷兰 AIVD 的突破性发现（2014）</h3><blockquote><p><strong>“Dutch spies caught Russian hackers on tape”（荷兰间谍当场抓获俄罗斯黑客）</strong></p></blockquote><p>2014 年夏季，荷兰总情报与安全局（AIVD）取得了一次<strong>史无前例的反情报突破</strong>——他们成功渗透了 Cozy Bear 物理办公室的网络摄像头系统，获得了该组织日常运作的直接画面证据。</p><p>这些监控画面证实了 Cozy Bear 对以下目标的攻击活动：</p><ul><li>美国民主党全国委员会（DNC）</li><li>美国国务院</li><li>白宫</li></ul><p>荷兰方面将此关键证据分享给了美国情报机构，这些证据后来被用于 FBI 对 2016 年俄罗斯选举干扰的调查中。这也是迄今为止公开披露的最成功的 APT 组织物理层反渗透行动之一。</p><h3 id="2-1-2-“Duke”系列恶意软件平台演进"><a href="#2-1-2-“Duke”系列恶意软件平台演进" class="headerlink" title="2.1.2 “Duke”系列恶意软件平台演进"></a>2.1.2 “Duke”系列恶意软件平台演进</h3><p>舒适熊的核心武器库是一套代号为 <strong>“Duke”（公爵）</strong> 的恶意软件平台，历经多年持续进化：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">时间线演进：</span><br><span class="line">2008年 → MiniDuke（汇编语言开发，初始版本，体积小巧）</span><br><span class="line">   ↓</span><br><span class="line">2013年 → CosmicDuke（更新版，更灵活的插件框架）</span><br><span class="line">   ↓</span><br><span class="line">2014年 → OnionDuke（利用 Tor 网络隐藏 C2 通信）</span><br><span class="line">        → CozyDuke（增加 C/C++ 组件和反分析功能）</span><br><span class="line">        → SeaDuke（专门用于高价值目标的特洛伊木马）</span><br><span class="line">   ↓</span><br><span class="line">2015年 → HAMMERTOSS（通过 Twitter 和 GitHub 隐蔽通道传递命令）</span><br><span class="line">   ↓</span><br><span class="line">2019年 → PolyglotDuke、RegDuke、FatDuke（Operation Ghost 幽灵行动）</span><br></pre></td></tr></table></figure><p><strong>特色技术手段：</strong></p><table><thead><tr><th>技术</th><th>描述</th></tr></thead><tbody><tr><td><strong>鱼叉式钓鱼</strong></td><td>使用定制化钓鱼邮件，曾伪装成”有趣的办公室猴子”Flash 文件</td></tr><tr><td><strong>Tor 网络利用</strong></td><td>通过俄罗斯 Tor 出口节点感染传输中的二进制可执行文件</td></tr><tr><td><strong>社交媒体 C2</strong></td><td>HAMMERTOSS 利用 Twitter 和 GitHub 进行隐蔽命令传递</td></tr><tr><td><strong>密码喷洒（Password Spraying）</strong></td><td>2024 年攻击 Microsoft 时使用的暴力破解变体</td></tr><tr><td><strong>供应链攻击</strong></td><td>SolarWinds Orion 软件供应链入侵</td></tr><tr><td><strong>Active Directory 绕过</strong></td><td>FoggyWeb 和 MagicWeb 工具用于绕过 AD 认证</td></tr></tbody></table><p><strong>后期高级工具（2021-2022）：</strong></p><ul><li><strong>FoggyWeb</strong>：从受损的 Active Directory 实例中转储认证令牌</li><li><strong>MagicWeb</strong>：在 Active Directory 联合身份验证服务（AD FS）服务器上绕过用户认证</li></ul><h3 id="2-2-历年攻击行动一览"><a href="#2-2-历年攻击行动一览" class="headerlink" title="2.2 历年攻击行动一览"></a>2.2 历年攻击行动一览</h3><h4 id="2014-年：办公室猴子行动"><a href="#2014-年：办公室猴子行动" class="headerlink" title="2014 年：办公室猴子行动"></a>2014 年：办公室猴子行动</h4><p>2014 年 3 月，位于华盛顿特区的一家私人研究机构的网络上发现了恶意软件包 Cozyduke（Trojan.Cozer）。随后，舒适熊发起了一系列电子邮件活动，试图诱使受害者点击”办公室猴子”（Office Monkeys）的 Flash 视频，其中包含恶意可执行文件。到 2014 年 7 月，该组织将受 Cozyduke 感染的系统上安装了 Miniduke。</p><p>同年夏天，荷兰情报部门通过数字代理对舒适熊进行了<strong>反渗透</strong>，发现其针对美国民主党、国务院和白宫网络发起攻击的情报。</p><h4 id="2015-年：入侵五角大楼"><a href="#2015-年：入侵五角大楼" class="headerlink" title="2015 年：入侵五角大楼"></a>2015 年：入侵五角大楼</h4><p>2015 年 8 月，舒适熊针对五角大楼电子邮件系统发起了鱼叉式网络钓鱼攻击，导致在调查期间<strong>关闭了整个联合参谋部非机密电子邮件系统和互联网访问</strong>。</p><h4 id="2016-年：美国大选与-DNC-攻击"><a href="#2016-年：美国大选与-DNC-攻击" class="headerlink" title="2016 年：美国大选与 DNC 攻击"></a>2016 年：美国大选与 DNC 攻击</h4><p>2016 年 6 月，舒适熊与另一 APT 组织奇幻熊一起卷入了<strong>民主党全国委员会（DNC）网络攻击</strong>，窃取了大量邮件。这些邮件的曝光直接影响了对美国 2016 年总统大选结果的舆论走向。</p><p>2016 年美国总统大选之后，舒适熊又针对美国总部设的智囊团和非政府组织（NGO）发起了一系列经过协调且精心计划的鱼叉式网络钓鱼攻击。</p><h4 id="2017-年：挪威与荷兰政府部门"><a href="#2017-年：挪威与荷兰政府部门" class="headerlink" title="2017 年：挪威与荷兰政府部门"></a>2017 年：挪威与荷兰政府部门</h4><ul><li><strong>挪威</strong>：2017 年 2 月 3 日，挪威警察安全局（PST）报告说，舒适熊尝试伪造挪威国防部、外交部和工党的 9 个人的电子邮件账户，对其目标包括挪威辐射防护局发起攻击。挪威总理埃尔纳·索尔伯格称这些行为是”对我们的民主体制的严重攻击”。</li><li><strong>荷兰</strong>：2017 年 2 月，据透露，在过去的六个月中，舒适熊和奇幻熊曾多次尝试入侵荷兰总务部等政府部门。通用情报与安全局负责人罗伯·伯托利确认黑客是俄罗斯人，曾试图获取政府机密文件。随后，荷兰宣布 2017 年 3 月大选的选票将进行<strong>人工计票</strong>。</li></ul><h4 id="2019-年：幽灵行动"><a href="#2019-年：幽灵行动" class="headerlink" title="2019 年：幽灵行动"></a>2019 年：幽灵行动</h4><p>由于发现了三个归因于舒适熊的新恶意软件家族：<strong>PolyglotDuke、RegDuke 和 FatDuke</strong>，这表明舒适熊并没有停止活动，而是开发了更难发现的新工具。使用这些新发现的软件包进行的行动被统称为**”幽灵行动”（Operation Ghost）**。</p><h4 id="2020-年：两大震惊世界的行动"><a href="#2020-年：两大震惊世界的行动" class="headerlink" title="2020 年：两大震惊世界的行动"></a>2020 年：两大震惊世界的行动</h4><h5 id="（1）窃取-COVID-19-疫苗数据"><a href="#（1）窃取-COVID-19-疫苗数据" class="headerlink" title="（1）窃取 COVID-19 疫苗数据"></a>（1）窃取 COVID-19 疫苗数据</h5><p>2020 年 7 月，NSA、NCSC 和 CSE 联合指控舒适熊窃取了有关 COVID-19 疫苗和治疗方法的数据。英国国家网络安全中心（NCSC）声称有 95% 以上的把握可以确定 APT29 是俄罗斯情报机构的一部分。</p><p>英国外交大臣多米尼克·拉布严厉谴责：”俄罗斯情报部门正将目标投向那些与新冠疫情抗争的人，这是完全不可接受的。”</p><p>该组织使用了名为 <strong>“WellMess”</strong> 和 <strong>“WellMail”</strong> 的自定义恶意软件，通过鱼叉式网络钓鱼攻击疫苗研发机构。克里姆林宫发言人佩斯科夫否认指控，称”俄罗斯与这些企图无关”。</p><blockquote><p>值得注意的是，此次归因在安全行业内引发了巨大争议——英国 NCSC 的报告中<strong>没有提供任何用于归因的技术证据</strong>，引发了卡巴斯基 GReAT 安全研究员 Brian Bartholomew 等人的公开质疑，而具有美国情报界背景的 Jason Kikta 等人则对此进行了猛烈抨击。</p></blockquote><h5 id="（2）SolarWinds-供应链攻击——“史上最大规模、最复杂的网络攻击”"><a href="#（2）SolarWinds-供应链攻击——“史上最大规模、最复杂的网络攻击”" class="headerlink" title="（2）SolarWinds 供应链攻击——“史上最大规模、最复杂的网络攻击”"></a>（2）SolarWinds 供应链攻击——“史上最大规模、最复杂的网络攻击”</h5><p>2020 年 12 月，一场震惊全球的供应链攻击被确认。舒适熊利用美国科技公司 SolarWinds 的 Orion 网络监控软件的更新机制，植入后门程序 <strong>SUNBURST</strong>（微软命名为 Solorigate），使黑客可以访问使用其产品的数千家公司和政府机关。</p><p><strong>攻击机制详解：</strong></p><ol><li>攻击者通过 DLL 劫持技术，将 SolarWinds 产品中的动态库文件替换为修改后的恶意程序</li><li>恶意 DLL 经过<strong>合法数字签名</strong>，再替换官方 DLL 文件，实现绕过防御措施加载</li><li>Solorigate 后门修改了官方 DLL 文件中的 <code>BackgroundInventory.RefreshInstance()</code> 函数，添加 try-catch 处理，将恶意代码内嵌其中</li><li>后门主要功能集中在 <code>OrionImprovementBusinessLayer</code> 类中，核心功能包括：<ul><li>利用 DGA 算法动态生成 C2 服务器域名</li><li>使用 HTTPS 协议与 C2 进行加密通信</li><li>执行远程指令（文件上传&#x2F;下载、命令执行等）</li></ul></li></ol><p><strong>影响范围：</strong></p><ul><li>SolarWinds 全球拥有超过 <strong>300,000 家客户</strong>，包括美国军方、五角大楼、司法部、国务院、商务部、财政部以及 400 多家财富 500 强公司</li><li>潜在受影响客户不到 <strong>18,000 个</strong>，约 <strong>250 个</strong> 政府和企业网络被实际访问</li><li>受影响机构包括：美国商务部、能源部、司法部、财政部、国务院、国土安全部、五角大楼部分区域、国立卫生研究院</li><li>思科、英特尔、德勤等公司也成为受害者</li><li>FireEye 是第一个发现此次攻击的安全公司</li></ul><p>微软总裁布拉德·史密斯在接受 CBS “60 分钟”节目采访时表示：</p><blockquote><p><strong>“从软件工程的角度来看，可以说这是世界上有史以来规模最大、最复杂的攻击。当我们分析了所有内容时，问自己有多少工程师涉及此次攻击行动，我们得出的答案肯定是超过 1000 人。”</strong></p></blockquote><p>美国 FBI、CISA、ODNI 和 NSA 联合工作组发表声明，指控确认与俄罗斯有关的 APT 组织针对 SolarWinds 发起了复杂的供应链攻击。声明称：”少于 10 个”的政府机构受到了威胁。</p><p>俄罗斯大使馆否认三连：”信息领域的恶意活动与俄罗斯外交政策、国家利益和我们对国家间关系的理解相抵触。俄罗斯不会在网络领域进行进攻性行动。”</p><h3 id="2-3-归因争议：网络威胁归因的两面性"><a href="#2-3-归因争议：网络威胁归因的两面性" class="headerlink" title="2.3 归因争议：网络威胁归因的两面性"></a>2.3 归因争议：网络威胁归因的两面性</h3><p>舒适熊的多次攻击行动引发了安全行业对**网络威胁归因（Threat Attribution）**的激烈讨论。归因主要分为两个层次：</p><ol><li><strong>技术层面——活动归因（How）</strong>：对恶意活动进行聚类，通过寻找历史 TTPs（战术、技术和程序）和当前活动 TTPs 的重叠，将不同活动聚类到同一组中</li><li><strong>战略层面——归因到具体威胁行为体和实体（Who）</strong>：追踪攻击者的真实身份和幕后组织者，涉及初始地域来源、特殊设备&#x2F;网络 ID、相关个人或组织三个层面的判定</li></ol><p>在多次归因事件中，政府机构的安全声明往往缺少技术证据支撑，而安全厂商之间的学术争论也常常被大国博弈的政治因素所裹挟。</p><h4 id="2021-2024-年：持续活跃"><a href="#2021-2024-年：持续活跃" class="headerlink" title="2021-2024 年：持续活跃"></a>2021-2024 年：持续活跃</h4><h5 id="2021-年：入侵共和党全国委员会（RNC）"><a href="#2021-年：入侵共和党全国委员会（RNC）" class="headerlink" title="2021 年：入侵共和党全国委员会（RNC）"></a>2021 年：入侵共和党全国委员会（RNC）</h5><p>2021 年 7 月，舒适熊入侵了<strong>美国共和党全国委员会（RNC）</strong>。与 2016 年的 DNC 攻击类似，但这次的目标是共和党。攻击通过被攻陷的第三方 IT 供应商 <strong>Synnex</strong> 实施，显示了该组织对供应链攻击路径的持续偏好。</p><h5 id="2021-年：FoggyWeb-工具披露"><a href="#2021-年：FoggyWeb-工具披露" class="headerlink" title="2021 年：FoggyWeb 工具披露"></a>2021 年：FoggyWeb 工具披露</h5><p>2021 年 9 月，Microsoft 披露了舒适熊使用的 <strong>FoggyWeb</strong> 工具——该工具可从受损的 Active Directory 实例中转储认证令牌，实现持久化访问。</p><h5 id="2022-年：MagicWeb-AD-FS-认证绕过"><a href="#2022-年：MagicWeb-AD-FS-认证绕过" class="headerlink" title="2022 年：MagicWeb AD FS 认证绕过"></a>2022 年：MagicWeb AD FS 认证绕过</h5><p>2022 年 8 月 24 日，Microsoft 披露舒适熊使用 <strong>MagicWeb</strong> 工具在 Active Directory 联合身份验证服务（AD FS）服务器上<strong>绕过用户认证</strong>，这意味着即使受害者修改密码，攻击者仍可维持访问权限。</p><h5 id="2024-年：入侵-Microsoft-自身"><a href="#2024-年：入侵-Microsoft-自身" class="headerlink" title="2024 年：入侵 Microsoft 自身"></a>2024 年：入侵 Microsoft 自身</h5><ul><li><strong>时间线</strong>：2023 年 11 月开始，2024 年 1 月被发现并终止</li><li><strong>目标</strong>：Microsoft 高管及法律&#x2F;网络安全团队的电子邮箱</li><li><strong>手段</strong>：<strong>密码喷洒攻击</strong>（Password Spraying）——一种针对弱密码的暴力破解变体</li><li><strong>目的</strong>：了解 Microsoft 对该组织的调查情况（”调查调查者”）</li><li><strong>影响</strong>：少量企业邮箱账户被访问，未发现对客户环境或 AI 系统造成影响</li></ul><h5 id="2024-年：入侵-TeamViewer"><a href="#2024-年：入侵-TeamViewer" class="headerlink" title="2024 年：入侵 TeamViewer"></a>2024 年：入侵 TeamViewer</h5><p>2024 年 6 月 28 日，德国 <strong>TeamViewer SE</strong> 公司宣布其企业 IT 网络被舒适熊入侵。幸运的是，用户数据和远程桌面产品本身未受影响。这是该组织针对欧洲科技公司的重要案例。</p><hr><h2 id="三、奇幻熊-Fancy-Bear（APT28）"><a href="#三、奇幻熊-Fancy-Bear（APT28）" class="headerlink" title="三、奇幻熊 Fancy Bear（APT28）"></a>三、奇幻熊 Fancy Bear（APT28）</h2><h3 id="3-1-组织概况"><a href="#3-1-组织概况" class="headerlink" title="3.1 组织概况"></a>3.1 组织概况</h3><table><thead><tr><th>属性</th><th>信息</th></tr></thead><tbody><tr><td><strong>代号</strong></td><td>APT28</td></tr><tr><td><strong>别名</strong></td><td><strong>核心别名</strong>：Fancy Bear、APT28、Pawn Storm、Sofacy Group、Sednit<br><strong>厂商命名</strong>：STRONTIUM（微软命名）、Tsar Team、Threat Group-4127、T-APT-12<br><strong>合并称呼</strong>：Grizzly Steppe（与 Cozy Bear&#x2F;APT29 合称时，意为”灰熊步态”）</td></tr><tr><td><strong>成立时间</strong></td><td>可能成立于 2004–2007 年间，活动可追溯至 2000 年代中期</td></tr><tr><td><strong>关联机构</strong></td><td><strong>俄罗斯军事情报总局（GRU）第 26165 部队</strong>——2018 年美国特别顾问调查报告正式认定；该部队总部主要从事网络攻击和骇客数据解密等工作<br><strong>隶属关系</strong>：在俄罗斯”双熊”体系中隶属于”安逸熊”（上级协调组织）</td></tr><tr><td><strong>主要目标</strong></td><td>政府、军队、航空航天、国防、国际组织、媒体、体育机构；<strong>重点区域</strong>：外高加索地区、NATO 北约成员国</td></tr><tr><td><strong>攻击风格</strong></td><td>高调曝光、公开炫耀战果（全球罕见建官网的 APT）、零日漏洞利用、钓鱼网站、定向攻击</td></tr><tr><td><strong>重大事件</strong></td><td><strong>2023 年 7 月 24 日</strong>：奇幻熊总部成为乌克兰无人机攻击目标，总部其中一栋大楼在爆炸式攻击后倒塌</td></tr></tbody></table><p>奇幻熊是一个被认为归属于俄罗斯军事情报局（GRU）的网络间谍组织。防火墙公司 CrowdStrike 表示此组织与俄罗斯军事组织 GRU 存在一定程度的关联。英国外交联邦发展事务所以及网络安全公司 SecureWorks、ThreatConnect、Mandiant 也表示此组织很可能是由俄罗斯政府扶持并运作。</p><p>2018 年，美国特别顾问调查报告认定<strong>奇幻熊为 GRU 第 26165 部队的分支</strong>，该部队主要从事网络攻击和骇客数据解密等工作。</p><p><strong>“奇幻熊”这个名字来自美国资安研究员 Dmitri Alperovitch（CrowdStrike 联合创始人）的命名。</strong></p><p>今天我们要讲的这个组织，颇有些”豪迈”的”英雄气概”，我们暂且把它归为**”豪放派”**。它还有个很霸气、又萌萌哒的名字——<strong>奇幻熊（Fancy Bear）</strong>。</p><h3 id="3-1-1-完整攻击目标清单"><a href="#3-1-1-完整攻击目标清单" class="headerlink" title="3.1.1 完整攻击目标清单"></a>3.1.1 完整攻击目标清单</h3><p>根据维基百科及多方安全机构报告，奇幻熊的攻击目标覆盖全球各大关键领域：</p><table><thead><tr><th>目标类型</th><th>具体受害者&#x2F;机构</th><th>攻击时间</th></tr></thead><tbody><tr><td><strong>政府立法机构</strong></td><td>德国联邦议院</td><td>2014-2015</td></tr><tr><td></td><td>挪威议会</td><td>2014-2017</td></tr><tr><td><strong>政府行政部门</strong></td><td>美国白宫</td><td>2015</td></tr><tr><td></td><td>荷兰政府部门（总务部等）</td><td>2016-2017</td></tr><tr><td><strong>军事&#x2F;防务</strong></td><td>北约（NATO）总部</td><td>2015</td></tr><tr><td></td><td>乌克兰军事部门</td><td>2014-2016</td></tr><tr><td><strong>媒体机构</strong></td><td>法国电视台 TV5Monde</td><td>2015（大规模攻击导致频道瘫痪）</td></tr><tr><td><strong>政党&#x2F;选举</strong></td><td>美国民主党全国委员会（DNC）</td><td>2016</td></tr><tr><td></td><td>法国总统候选人 Emmanuel Macron</td><td>2017（大选期间）</td></tr><tr><td></td><td>希拉里·克林顿总统竞选团队</td><td>2016（SecureWorks 确认）</td></tr><tr><td><strong>国际组织</strong></td><td>欧洲安全与合作组织（OSCE）</td><td>—</td></tr><tr><td></td><td>国际田联（IAAF）</td><td>—</td></tr><tr><td><strong>体育&#x2F;反兴奋剂</strong></td><td>世界反兴奋剂机构（WADA）</td><td>2016-2018</td></tr><tr><td></td><td>国际奥委会（IOC）&#x2F;美国奥委会</td><td>2018</td></tr><tr><td><strong>其他</strong></td><td>外高加索地区多国政府和机构</td><td>持续</td></tr></tbody></table><h3 id="3-1-2-2023-年：乌克兰无人机袭击奇幻熊总部"><a href="#3-1-2-2023-年：乌克兰无人机袭击奇幻熊总部" class="headerlink" title="3.1.2 2023 年：乌克兰无人机袭击奇幻熊总部"></a>3.1.2 2023 年：乌克兰无人机袭击奇幻熊总部</h3><blockquote><p><strong>物理打击回响网络战</strong></p></blockquote><p>2023 年 7 月 24 日，在俄乌冲突持续背景下，<strong>奇幻熊总部成为乌克兰无人机攻击的目标</strong>。据德国之声（DW）等媒体报道，位于莫斯科的 GRU 第 26165 部队总部大楼在<strong>爆炸式攻击后倒塌</strong>。</p><p>这是极为罕见的案例——一个 APT 黑客组织的<strong>物理总部遭到实体军事打击</strong>。事件标志着网络战与常规军事行动之间的界限进一步模糊，也表明在当代冲突中，网络攻击能力已成为与常规军事力量同等重要的打击目标。</p><h3 id="3-2-为啥说它”豪迈”？"><a href="#3-2-为啥说它”豪迈”？" class="headerlink" title="3.2 为啥说它”豪迈”？"></a>3.2 为啥说它”豪迈”？</h3><p>一般有哪个黑客组织敢这样明目张胆地炫耀自己的”战果”？奇幻熊就这么做了——<strong>它开了自己的官网</strong>。</p><p>“奇幻熊”在自我介绍中是这样说的：</p><blockquote><p><strong>“我们是一个国际黑客组织，致力于维护公平的竞赛和干净的运动。我们宣布开始 ‘#OpOlympics’，将告诉你奥林匹克奖牌是如何赢得的。我们黑进了世界反兴奋剂机构的数据库，被我们所见的给震惊了。我们将从美国队开刀，他们的胜利被玷污，我们也会放出别的世界队伍的消息，你们就等着看著名运动员的服用违禁药的消息吧。”</strong></p></blockquote><p>在其官网上，还有其”揭露”的多个项目：</p><table><thead><tr><th>项目名称</th><th>发布日期</th></tr></thead><tbody><tr><td>U.S. and Canada Sports Officials’ Secret Plot Revealed</td><td>2016-12-13</td></tr><tr><td>U.S. Anti-Doping Agency Staff Emails</td><td>2016-10-06</td></tr><tr><td>WADA databases. Part 6</td><td>2016-09-30</td></tr><tr><td>WADA databases. Part 5</td><td>2016-09-23</td></tr><tr><td>WADA databases. Part 4</td><td>2016-09-19</td></tr><tr><td>WADA databases. Part 3</td><td>2016-09-17</td></tr><tr><td>WADA databases. Part 2</td><td>2016-09-15</td></tr><tr><td>American Athletes Caught Doping</td><td>2016-09-13</td></tr></tbody></table><p>有人会问，它这么”坦诚”，没人找它们喝茶吗？肯定有人找，但未必能找得到——还没人拿到确凿的证据指认”它究竟是谁”。</p><h3 id="3-3-前世今生：为何与奥林匹克”杠上”？"><a href="#3-3-前世今生：为何与奥林匹克”杠上”？" class="headerlink" title="3.3 前世今生：为何与奥林匹克”杠上”？"></a>3.3 前世今生：为何与奥林匹克”杠上”？</h3><p>这还得从多年前说起。当年有人举报俄罗斯队违规使用兴奋剂，相关机构在 2014 年夏季奥运会期间对此展开调查。俄罗斯官员一再否认所有指控，但证据及后续的医学检测均显示，<strong>超过 1000 名俄罗斯运动员涉嫌从其国家资助的兴奋剂计划中受益</strong>，于是俄罗斯队被全面禁赛。</p><p>此后就拉开了奇幻熊的”神奇之旅”。</p><h3 id="3-4-2016-年-WADA-数据库入侵事件——五弹连发"><a href="#3-4-2016-年-WADA-数据库入侵事件——五弹连发" class="headerlink" title="3.4 2016 年 WADA 数据库入侵事件——五弹连发"></a>3.4 2016 年 WADA 数据库入侵事件——五弹连发</h3><p>2016 年，这只奇幻熊侵入了世界反兴奋剂机构（WADA）的数据库。随后，陆续曝光数十位世界级知名运动员”以治疗为目的”在该机构的允许下使用违禁药物。</p><h4 id="第一弹：9月13日-——-全球哗然"><a href="#第一弹：9月13日-——-全球哗然" class="headerlink" title="第一弹：9月13日 —— 全球哗然"></a>第一弹：9月13日 —— 全球哗然</h4><p>第一批名单公布，该资料显示：</p><ul><li><strong>西蒙·拜尔斯</strong>（Simone Biles）：里约奥运会 4 金得主，在 2016 年 8 月奥运会期间兴奋剂检测呈阳性，她在 2012－2014 年期间多次被获准使用禁药用于治疗</li><li><strong>威廉姆斯姐妹</strong>（Serena &amp; Venus Williams）：最近几年都被允许使用含兴奋剂成分的药物</li><li><strong>多恩</strong>（Brittney Griner）：女篮明星也有类似被允许使用特定”禁药”的情况</li></ul><h4 id="第二弹：9月15日"><a href="#第二弹：9月15日" class="headerlink" title="第二弹：9月15日"></a>第二弹：9月15日</h4><p>第二批名单公布。英国广播公司的网站报道称：</p><ul><li><strong>布拉德利·威金斯</strong>（Bradley Wiggins）：里约奥运会场地自行车男子团体追逐赛金牌得主</li><li><strong>克里斯·弗鲁姆</strong>（Chris Froome）：三次环法自行车赛冠军</li></ul><h4 id="第三弹：9月16日-——-触目惊心"><a href="#第三弹：9月16日-——-触目惊心" class="headerlink" title="第三弹：9月16日 —— 触目惊心"></a>第三弹：9月16日 —— 触目惊心</h4><p>第三批名单公布，多名里约奥运会冠军被获准使用兴奋剂治疗伤病：</p><table><thead><tr><th>运动员</th><th>国家</th><th>项目</th><th>成绩</th></tr></thead><tbody><tr><td>尼古拉·亚当斯</td><td>英国</td><td>女子拳击</td><td>冠军</td></tr><tr><td>劳拉·特罗特</td><td>英国</td><td>女子自行车</td><td>冠军</td></tr><tr><td>西沃恩·玛丽·奥康纳</td><td>英国</td><td>女子游泳</td><td>亚军</td></tr><tr><td>奥利维娅·卡内基·布朗</td><td>英国</td><td>女子赛艇</td><td>亚军</td></tr><tr><td>金伯利·布伦南</td><td>澳大利亚</td><td>女子赛艇</td><td>冠军</td></tr><tr><td>亚历山大·别洛诺戈夫</td><td>澳大利亚</td><td>男子赛艇</td><td>亚军</td></tr><tr><td>杰克·博布里奇</td><td>澳大利亚</td><td>男子自行车</td><td>亚军</td></tr></tbody></table><h4 id="第四弹：9月20日-——-世界冠军赫然上榜"><a href="#第四弹：9月20日-——-世界冠军赫然上榜" class="headerlink" title="第四弹：9月20日 —— 世界冠军赫然上榜"></a>第四弹：9月20日 —— 世界冠军赫然上榜</h4><p>第四批名单公布：</p><ul><li><strong>拉斐尔·纳达尔</strong>（Rafael Nadal）：西班牙网球名将</li><li><strong>莫·法拉赫</strong>（Mo Farah）：里约奥运会男子 5000 米和 10000 米双料冠军</li></ul><h4 id="第五弹：9月24日"><a href="#第五弹：9月24日" class="headerlink" title="第五弹：9月24日"></a>第五弹：9月24日</h4><p>第五批名单公布，又包括五名里奥奥运会冠军和一位中国乒乓球迷熟悉的老对手：</p><ul><li><strong>法比安·坎切拉拉</strong>（瑞士）：男子公路自行车个人计时赛冠军</li><li><strong>尼诺·舒尔特</strong>（瑞士）：山地自行车金牌得主<br><strong>汉娜·米尔斯</strong>（英国）：帆船女子双人艇 470 级冠军</li><li><strong>坎贝尔</strong>（澳大利亚）：女子 4×100 米自由泳接力成员，当时 100 米自由泳世界纪录保持者——<strong>名气最响的运动员</strong></li><li><strong>麦迪逊</strong>（澳大利亚）：坎贝尔队友</li><li><strong>蒂姆·波尔</strong>（德国）：乒乓球名将，中国乒乓球队的老对手，被披露曾服用一种肾上腺皮质激素（时间为 2012 年伦敦奥运会前夕）。里约奥运会上，波尔担任了德国代表团的开幕式旗手</li></ul><blockquote><p>值得一提的是，打破女子 100 米自由泳世界纪录的澳大利亚自由泳名将坎贝尔，资料显示她一共服用了三种禁药，曾一日两次使用糖皮质激素和沙美特罗，时间是 2013 年 7 月 21 日之前。</p></blockquote><p>以上运动员不仅在里约奥运会上获得金牌，还获得了**”治疗豁免用药权”（TUE, Therapeutic Use Exemption）**。</p><h3 id="3-5-后续影响"><a href="#3-5-后续影响" class="headerlink" title="3.5 后续影响"></a>3.5 后续影响</h3><p>“奇幻熊”曝光的用药名单给世界体坛带来巨大的震动。同时，作为兴奋剂检测的权威，<strong>世界反兴奋剂机构陷入了严重的信用危机</strong>。国际奥委会主席巴赫对于黑客行为深感忧虑。</p><p>世界反兴奋剂机构表示，这次黑客攻击源自俄罗斯。俄罗斯断然否认”黑客组织与俄政府有关”，也否认了俄政府支持这一黑客行动。</p><p>俄总统普京在独联体峰会期间回应说：</p><blockquote><p><strong>“我们并不赞同黑客行为，但这次事件不可能不受到国家社会，尤其是体育界的关注。国际社会应注意在反兴奋剂行动中存在的双重标准。”</strong></p></blockquote><p>受”奇幻熊”影响，世界舆论把矛头都对准了世界反兴奋剂机构，被牵连的国际奥委会也不得不开展危机公关，表示将成立一个诚信联盟，以应对世界体坛出现的腐败、禁药丑闻等问题。10 月 8 日，国际奥委会在瑞士洛桑召开特别峰会，讨论未来世界反兴奋剂形势。</p><h3 id="3-6-故事并未结束"><a href="#3-6-故事并未结束" class="headerlink" title="3.6 故事并未结束"></a>3.6 故事并未结束</h3><p>你以为故事就此结束了？<strong>NO！</strong></p><h4 id="2018-年：再度出击"><a href="#2018-年：再度出击" class="headerlink" title="2018 年：再度出击"></a>2018 年：再度出击</h4><p>2018 年，奇幻熊再度对 WADA 展开攻击，并于 1 月 10 日在推特发文，声称获取了国际奥委会（IOC）和世界反兴奋剂组织（WADA）的电子邮件，指责美国在内的”盎格鲁·撒克逊（Anglo-Saxon）”国家打着纯洁体育的名义谋求金钱和权力，而俄罗斯被禁赛是政治阴谋。同年，<strong>平昌冬奥会期间也遭受其黑客攻击</strong>。</p><h4 id="2020-年：目标东京奥运会"><a href="#2020-年：目标东京奥运会" class="headerlink" title="2020 年：目标东京奥运会"></a>2020 年：目标东京奥运会</h4><p>从 9 月 16 日开始，微软威胁情报中心的安全研究员追踪到奇幻熊的”新动态”。这次，它的目标对准了 <strong>2020 年东京奥运会</strong>。与以往攻击方法相似，奇幻熊依然采用包括鱼叉式网络钓鱼、密码喷雾、远程侵入互联网设备以及使用开源和自定义恶意软件。这次，在<strong>三大洲至少 16 个国家</strong>的国际体育和反兴奋剂组织均成为攻击目标。</p><h3 id="3-7-其他重大攻击事件"><a href="#3-7-其他重大攻击事件" class="headerlink" title="3.7 其他重大攻击事件"></a>3.7 其他重大攻击事件</h3><p>你以为这些就是奇幻熊全部的”英勇事迹”吗？<strong>NO！</strong></p><p>以下攻击统统都算在了它的头上：</p><table><thead><tr><th>时间</th><th>目标</th><th>详情</th></tr></thead><tbody><tr><td>2014-2016</td><td>德国议会</td><td>成功入侵德国联邦议院系统</td></tr><tr><td>2015 年 4 月</td><td>法国电视网络 TV5Monde</td><td>发起大规模网络攻击</td></tr><tr><td>2015 年 8 月</td><td>白宫、北约</td><td>入侵目标系统</td></tr><tr><td>2015 年 10 月</td><td>荷兰安全委员会、MH17 相关记者</td><td>攻击马航 MH17 坠机事件相关记者</td></tr><tr><td>2016 年 3 月</td><td>美国民主党全国委员会（DNC）</td><td>与舒适熊联合攻击，窃取大量邮件</td></tr><tr><td>2016 年 5 月</td><td>美国政府部门</td><td>多个机构遭入侵</td></tr><tr><td>2017 年</td><td>法国总统候选人马克龙</td><td>在大选期间对其发起攻击</td></tr><tr><td>2017 年 2 月</td><td>荷兰政府部门</td><td>试图窃取机密文件</td></tr><tr><td>2018 年 1 月</td><td>国际奥委会和美国奥委会</td><td>再次展开攻击</td></tr><tr><td>2023 年 7 月</td><td>—</td><td>幻想熊总部成为乌克兰无人机攻击的目标，总部一栋大楼在爆炸后倒塌</td></tr></tbody></table><p>尽管不断被曝光和谴责，但该黑客组织的活动似乎一点不受影响，依然十分活跃。</p><hr><h2 id="四、奇幻熊的技术进化史"><a href="#四、奇幻熊的技术进化史" class="headerlink" title="四、奇幻熊的技术进化史"></a>四、奇幻熊的技术进化史</h2><p>根据 Palo Alto Networks、FireEye、思科等多家国外网络安全公司的分析，奇幻熊活跃最为频繁是在 2018 年至 2019 年期间，并在<strong>武器化、恶意软件传播及长久驻留</strong>三大领域进行了重大更新。</p><h3 id="4-1-武器化技术的演进"><a href="#4-1-武器化技术的演进" class="headerlink" title="4.1 武器化技术的演进"></a>4.1 武器化技术的演进</h3><p>武器化可以被理解为一种”战前准备”技术，是指为实施复杂攻击而所做的一切准备，包括搭建基础设施（C2、域名和 IP、证书以及库）、分发恶意软件样本等。</p><table><thead><tr><th>时间段</th><th>使用技术</th><th>特点</th></tr></thead><tbody><tr><td>2017 - 2018 初</td><td>T1251、T1329、T1336、T1319</td><td>侧重借助第三方资源实施攻击，节省开发成本</td></tr><tr><td>2018 初 - 2018 中</td><td>T1314、T1322、T1328</td><td>开始转向内部专业资源，提高攻击成功率</td></tr><tr><td>2018.10 - 2019.03</td><td>T1345</td><td>引入完全不同的武器化技术，开发能力新提升</td></tr></tbody></table><h3 id="4-2-恶意软件传播：漏洞利用"><a href="#4-2-恶意软件传播：漏洞利用" class="headerlink" title="4.2 恶意软件传播：漏洞利用"></a>4.2 恶意软件传播：漏洞利用</h3><p>利用漏洞来传播恶意软件，是奇幻熊的惯用手法。据称，奇幻熊过去常使用的漏洞包括：</p><h4 id="Windows-系统漏洞"><a href="#Windows-系统漏洞" class="headerlink" title="Windows 系统漏洞"></a>Windows 系统漏洞</h4><table><thead><tr><th>CVE 编号</th><th>漏洞名称 &#x2F; 别名</th><th>漏洞类型</th><th>影响组件</th><th>简要说明</th></tr></thead><tbody><tr><td><strong>CVE-2017-0144</strong></td><td><strong>EternalBlue（永恒之蓝）</strong></td><td>SMBv1 远程代码执行（RCE）</td><td>Windows SMBv1 协议（srv.sys）</td><td>NSA 开发的 exploit，2017 年由 Shadow Brokers 泄露。利用 SMBv1 处理特制数据包时的堆缓冲区溢出，可实现<strong>无需认证的远程代码执行</strong>，且具备<strong>蠕虫式自我传播能力</strong>。被 WannaCry 勒索病毒和 NotPetya 破坏性恶意软件大规模使用，造成全球数百亿美元损失。微软于 2017 年 3 月发布 MS17-010 补丁修复。CVSS 8.1（高危）</td></tr><tr><td><strong>CVE-2012-0158</strong></td><td><strong>MSCOMCTL.OCX RCE 漏洞</strong></td><td>ActiveX 控件缓冲区溢出 → RCE</td><td>MSCOMCTL.OCX（ListView&#x2F;TreeView 等 Common Controls）</td><td>攻击者可构造畸形 DOC&#x2F;RTF 文件，通过嵌入恶意 ActiveX 控件触发缓冲区溢出，实现远程代码执行。影响 Office 2003&#x2F;2007&#x2F;2010、SQL Server、Visual Basic 6.0 等大量产品。该漏洞发布后<strong>立即被野外大规模利用</strong>，瑞星等安全厂商截获大量基于此漏洞的畸形文档病毒。微软安全公告 MS12-027。CVSS 9.3（严重）</td></tr><tr><td><strong>CVE-2015-1641</strong></td><td><strong>Word 类型混淆漏洞（Type Confusion）</strong></td><td>内存损坏 → RCE</td><td>Microsoft Word（WWLIB.DLL）</td><td>Word 在处理 <code>displacedByCustomXml</code> 属性时，未对 customXml 标签对象进行有效性验证，攻击者可传入其他标签对象导致<strong>类型混淆</strong>，进而实现任意内存写入和远程代码执行。影响 Word 2007&#x2F;2010&#x2F;2013 等。被 Patchwork APT 组织等广泛使用。EPSS 利用概率排名 2015 年 Office 漏洞第一（99.9th 百分位）。CVSS 7.8</td></tr><tr><td><strong>CVE-2015-1642</strong></td><td><strong>Office 内存损坏漏洞</strong></td><td>内存损坏 → RCE</td><td>Microsoft Office</td><td>Office 存在内存损坏漏洞，允许远程攻击者通过精心构造的文档（如 RTF）执行任意代码。与 CVE-2015-1641 同期被发现并利用，属于同一批 Office 零日漏洞之一。CVSS 7.8</td></tr><tr><td><strong>CVE-2015-1701</strong></td><td><strong>Win32k 权限提升漏洞</strong></td><td>本地权限提升（LPE）</td><td>Win32k.sys 内核驱动</td><td>Win32k 内核模式驱动存在未明漏洞，允许本地攻击者通过精心构造的应用程序<strong>从普通用户权限提升至 SYSTEM 权限</strong>。常与其他 RCE 漏洞配合使用，形成完整的”RCE + 提权”攻击链。CISA 已列入已知被利用漏洞目录（KEV）。CVSS 7.8</td></tr><tr><td><strong>CVE-2015-2387</strong></td><td><strong>ATMFD.DLL 内存损坏漏洞（字体驱动提权）</strong></td><td>本地权限提升（LPE）</td><td>ATMFD.DLL（Adobe Type Manager Font Driver）</td><td>ATM 字体驱动存在越界写入漏洞，允许本地用户通过精心构造的应用程序<strong>获取系统最高权限</strong>。影响 Windows Server 2003 至 Windows 8.1 全系列版本。微软安全公告 MS15-077。CISA KEV。CVSS 7.8</td></tr><tr><td><strong>CVE-2015-2590</strong></td><td><strong>Oracle Java SE 远程代码执行</strong></td><td>Java 远程代码执行（RCE）</td><td>Oracle Java Runtime Environment (JRE)</td><td>Oracle Java 库中存在未公开漏洞，允许远程攻击者通过未知向量<strong>危及机密性、完整性和可用性</strong>。常用于攻击使用旧版 Java 插件的浏览器环境。CVSS 9.8（严重）</td></tr><tr><td><strong>CVE-2015-3043</strong></td><td><strong>Flash FLV 缓冲区溢出</strong></td><td>Flash 内存损坏 → RCE</td><td>Adobe Flash Player（FLV 解码器）</td><td>Flash 在播放 FLV 文件时，解压 Nellymoser 压缩的音频 tag 时<strong>未正确检查所需 buffer 长度</strong>，导致缓冲区溢出。可通过构造特定 audio tag 触发并执行恶意代码。CISA KEV。CVSS 9.8</td></tr><tr><td><strong>CVE-2015-4902</strong></td><td><strong>Flash Player 内存损坏</strong></td><td>Flash 内存损坏 → RCE</td><td>Adobe Flash Player</td><td>Flash Player 存在内存损坏漏洞，允许攻击者执行任意代码或造成拒绝服务。CISA 已列入 KEV 目录。CVSS 9.8</td></tr><tr><td><strong>CVE-2015-7645</strong></td><td><strong>Flash IExternalizable.writeExternal 类型混淆</strong></td><td>Flash 任意代码执行（RCE）</td><td>Adobe Flash Player 18.x&#x2F;19.x</td><td>通过构造恶意 SWF 文件，利用 <code>IExternalizable.writeExternal</code> 接口的<strong>类型混淆漏洞</strong>执行任意代码。<strong>被趋势科技确认在野外被 Pawn Storm（即奇幻熊 APT28）活动所利用</strong>。Adobe 安全公告 APSA15-05 &#x2F; APSB15-27。CVSS 9.3</td></tr><tr><td><strong>CVE-2015-5119</strong></td><td><strong>Flash ByteArray Use-After-Free（Hacking Team 漏洞）</strong></td><td>UAF → RCE</td><td>Adobe Flash Player（ActionScript 3 ByteArray 类）</td><td>Flash AS3 的 ByteArray 类存在<strong>释放后重用（UAF）漏洞</strong>，攻击者可通过覆盖 <code>valueOf</code> 函数触发。此漏洞因 <strong>Hacking Team 数据泄露事件</strong>而曝光（泄露了武器化的 exploit 代码），随后在 2015 年 7 月被迅速大规模利用。CVSS 9.8（严重），CISA KEV</td></tr><tr><td><strong>CVE-2017-0262</strong></td><td><strong>Office EPS 远程代码执行</strong></td><td>EPS 解析 → RCE</td><td>Microsoft Office（EPS 过滤器）</td><td>Office 在处理封装 PostScript（EPS）文件时存在<strong>远程命令执行漏洞</strong>。攻击者可向目标发送包含畸形 EPS 对象的 DOC 文档，当用户打开时即可在电脑上执行任意指令。<strong>与 CVE-2017-0263 配合形成”RCE + 提权”完整攻击链</strong>。微软已收到有限定向攻击的报告。CVSS 7.8</td></tr><tr><td><strong>CVE-2017-0263</strong></td><td><strong>Win32k 内核权限提升（Use-After-Free）</strong></td><td>内核 UAF → LPE</td><td>Win32k.sys（Windows 内核模式驱动）</td><td>Win32k 驱动在处理上下文菜单时存在 <strong>Use-After-Free 漏洞</strong>——菜单窗口折叠后内存被释放但指针未清零，本地攻击者可借此<strong>在内核模式下运行任意代码</strong>，从普通用户提升到 SYSTEM 权限。Positive Technologies 专家发现此零日漏洞在被野外利用。CISA KEV。CVSS 7.8</td></tr><tr><td><strong>CVE-2010-3333</strong></td><td><strong>RTF 栈缓冲区溢出漏洞</strong></td><td>RTF 栈溢出 → RCE</td><td>Microsoft Office（Open XML 文件格式转换器）</td><td>Open XML 文件格式转换器在处理 RTF 的 “pFragments” 属性值时，<strong>未正确计算属性值占用的空间大小</strong>，导致栈溢出。攻击者通过构造畸形 RTF 数据即可远程执行代码。影响 Office XP&#x2F;2003&#x2F;2007&#x2F;2010 全系列。”红色十月”（Red October）APT 组织曾利用此漏洞。微软公告 MS10-087。CVSS 9.3</td></tr></tbody></table><h4 id="Adobe-Flash-漏洞"><a href="#Adobe-Flash-漏洞" class="headerlink" title="Adobe Flash 漏洞"></a>Adobe Flash 漏洞</h4><table><thead><tr><th>CVE 编号</th><th>漏洞名称 &#x2F; 别名</th><th>漏洞类型</th><th>影响版本</th><th>简要说明</th></tr></thead><tbody><tr><td><strong>CVE-2014-0515</strong></td><td><strong>Flash Pixel Bender 缓冲区溢出</strong></td><td>Pixel Bender 组件缓冲区溢出 → RCE</td><td>Flash Player 11.7 以下 &#x2F; 13.0.0.206 以下</td><td>Flash Pixel Bender 组件存在缓冲区溢出漏洞，允许攻击者<strong>远程控制受影响的系统</strong>。CVSS 10.0（极高），无需认证、可完全控制机密性&#x2F;完整性&#x2F;可用性。2014 年 4 月被野外利用。Adobe 公告 APSB14-13</td></tr><tr><td><strong>CVE-2014-4076</strong></td><td><strong>TCP&#x2F;IP 权限提升漏洞</strong></td><td>本地权限提升（LPE）</td><td>Windows Server 2003 SP2</td><td>Windows TCP&#x2F;IP 协议栈驱动（tcpip.sys&#x2F;tcpip6.sys）存在漏洞，允许本地用户通过<strong>精心构造的 IOCTL 调用</strong>提升权限。Exploit-DB 上有公开利用代码（EDB-35936、EDB-37755）。CVSS 7.2</td></tr><tr><td><strong>CVE-2014-1776</strong></td><td><strong>IE&#x2F;Flash 零日漏洞（Operation Clandestine Fox）</strong></td><td>IE VML UAF + Flash 绕过 DEP&#x2F;ASLR → RCE</td><td>Internet Explorer 6-11 + Flash Pixel Bender</td><td><strong>Windows XP 停止支持后的首个重大零日漏洞</strong>！利用 IE 中 VML（向量标记语言）对象的 Use-After-Free 漏洞，结合 Adobe Flash 的 Vector Markup Language <strong>绕过 ASLR 和 DEP 安全防护</strong>。FireEye 发现后被 APT 组织在定向攻击中使用。影响 IE 全版本（6-11），是 Windows XP 用户面临的重大威胁（微软不再为 XP 提供补丁）。卡巴斯基发现攻击源自叙利亚司法部网站挂马。CVSS 9.3</td></tr></tbody></table><blockquote><p>注：CVE-2015-3043 同时出现在 Windows 和 Flash 分类中，因其涉及 Flash 组件但也可通过系统层面利用。</p></blockquote><h4 id="Internet-Explorer-其他漏洞"><a href="#Internet-Explorer-其他漏洞" class="headerlink" title="Internet Explorer &#x2F; 其他漏洞"></a>Internet Explorer &#x2F; 其他漏洞</h4><table><thead><tr><th>CVE 编号</th><th>漏洞名称 &#x2F; 别名</th><th>漏洞类型</th><th>影响组件</th><th>简要说明</th></tr></thead><tbody><tr><td><strong>CVE-2013-3897</strong></td><td><strong>IE CDisplayPointer Use-After-Free（MS13-080）</strong></td><td>IE mshtml.dll UAF → RCE</td><td>Internet Explorer 6-11（mshtml.dll）</td><td>IE 的 CDisplayPointer 类存在 <strong>Use-After-Free 漏洞</strong>，攻击者可通过精心构造的 JavaScript 代码（利用 <code>onpropertychange</code> 事件处理器）触发内存损坏，进而远程执行代码或造成拒绝服务。<strong>2013 年 9-10 月被发现在野外有限定向攻击中使用</strong>。微软安全公告 MS13-080。US-CERT 警告 TA13-288A。CVSS 8.8</td></tr><tr><td><strong>CVE-2013-3906</strong></td><td><strong>Office TIFF 图像远程代码执行</strong></td><td>TIFF 解析 → RCE</td><td>Microsoft Office（TIFF 图像处理）</td><td>允许远程攻击者诱骗用户打开包含<strong>精心构造的 TIFF 图像</strong>的 Word 文档，从而执行任意代码。影响 Microsoft Excel Viewer、Lync、Office 全系列等多个产品。EPSS 利用概率达 99.7th 百分位。CVSS 7.8</td></tr><tr><td><strong>CVE-2013-1347</strong></td><td><strong>IE8 Use-After-Free</strong></td><td>IE8 UAF → RCE</td><td>Internet Explorer 8</td><td>IE8 存在 Use-After-Free 漏洞，允许远程攻击者在 vulnerable 系统上<strong>执行任意代码</strong>。攻击方式为诱骗用户访问恶意网站。EPSS 利用概率 99.5th 百分位。CVSS 8.8</td></tr></tbody></table><p>不难看出，这些漏洞主要集中在 <strong>Windows 系统内核</strong>、<strong>Microsoft Office 文档解析</strong>、<strong>Adobe Flash</strong> 和 <strong>Internet Explorer</strong> 四大领域。但在近年的活动中，安全研究人员发现奇幻熊正开始更多地利用 <strong>Microsoft Office 漏洞</strong>以释放第二阶段有效载荷。</p><blockquote><p>从漏洞时间线来看，奇幻熊的武器库涵盖了 2010 年至 2017 年间的多个关键漏洞，其中不乏 EternalBlue（CVE-2017-0144）、MSCOMCTL（CVE-2012-0158）、Flash UAF（CVE-2015-5119）等<strong>历史上最具破坏力的漏洞</strong>。这也印证了其作为国家级 APT 组织的资源储备和技术实力。</p></blockquote><h3 id="4-3-长久驻留技术"><a href="#4-3-长久驻留技术" class="headerlink" title="4.3 长久驻留技术"></a>4.3 长久驻留技术</h3><p>长久驻留可以通过许多方式来实现，如周期性利用远程代码执行漏洞以及借助恶意软件。</p><table><thead><tr><th>时间段</th><th>主要技术</th><th>说明</th></tr></thead><tbody><tr><td>2018 年</td><td>T1055、T1045、T1064、T1158、T1037</td><td>大多数属于”脚本”范畴，如 Logon 脚本和 JS&#x2F;WB 脚本</td></tr><tr><td>2018 年中</td><td>T1086、T1140</td><td>开始使用 PowerShell</td></tr><tr><td>2019 年初</td><td>T1221、T1204、T1045、T1047、T1112</td><td>引入更为高级的技术</td></tr></tbody></table><h3 id="4-4-典型攻击案例深度分析：2018-年罗马尼亚外交部攻击"><a href="#4-4-典型攻击案例深度分析：2018-年罗马尼亚外交部攻击" class="headerlink" title="4.4 典型攻击案例深度分析：2018 年罗马尼亚外交部攻击"></a>4.4 典型攻击案例深度分析：2018 年罗马尼亚外交部攻击</h3><p>腾讯御见威胁情报中心检测到奇幻熊利用英国信息服务提供商 IHS Markit 公司的邮箱账号向罗马尼亚外交部发送钓鱼邮件进行定向攻击。</p><p><strong>攻击流程：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">钓鱼邮件 → Excel 附件(含宏) → 启用宏 → certutil.exe 解密 PE 文件 → 执行木马</span><br><span class="line">→ 释放 dll + bat → 修改注册表实现开机自启动 → 注入浏览器进程 → C2 通信</span><br></pre></td></tr></table></figure><p><strong>关键技术细节：</strong></p><ol><li><strong>诱饵制作</strong>：邮件主题为《Upcoming Events February 2018》，附件为 Excel 表格，表格大部分内容为空，弹出”启用宏”提示</li><li><strong>白利用技术</strong>：使用微软签名程序 <code>certutil.exe</code> 对 base64 加密的 PE 文件进行解密</li><li><strong>持久化机制</strong>：利用 <code>UserInitMprLogonScript</code> 注册表键值实现开机自启动</li><li><strong>进程注入</strong>：将恶意 DLL 注入 <code>firefox.exe</code> 或 <code>iexplore.exe</code> 浏览器进程</li><li><strong>隐蔽通信</strong>：配置两个 C2 地址（一真一假，<code>google.com</code> 作为伪装），中途不断切换；读取 Firefox 配置文件获取本地代理设置</li><li><strong>木马功能</strong>：基于 Carberp 银行木马的变种，具备信息收集、文件上传下载、截屏等功能</li></ol><hr><h2 id="五、”双熊”对比一览"><a href="#五、”双熊”对比一览" class="headerlink" title="五、”双熊”对比一览"></a>五、”双熊”对比一览</h2><table><thead><tr><th>维度</th><th>舒适熊 APT29</th><th>奇幻熊 APT28</th></tr></thead><tbody><tr><td><strong>归属机构</strong></td><td>SVR（对外情报局）&#x2F; FSB</td><td>GRU（军事情报总局）第 26165 部队</td></tr><tr><td><strong>风格定位</strong></td><td>“婉约派”——隐蔽渗透、长期潜伏</td><td>“豪放派”——高调曝光、公开炫耀</td></tr><tr><td><strong>主要目标</strong></td><td>外交、情报、能源、医疗、疫苗研发</td><td>政府、军队、体育组织、选举干预</td></tr><tr><td><strong>标志性事件</strong></td><td>SolarWinds 供应链攻击、COVID-19 疫苗数据窃取</td><td>WADA 数据库入侵、DNC 邮件泄露</td></tr><tr><td><strong>攻击手段</strong></td><td>供应链攻击、DLL 劫持、高度定制化恶意软件</td><td>零日漏洞利用、钓鱼网站、公开官网发布数据</td></tr><tr><td><strong>命名来源</strong></td><td>CrowdStrike 命名</td><td>CrowdStrike 命名（Dmitri Alperovitch）</td></tr><tr><td><strong>是否建官网</strong></td><td>否</td><td><strong>是</strong>——全球罕见</td></tr><tr><td><strong>活跃时间</strong></td><td>2008 年至今</td><td>2004-2007 年至今</td></tr></tbody></table><hr><h2 id="六、总结与启示"><a href="#六、总结与启示" class="headerlink" title="六、总结与启示"></a>六、总结与启示</h2><h3 id="6-1-技术层面的启示"><a href="#6-1-技术层面的启示" class="headerlink" title="6.1 技术层面的启示"></a>6.1 技术层面的启示</h3><ol><li><strong>及时更新系统和补丁</strong>：两熊组织都大量利用已知漏洞（尤其是 Office、Flash、Windows），及时打补丁是最基本也是最有效的防御手段</li><li><strong>警惕供应链攻击</strong>：SolarWinds 事件提醒我们，软件供应链将成为未来攻击的重要切入点</li><li><strong>防范钓鱼邮件</strong>：鱼叉式网络钓鱼是两熊组织的共同首选攻击向量，用户安全意识培训至关重要</li><li><strong>零信任原则</strong>：企业应将零信任原则不仅应用于用户，还应应用于应用程序和服务器</li></ol><h3 id="6-2-战略层面的思考"><a href="#6-2-战略层面的思考" class="headerlink" title="6.2 战略层面的思考"></a>6.2 战略层面的思考</h3><ul><li><strong>归因困境</strong>：网络攻击的溯源和归因始终面临技术与政治的双重挑战，确凿证据往往难以获取</li><li><strong>大国博弈工具化</strong>：APT 组织已成为大国之间非对称对抗的重要工具，超越了传统网络安全的范畴</li><li><strong>规则挑战者</strong>：以奇幻熊为代表的组织表现出对国际规则的蔑视，试图挑战那些寻常人从未想过的底线</li></ul><h3 id="6-3-未来展望"><a href="#6-3-未来展望" class="headerlink" title="6.3 未来展望"></a>6.3 未来展望</h3><p>未来，奥运会和各国大选所面临的黑客攻击可能会越来越多。随着数字化程度的加深，关键基础设施、公共卫生系统、民主选举过程都可能成为 APT 组织的目标。</p><p>正如微软总裁所言，SolarWinds 攻击的影响短时间难以消除。而奇幻熊和舒适熊的故事，远未结束。</p><hr><p><em>本文仅供技术研究与安全科普使用，请勿用于非法用途。</em></p>]]>
    </content>
    <id>http://example.com/2026/08/14/20260814/2026081402/</id>
    <link href="http://example.com/2026/08/14/20260814/2026081402/"/>
    <published>2026-08-14T15:18:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>一提到黑客组织，往往就想到大规模、有计划、有组织的网络攻击。在深入了解这两个组织之前，我们必须先理解一个核心概念——<strong>APT</strong>。</p>
</blockquote>
<hr>
<h2 id="一、什么是-APT？"><]]>
    </summary>
    <title>黑客组织 俄罗斯&quot;双熊&quot;：APT29 舒适熊与 APT28 奇幻熊深度解析</title>
    <updated>2026-08-14T15:48:52.808Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="网络安全" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="社会工程学" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="社会工程学" scheme="http://example.com/tags/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="黑客攻击事件" scheme="http://example.com/tags/%E9%BB%91%E5%AE%A2%E6%94%BB%E5%87%BB%E4%BA%8B%E4%BB%B6/"/>
    <category term="黑灰产" scheme="http://example.com/tags/%E9%BB%91%E7%81%B0%E4%BA%A7/"/>
    <content>
      <![CDATA[<blockquote><p>2025年12月22日晚22时，国内头部短视频平台快手遭遇了一场堪称”P0级事故”的大规模网络攻击。约1.7万个僵尸账号同步开播，色情、暴力、恐怖内容如潮水般涌入直播间，单场观看量逼近10万人。平台被迫无差别关停全部直播频道，股价单日蒸发超百亿港元，最终招致1.191亿元行政罚款。</p></blockquote><p>但这不仅仅是一起”技术漏洞被利用”的安全事件。当我们把时间线摊开来看，一个令人不安的真相浮出水面：<strong>这次攻击的时间节点和攻击内容，与当时国家正在开展的”清朗”专项行动存在惊人的重合。</strong></p><p>这是巧合？还是一场精心策划的报复？</p><hr><h2 id="一、事件全景：90分钟的”至暗时刻”"><a href="#一、事件全景：90分钟的”至暗时刻”" class="headerlink" title="一、事件全景：90分钟的”至暗时刻”"></a>一、事件全景：90分钟的”至暗时刻”</h2><h3 id="1-1-攻击时间线"><a href="#1-1-攻击时间线" class="headerlink" title="1.1 攻击时间线"></a>1.1 攻击时间线</h3><table><thead><tr><th>时间</th><th>事件</th></tr></thead><tbody><tr><td><strong>18:00-20:00</strong></td><td>🔴 <strong>前序探测期</strong>：平台出现零星违规内容，处于常规风控可处理范围</td></tr><tr><td><strong>~21:30</strong></td><td>🟡 <strong>压力测试</strong>：部分用户反映验证码加载异常、视频卡顿、直播推流不稳（当时归因为”网络波动”）</td></tr><tr><td><strong>22:00</strong></td><td>💥 <strong>攻击全面爆发</strong>：约1.7万个僵尸账号同步开播，集中推送预制违规视频</td></tr><tr><td><strong>22:00-23:00</strong></td><td>🔥 <strong>系统僵持期</strong>：举报功能失效，封禁指令执行严重滞后，峰值在线人数一度达26万</td></tr><tr><td><strong>23:00-00:30</strong></td><td>🛑 <strong>应急阻断期</strong>：启动熔断机制，全量关闭直播频道，页面显示”服务器繁忙”</td></tr><tr><td><strong>00:45起</strong></td><td>🔧 <strong>服务恢复期</strong>：逐步恢复直播功能，1.7万个涉事账号全部冻结</td></tr></tbody></table><h3 id="1-2-攻击规模"><a href="#1-2-攻击规模" class="headerlink" title="1.2 攻击规模"></a>1.2 攻击规模</h3><ul><li><strong>僵尸账号数量</strong>：约17,000个</li><li><strong>攻击持续时间</strong>：60-90分钟爆发期 + 约2小时处置期</li><li><strong>峰值直播间观看量</strong>：最高接近<strong>26万</strong>（不同来源报道10万-43万不等）</li><li><strong>应急处置结果</strong>：冻结涉案账号17,000个，清理违规链接超3万条</li><li><strong>经济损失</strong>：股价下跌3.5%-6%，市值蒸发超<strong>百亿港元</strong></li><li><strong>行政处罚</strong>：2026年2月6日，北京市网信办罚款<strong>1.191亿元</strong></li></ul><h3 id="1-3-内部视角：当攻击发生时，快手内部发生了什么？"><a href="#1-3-内部视角：当攻击发生时，快手内部发生了什么？" class="headerlink" title="1.3 内部视角：当攻击发生时，快手内部发生了什么？"></a>1.3 内部视角：当攻击发生时，快手内部发生了什么？</h3><p>事后流出的内部聊天记录（下图）还原了当晚安全团队的实时状态：</p><blockquote><p><strong>23:29</strong> — “快手服务器遭到网络攻击”</p><p><strong>23:29</strong> — “<strong>已无力抵抗</strong>“</p><p><strong>23:30</strong> — “<strong>重大网络安全事故</strong>“</p><p><strong>23:30</strong> — 内部通报细节：</p><blockquote><p>“黑产团伙购买了可以绕过实名认证的’接码’服务，利用自动化脚本批量注册或盗用账号（僵尸号），然后在同一时间开启成千上万个直播。</p><p>这些直播通常质量很低，或者直接播放录屏，内容极度露骨。因为是机器操作，一旦被封禁（或者直播完），账号直接丢弃，不留痕迹。</p><p>一次性涌现1.7万个僵尸账号进行类似的涉黄涉赌广告群发……”</p></blockquote></blockquote><p><img src="/../../images/20260814/640.jpg" alt="内部聊天记录截图"></p><p><em>图：网传快手内部工作群聊天记录，记录了从发现攻击到确认”无力抵抗”的全过程</em></p><p>这段内部信息揭示了几个关键事实：</p><ol><li><strong>安全团队在攻击爆发后约30分钟即判断”已无力抵抗”</strong>——说明攻击强度远超预案覆盖范围</li><li><strong>确认了”接码服务绕过实名认证”的攻击路径</strong>——与后续安全专家的技术分析完全吻合</li><li><strong>账号是”一次性”使用的</strong>——被封即弃，不留痕迹，极大增加了溯源难度</li><li><strong>内容以”录屏”为主</strong>——预制视频而非实时拍摄，进一步印证了有预谋的工业化操作</li></ol><hr><h2 id="二、攻击手法拆解：低成本如何击穿高投入防线？"><a href="#二、攻击手法拆解：低成本如何击穿高投入防线？" class="headerlink" title="二、攻击手法拆解：低成本如何击穿高投入防线？"></a>二、攻击手法拆解：低成本如何击穿高投入防线？</h2><p>这不是一群黑客在地下室里敲键盘的传统攻击。安全机构火绒安全的复盘报告指出：这是一次具有里程碑意义的<strong>业务逻辑DDoS攻击</strong>。</p><h3 id="2-1-三步攻击链路"><a href="#2-1-三步攻击链路" class="headerlink" title="2.1 三步攻击链路"></a>2.1 三步攻击链路</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">第一步：批量&quot;造号&quot;</span><br><span class="line">├── 第三方接码平台获取虚拟手机号 + 猫池设备</span><br><span class="line">├── 批量注册新账号 + 绕过实名认证（成本：几毛钱~几块钱/个）</span><br><span class="line">└── 劫持闲置&quot;僵尸账号&quot;开通直播权限</span><br><span class="line">        ↓</span><br><span class="line">第二步：隐身突破</span><br><span class="line">├── 模拟正常用户行为提升账号权重</span><br><span class="line">├── 预制违规录屏视频（非实时拍摄）</span><br><span class="line">└── 利用直播推流接口底层漏洞绕过审核链路</span><br><span class="line">        ↓</span><br><span class="line">第三步：同步突袭</span><br><span class="line">├── 选定22:00晚间流量高峰（+ 人力审核交接班时段）</span><br><span class="line">├── &quot;指挥中心&quot;同步指令，1.7万账号毫秒级开播</span><br><span class="line">└── 饱和式冲击压垮人工+AI审核系统</span><br></pre></td></tr></table></figure><h3 id="2-2-实名认证是如何被绕过的？"><a href="#2-2-实名认证是如何被绕过的？" class="headerlink" title="2.2 实名认证是如何被绕过的？"></a>2.2 实名认证是如何被绕过的？</h3><p>这是最关键也最令人不安的一环。要理解攻击者如何批量获取1.7万个”已实名”账号，我们需要深入解剖黑灰产的基础设施——<strong>接码平台</strong>和<strong>猫池</strong>。</p><h4 id="什么是”猫池”？（GoIP-GSM-Modem-Pool）"><a href="#什么是”猫池”？（GoIP-GSM-Modem-Pool）" class="headerlink" title="什么是”猫池”？（GoIP &#x2F; GSM Modem Pool）"></a>什么是”猫池”？（GoIP &#x2F; GSM Modem Pool）</h4><p><strong>猫池（Modem Pool）</strong>，业内也叫 GoIP 设备或 GSM 网关，是一种可以同时插入多张手机SIM卡的硬件设备。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">┌─────────────────────────────────────┐</span><br><span class="line">│            一台 &quot;猫池&quot; 设备          │</span><br><span class="line">│                                     │</span><br><span class="line">│  ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐    │</span><br><span class="line">│  │SIM│ │SIM│ │SIM│ │SIM│ │SIM│ ← 8-256张SIM卡</span><br><span class="line">│  │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │    │</span><br><span class="line">│  └───┘ └───┘ └───┘ └───┘ └───┘    │</span><br><span class="line">│                                     │</span><br><span class="line">│  ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐    │</span><br><span class="line">│  │SIM│ │SIM│ │SIM│ │SIM│ │SIM│    │</span><br><span class="line">│  │ 6 │ │ 7 │ │ 8 │ │ 9 │ │10 │    │</span><br><span class="line">│  └───┘ └───┘ └───┘ └───┘ └───┘    │</span><br><span class="line">│             ↓↓↓                     │</span><br><span class="line">│      [ USB/网口 → 电脑 ]            │</span><br><span class="line">│         (统一控制软件)               │</span><br><span class="line">└─────────────────────────────────────┘</span><br></pre></td></tr></table></figure><p><strong>核心特点：</strong></p><table><thead><tr><th>参数</th><th>说明</th></tr></thead><tbody><tr><td><strong>端口数量</strong></td><td>常见规格：8口、16口、32口、64口、128口、256口</td></tr><tr><td><strong>设备价格</strong></td><td>8口约650元，16口约900元，高端口数可达数千元</td></tr><tr><td><strong>功能</strong></td><td>可同时拨打电话、接收&#x2F;发送短信、上网跑流量</td></tr><tr><td><strong>支持制式</strong></td><td>2G&#x2F;3G&#x2F;4G，分移动&#x2F;联通&#x2F;电信版本</td></tr><tr><td><strong>管理方式</strong></td><td>通过PC端软件统一控制所有SIM卡，可编程自动化操作</td></tr></tbody></table><p><strong>通俗理解：</strong> 猫池就像一个”N卡N待”的超级手机。普通手机只能插1张卡，而一台256口的猫池可以同时让<strong>256个手机号在线工作</strong>。插上卡、连上电脑，配合自动化脚本，一个人就能操控数百甚至数千个手机号。</p><h4 id="什么是”接码平台”？"><a href="#什么是”接码平台”？" class="headerlink" title="什么是”接码平台”？"></a>什么是”接码平台”？</h4><p><strong>接码平台</strong>是建立在猫池之上的<strong>短信验证码分发服务平台</strong>。它的工作原理如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">        黑灰产攻击者的完整&quot;造号&quot;流程</span><br><span class="line"></span><br><span class="line">   攻击者电脑                    接码平台                    卡商机房</span><br><span class="line">┌──────────────┐           ┌──────────────┐          ┌──────────────┐</span><br><span class="line">│  自动化脚本   │  ①请求号码 │              │  ③分配  │              │</span><br><span class="line">│  (&quot;我要注册   │ ────────→ │   接码平台    │ ←────── │  猫池设备群  │</span><br><span class="line">│   快手账号&quot;)  │           │  (云端中介)   │          │ (数百台猫池) │</span><br><span class="line">│              │  ②返回号码 │              │  ④接收   │   数万张SIM卡 │</span><br><span class="line">│  用该号码     │ ←──────── │              │ ───────→ │              │</span><br><span class="line">│  注册快手账号 │           │              │  验证码   │              │</span><br><span class="line">│              │  ⑤输入验证码│             │          │              │</span><br><span class="line">│  收到验证码   │ ←──────── │  转发验证码   │ ←─────── │              │</span><br><span class="line">│  完成注册 ✓   │           │              │          │              │</span><br><span class="line">└──────────────┘           └──────────────┘          └──────────────┘</span><br><span class="line">       ↑                           ↑                         ↑</span><br><span class="line">       │                           │                         │</span><br><span class="line">       └──── 整个过程 3-5 秒全自动 ──┴─────────────────────────┘</span><br></pre></td></tr></table></figure><p><strong>具体运作方式：</strong></p><ol><li>攻击者在接码平台上选择项目（如”快手注册”）</li><li>平台从连接的猫池中<strong>分配一个可用手机号</strong></li><li>攻击者用这个号码在快手注册，快手发送验证码到该手机号</li><li>猫池自动接收短信验证码</li><li>接码平台将验证码实时显示给攻击者（或通过API自动回传）</li><li>攻击者输入验证码，<strong>注册完成</strong></li><li>该号码被标记为”已使用”，下次分配新号码</li></ol><p><strong>整个过程3-5秒，全自动化，无需人工干预。</strong></p><h4 id="手机卡从哪来？"><a href="#手机卡从哪来？" class="headerlink" title="手机卡从哪来？"></a>手机卡从哪来？</h4><p>这是整个产业链中最灰色的环节。手机卡的来源主要有以下几种：</p><table><thead><tr><th>卡类型</th><th>来源</th><th>实名状态</th><th>单价</th><th>特点</th></tr></thead><tbody><tr><td><strong>三大运营商卡</strong></td><td>卡商组织人员批量办卡 &#x2F; 非法购买公民信息</td><td>已实名（真假难辨）</td><td>25-30元&#x2F;张</td><td>最稳定，但风险最高</td></tr><tr><td><strong>虚拟运营商卡</strong></td><td>165&#x2F;170&#x2F;171等号段</td><td>需实名（部分可绕过）</td><td>3.5-5元&#x2F;张</td><td>便宜量大，”注册养号必备神器”</td></tr><tr><td><strong>物联网卡</strong></td><td>企业名义批量办理</td><td>企业实名</td><td>更低</td><td>近年来多数已不支持收短信，使用率下降</td></tr><tr><td><strong>境外手机卡</strong></td><td>境外购买</td><td>仅需营业执照</td><td>不等</td><td>无需国内实名，但部分平台不认</td></tr></tbody></table><blockquote><p>广州市公安局网警支队的说明：”有的卡商直接组织他人到运营商办卡，有的可能非法购买公民个人信息。”</p></blockquote><p><strong>成本核算：</strong></p><p>以本次快手的1.7万个账号为例：</p><ul><li>按虚拟运营商卡计算：17,000 × 5元 &#x3D; <strong>8.5万元</strong>（低成本方案）</li><li>按三大运营商卡计算：17,000 × 25元 &#x3D; <strong>42.5万元</strong>（高质量方案）</li><li>加上猫池设备、接码平台服务费、脚本开发等：总成本预估在<strong>百万元级别</strong></li></ul><p><strong>对比：</strong> 快手2025年Q3研发投入约<strong>37亿元</strong>。</p><h4 id="为什么平台拦不住？"><a href="#为什么平台拦不住？" class="headerlink" title="为什么平台拦不住？"></a>为什么平台拦不住？</h4><p>平台通过短信验证码与运营商建立信任关系——验证码正确，就默认通过了实名认证。但正如网络安全顾问吴崖斌指出的：</p><blockquote><p><strong>“平台不是公安系统，它只能验证’这个号码是可用的’，开放直播权限。但手机号究竟是正常用户、不知情的信息被盗者，还是被批量操控的黑卡或虚拟号码，却不容易区分。”</strong></p></blockquote><p>这里存在一个<strong>根本性的信任盲区</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">平台看到的验证链路：</span><br><span class="line">用户手机号 → 运营商(已实名?) → 发送验证码 → 验证码正确 → ✅ 通过!</span><br><span class="line"></span><br><span class="line">实际可能的真相：</span><br><span class="line">猫池SIM卡 → 运营商(可能是假实名/批量实名) → 猫池自动接收 → 脚本自动填入 → ✅ &quot;通过&quot;</span><br><span class="line">                                                                    ↑</span><br><span class="line">                                                        平台完全无法感知差异</span><br></pre></td></tr></table></figure><p>换句话说：<strong>平台看到的是”运营商已实名”，而不知道这个”实名”背后站着的是真人还是猫池。</strong> 平台的实名验证本质上是<strong>对运营商的信任传递</strong>，而这个传递链条在猫池面前断裂了。</p><h4 id="这个产业链有多大？"><a href="#这个产业链有多大？" class="headerlink" title="这个产业链有多大？"></a>这个产业链有多大？</h4><p>根据公开报道的案件数据：</p><table><thead><tr><th>案件</th><th>规模</th></tr></thead><tbody><tr><td><strong>爱码案件</strong>（全国最大验证码平台）</td><td>绍兴警方查获<strong>700多万张</strong>手机SIM卡</td></tr><tr><td><strong>接码平台覆盖范围</strong></td><td>可提供<strong>9000多个网站项目</strong>的验证码接收服务</td></tr><tr><td><strong>行业增速</strong></td><td>阿里安全部追踪发现，一年间接码服务量翻了<strong>3倍</strong></td></tr><tr><td><strong>用途分布</strong></td><td>约<strong>70%用于诈骗</strong>，15-20%用于”撸羊毛”，其余为注册&#x2F;刷量</td></tr></tbody></table><blockquote><p>阿里巴巴集团安全部高级专家璃珞的判断：”七成是用这些手机号生成的账号用来欺诈，就是骗普通消费者。”</p></blockquote><p><strong>这意味着：</strong> 接码平台+猫池不仅是快手事件的作案工具，更是支撑着整个网络黑灰产业的<strong>基础设施级服务</strong>。从电信诈骗、盗号撞库、薅羊毛到这次针对快手的规模化攻击，底层用的是同一套体系。</p><h3 id="2-3-内容审核为何失灵？"><a href="#2-3-内容审核为何失灵？" class="headerlink" title="2.3 内容审核为何失灵？"></a>2.3 内容审核为何失灵？</h3><p>快手在2025年9月曾公开分享其安全大模型技术，宣称对海量内容审核已经从”人力标注”转向”自动化标注+人工辅助校验”。但在这次事件中，这套体系却全面溃败。</p><p>吴崖斌的分析指出了三个层面的原因：</p><h4 id="原因一：系统性超载"><a href="#原因一：系统性超载" class="headerlink" title="原因一：系统性超载"></a>原因一：系统性超载</h4><blockquote><p>“和图文审核不同，直播审核是一项实时、大流量、高负荷的任务：视频流量远高于图文内容，每一秒都包含图像、语音、文本等多重信息，需要实时抽帧、识别、分析。对机器来说，负荷是不一样的。短时间内涌入大量的直播用户，那么多疑似违规的直播，AI也需要时间的，也是有上限的。”</p></blockquote><h4 id="原因二：”可用性”优先于”安全性”"><a href="#原因二：”可用性”优先于”安全性”" class="headerlink" title="原因二：”可用性”优先于”安全性”"></a>原因二：”可用性”优先于”安全性”</h4><p>这是更深层的管理逻辑问题：</p><blockquote><p>“对平台而言，首要目标是保证业务正常运行不中断，毕竟平台上还有大量正常用户在使用直播。因此在异常初现端倪时，平台往往会选择先维持直播可用，再逐步清理风险内容。当异常规模远超预期，平台最终’关停直播’，是通过牺牲可用性，换取整体风险可控。”</p></blockquote><p><strong>翻译一下</strong>：平台一开始不想关直播（怕影响正常用户体验），试图边运行边清理，结果发现清理速度远远跟不上新增速度，最后才被迫”一刀切”。这个决策延迟让违规内容多传播了至少30-60分钟。</p><h4 id="原因三：人工审核同样面临极限"><a href="#原因三：人工审核同样面临极限" class="headerlink" title="原因三：人工审核同样面临极限"></a>原因三：人工审核同样面临极限</h4><blockquote><p>“面对成千上万路直播流，有限的人工审核员也无法在极短时间内逐一处置。最后可能就是系统忙不过来，人也忙不过来。”</p></blockquote><h3 id="2-4-更狠的一招：干扰防御通道"><a href="#2-4-更狠的一招：干扰防御通道" class="headerlink" title="2.4 更狠的一招：干扰防御通道"></a>2.4 更狠的一招：干扰防御通道</h3><p>攻击者在投放违规内容的同时，还对平台的<strong>举报&#x2F;封禁接口发起了高频请求</strong>，导致：</p><ul><li>用户<strong>举报按钮点击无响应</strong></li><li>后台<strong>封禁指令执行严重滞后</strong></li><li>违规直播间平均要<strong>15-20分钟</strong>才会被下架</li></ul><p>而新增违规内容的速度远超这个数字。这就是专家说的**”能发现违规、难执行封禁”的逻辑瘫痪**。</p><h3 id="2-5-成本对比：百万元攻击-vs-三十七亿防御"><a href="#2-5-成本对比：百万元攻击-vs-三十七亿防御" class="headerlink" title="2.5 成本对比：百万元攻击 vs 三十七亿防御"></a>2.5 成本对比：百万元攻击 vs 三十七亿防御</h3><p>据业内人士估算：</p><table><thead><tr><th>项目</th><th>金额</th></tr></thead><tbody><tr><td><strong>攻击成本估算</strong></td><td>约<strong>百万元级别</strong>（1.7万个账号 × 几元&#x2F;个 + 工具开发）</td></tr><tr><td><strong>快手2025Q3研发投入</strong></td><td>约<strong>37亿元人民币</strong></td></tr><tr><td><strong>研发占营收比</strong></td><td>约<strong>10%</strong>（连续三年未明显上升）</td></tr></tbody></table><p><strong>百万元击穿了三十七亿的防线。</strong></p><p>这组数据揭示了一个残酷的现实：<strong>攻击的成本在指数级下降，而防御的成本在指数级上升。</strong> 更值得注意的是，快手的研发投入已连续三年下降，当资源被分散至多条业务线时，安全这道”看不见的防线”往往最容易被忽视。</p><hr><h2 id="三、🎯-核心问题：为什么说这不是巧合？"><a href="#三、🎯-核心问题：为什么说这不是巧合？" class="headerlink" title="三、🎯 核心问题：为什么说这不是巧合？"></a>三、🎯 核心问题：为什么说这不是巧合？</h2><h3 id="3-1-时间线重合度分析"><a href="#3-1-时间线重合度分析" class="headerlink" title="3.1 时间线重合度分析"></a>3.1 时间线重合度分析</h3><p>让我们把<strong>攻击时间线</strong>和<strong>监管整治时间线</strong>放在一起看：</p><h4 id="国家”清朗”行动高压期（2025年下半年）"><a href="#国家”清朗”行动高压期（2025年下半年）" class="headerlink" title="国家”清朗”行动高压期（2025年下半年）"></a>国家”清朗”行动高压期（2025年下半年）</h4><table><thead><tr><th>时间</th><th>监管动作</th><th>力度</th></tr></thead><tbody><tr><td><strong>2025.9</strong></td><td>网信办约谈快手——热搜榜泛娱乐化、炒作明星琐事</td><td>⚠️ 直接针对快手</td></tr><tr><td><strong>2025.9</strong></td><td>成都快购（快手生态企业）因电商违法被立案调查</td><td>⚠️ 快手系</td></tr><tr><td><strong>2025.10.28</strong></td><td>中央网信办启动**”清朗·整治网络直播打赏乱象”专项行动**</td><td>🔴 全行业高压</td></tr><tr><td><strong>2025.11.26</strong></td><td>网信办通报一批<strong>低俗团播典型案例</strong>，永久封禁大量账号</td><td>🔴 重点打击”密码房福利””送礼解锁隐藏内容””低俗擦边”</td></tr><tr><td><strong>2025.12.3</strong></td><td>发布整治成果：<strong>处置违规直播间7.3万余个、账号2.4万余个</strong></td><td>🔴 史诗级清理</td></tr><tr><td><strong>2025.12.22</strong></td><td>⚡️ <strong>快手遭大规模攻击</strong></td><td>——</td></tr></tbody></table><p><strong>关键发现：</strong></p><blockquote><p><strong>发现一：攻击发生在”清朗”行动最密集的月份。</strong></p><p>2025年10-12月是”清朗·整治网络直播打赏乱象”专项行动的核心执行期。短短两个月内，全行业清理了<strong>7.3万个违规直播间、2.4万个账号</strong>。而快手作为头部平台，正是重点监管对象——9月刚被约谈，11月生态企业被罚。</p><p>黑灰产选择在<strong>12月下旬</strong>动手，此时恰好是”清朗”行动第一波整治成果发布后不久，全行业风控最紧、黑灰产利益受损最严重的时期。</p></blockquote><blockquote><p><strong>发现二：攻击内容与整治目标高度对应。</strong></p><p>“清朗”行动重点打击的是什么？</p><ul><li>❌ 色情低俗内容</li><li>❌ 暴力恐怖画面</li><li>❌ “密码房福利””送礼解锁隐藏内容”</li><li>❌ 低俗团播引诱打赏</li></ul><p>黑灰产在快手上投放的是什么？</p><ul><li>✅ 色情视频</li><li>✅ 暴力内容</li><li>✅ 恐怖画面</li><li>✅ “密码房福利”等诱导付费话术</li></ul><p><strong>每一项都精准命中整治清单。</strong> 这不是随机选择的违规类型，而是对着监管的”打击目录”逐条投放。</p></blockquote><h3 id="3-2-先兆中的预警信号"><a href="#3-2-先兆中的预警信号" class="headerlink" title="3.2 先兆中的预警信号"></a>3.2 先兆中的预警信号</h3><p>事后复盘显示，攻击前已有明显异常，但全部被忽略：</p><table><thead><tr><th>时间</th><th>异常信号</th><th>当时解读</th><th>实际性质</th></tr></thead><tbody><tr><td><strong>18:00-20:00</strong></td><td>零星违规内容出现</td><td>常规风控范围</td><td><strong>前序探测——测试风控阈值</strong></td></tr><tr><td><strong>~21:30</strong></td><td>登录验证码加载异常&#x2F;慢</td><td>网络波动</td><td><strong>压力测试——探测服务器负载</strong></td></tr><tr><td><strong>~21:30</strong></td><td>短视频播放卡顿</td><td>网络问题</td><td><strong>探测CDN分发状态</strong></td></tr><tr><td><strong>~21:30</strong></td><td>直播推流不稳定</td><td>偶发故障</td><td><strong>探测直播接口可用性</strong></td></tr></tbody></table><p>这些异常持续了<strong>30-90分钟</strong>，但无人将其关联为攻击前奏。安全专家指出：<strong>警报响了，但没人听懂它在说什么。</strong></p><hr><h2 id="四、动机推测：报复？示威？还是商业测试？"><a href="#四、动机推测：报复？示威？还是商业测试？" class="headerlink" title="四、动机推测：报复？示威？还是商业测试？"></a>四、动机推测：报复？示威？还是商业测试？</h2><p>截至目前，官方未公布攻击者身份或破案结果。但结合时间线和攻击特征，业界形成了几种主流推测：</p><h3 id="推测A：报复性攻击（Revenge-Attack）"><a href="#推测A：报复性攻击（Revenge-Attack）" class="headerlink" title="推测A：报复性攻击（Revenge Attack）"></a>推测A：报复性攻击（Revenge Attack）</h3><p><strong>逻辑链：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&quot;清朗&quot;行动大规模封禁黑灰产账号（7.3万直播间）</span><br><span class="line">        ↓</span><br><span class="line">黑灰产业链利益遭受重创</span><br><span class="line">        ↓</span><br><span class="line">选择年底对头部平台实施报复</span><br><span class="line">        ↓</span><br><span class="line">用监管最敏感的内容类型（色情暴力）制造最大舆论震动</span><br><span class="line">        ↓</span><br><span class="line">向监管层传递信号：&quot;你们封不住我们&quot;</span><br></pre></td></tr></table></figure><p><strong>支持证据：</strong></p><ul><li>时间点恰好在整治成果发布后不久</li><li>攻击内容100%对应整治目标</li><li>专家判断”奔着惊爆效果去的”</li></ul><h3 id="推测B：商业测试-实战演练（Live-Fire-Exercise）"><a href="#推测B：商业测试-实战演练（Live-Fire-Exercise）" class="headerlink" title="推测B：商业测试&#x2F;实战演练（Live Fire Exercise）"></a>推测B：商业测试&#x2F;实战演练（Live Fire Exercise）</h3><p><strong>逻辑链：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">黑灰产开发了一套新的自动化攻击工具链</span><br><span class="line">        ↓</span><br><span class="line">需要找一个真实目标做&quot;实战测试&quot;</span><br><span class="line">        ↓</span><br><span class="line">选在监管高压期攻击 → 即使被发现也可甩锅给&quot;报复&quot;</span><br><span class="line">        ↓</span><br><span class="line">同时测试：自动化注册能力、AI对抗审核能力、接口漏洞利用能力</span><br><span class="line">        ↓</span><br><span class="line">收集数据优化工具，为后续规模化牟利铺路</span><br></pre></td></tr></table></figure><p><strong>支持证据：</strong></p><ul><li>攻击手法高度工业化、自动化（非情绪化犯罪特征）</li><li>事前有完整的探测-测试-爆发流程（像军事行动而非泄愤）</li><li>利用事件测试了多项技术能力</li></ul><h3 id="推测C：复合动机（最可能）"><a href="#推测C：复合动机（最可能）" class="headerlink" title="推测C：复合动机（最可能）"></a>推测C：复合动机（最可能）</h3><p><strong>现实情况可能是A+B的结合体：</strong></p><blockquote><p>这不是单一动机的犯罪，而是一次<strong>多重目的的复合攻击</strong>：</p><ul><li>对”清朗”行动的<strong>反弹和示威</strong>（政治&#x2F;情绪层面）</li><li>自动化武器库的<strong>实战检验</strong>（技术&#x2F;商业层面）</li><li>向全行业展示**”我们仍然可以击穿你们”**的能力（战略层面）</li></ul></blockquote><p>安全专家漏洞银行CTO张雪松的评价佐证了这一点：”这是一次有组织、高度专业的攻击，绝不可能是新手’一不小心’造成的。”</p><p>从业10余年的网络安全专家朱传江则提供了另一个视角的佐证：</p><blockquote><p><strong>“过去，黑灰产多在暗处活动，而我从未听闻如此高调的攻击行动，正常来说都不会这么明目张胆。”</strong></p></blockquote><p>这种”反常的高调”本身就是一个重要信号——<strong>攻击者想要的不仅仅是”攻进去”，更是”让所有人知道我攻进去了”。</strong></p><hr><h2 id="五、暴露的行业痛点"><a href="#五、暴露的行业痛点" class="headerlink" title="五、暴露的行业痛点"></a>五、暴露的行业痛点</h2><h3 id="5-1-平台层面"><a href="#5-1-平台层面" class="headerlink" title="5.1 平台层面"></a>5.1 平台层面</h3><table><thead><tr><th>短板</th><th>具体表现</th></tr></thead><tbody><tr><td><strong>认证机制漏洞</strong></td><td>实名认证被”接码平台+猫池”绕过，几元&#x2F;个的账号即可开播</td></tr><tr><td><strong>AI审核延迟</strong></td><td>异步审核模式导致”已扩散才检测到”的时间差；面对饱和攻击时系统过载</td></tr><tr><td><strong>应急决策失误</strong></td><td>优先保”可用性”导致处置延迟30-60分钟，最终仍需”一刀切”关停</td></tr><tr><td><strong>信息沟通滞后</strong></td><td>攻击爆发到首次回应间隔超过2小时，谣言滋生</td></tr></tbody></table><h3 id="5-2-监管层面"><a href="#5-2-监管层面" class="headerlink" title="5.2 监管层面"></a>5.2 监管层面</h3><table><thead><tr><th>问题</th><th>说明</th></tr></thead><tbody><tr><td><strong>“通知-删除”模式过时</strong></td><td>法律仍以被动响应为主，但自动化攻击要求”即时处置”</td></tr><tr><td><strong>平台不能免责</strong></td><td>监管明确指出：即便遭受恶意攻击，也不能免除内容安全主体责任</td></tr><tr><td><strong>新规即将落地</strong></td><td>《直播电商监督管理办法》于2026年2月1日实施，要求建立全流程管理体系</td></tr><tr><td><strong>溯源追责困难</strong></td><td>截至2026年8月，攻击者身份仍未公布</td></tr></tbody></table><p>监管部门认定快手主要存在三方面问题：</p><ol><li><strong>未充分履行网络安全保护责任</strong></li><li><strong>已发现的技术漏洞未及时修复</strong></li><li><strong>对违规内容未能第一时间采取删除等措施</strong></li></ol><h3 id="5-3-行业共识：三分技术，七分管理"><a href="#5-3-行业共识：三分技术，七分管理" class="headerlink" title="5.3 行业共识：三分技术，七分管理"></a>5.3 行业共识：三分技术，七分管理</h3><p>此次事件推动了一个核心认知转变：</p><blockquote><p><strong>“流量优先”的时代结束了。当黑灰产已经用上自动化武器，任何平台都不能再抱有侥幸心理。”</strong></p></blockquote><p>安全行业内部有一句警言：<strong>“网络安全，三分靠技术，七分靠管理。”</strong></p><p>这里的”管理”包括：</p><ul><li>权限控制与流程设计</li><li>应急预案与定期演练</li><li>人员制度与跨部门协同</li><li><strong>研发资源的优先级排序</strong>（快手研发投入连续三年下降，值得反思）</li></ul><p>未来方向明确：</p><ol><li><strong>AI对抗AI</strong> — 用智能模型实时监测异常流量</li><li><strong>治理前移</strong> — 从被动响应转向主动预警</li><li><strong>熔断机制</strong> — 异常激增时自动暂停相关功能</li><li><strong>定期攻防演练</strong> — 自己攻击自己，提前发现盲区</li></ol><hr><h2 id="六、辟谣与澄清"><a href="#六、辟谣与澄清" class="headerlink" title="六、辟谣与澄清"></a>六、辟谣与澄清</h2><h3 id="关于”病毒链接盗号”传言"><a href="#关于”病毒链接盗号”传言" class="headerlink" title="关于”病毒链接盗号”传言"></a>关于”病毒链接盗号”传言</h3><p>事件发生后，有传言称”违规直播间中隐藏着病毒链接，许多用户点入后微信账号即被盗取”。</p><p><strong>2025年12月23日，微信官方发文辟谣：</strong></p><blockquote><p>“经核实，上述信息不属实。微信账号有严格的安全保护机制，截至目前，我们没有发现相关问题和收到类似反馈。”</p></blockquote><p>这一点需要在复盘中被明确记录——<strong>恐慌本身也是攻击的一部分</strong>，谣言的快速传播放大了事件的破坏力。</p><hr><h2 id="七、更大的背景：中国网络安全态势"><a href="#七、更大的背景：中国网络安全态势" class="headerlink" title="七、更大的背景：中国网络安全态势"></a>七、更大的背景：中国网络安全态势</h2><p>快手事件并非孤例。根据国家互联网应急中心（CNCERT）监测数据：</p><table><thead><tr><th>威胁类型</th><th>2024年数据</th></tr></thead><tbody><tr><td>境外APT组织攻击</td><td>超<strong>600起</strong>，针对党政机关、高校、科研院所、重点企业</td></tr><tr><td>恶意软件日均传播</td><td>约<strong>349万余次</strong></td></tr><tr><td>新增勒索软件团伙</td><td>超<strong>40个</strong></td></tr><tr><td>已披露勒索攻击事件</td><td>超<strong>5700起</strong></td></tr></tbody></table><p>奇安信《2024年度网络安全威胁报告》指出：</p><blockquote><p>“攻击者积极挖掘新攻击面并更新技战术，利用AI生成内容、快速开发漏洞利用代码，<strong>攻击门槛持续降低，而防御成本不断上升</strong>。尽管有些威胁组织我们清楚攻击者的目的和所在地区，但目前无法归属到背后具体的攻击实体。”</p></blockquote><p>快手12·22事件正是这一大趋势的缩影——<strong>攻击越来越便宜、越来越自动化、越来越难溯源；而防御越来越贵、越来越复杂、永远追不上。</strong></p><hr><h2 id="八、最终结局与启示"><a href="#八、最终结局与启示" class="headerlink" title="八、最终结局与启示"></a>八、最终结局与启示</h2><h3 id="8-1-处罚结果"><a href="#8-1-处罚结果" class="headerlink" title="8.1 处罚结果"></a>8.1 处罚结果</h3><table><thead><tr><th>维度</th><th>结果</th></tr></thead><tbody><tr><td>行政处罚</td><td>罚款<strong>1.191亿元</strong> + 警告 + 责令整改 + 从严处理责任人</td></tr><tr><td>刑事侦查</td><td>北京海淀警方介入，<strong>破案结果尚未公布</strong></td></tr><tr><td>快手回应</td><td>《致歉声明》：”诚恳接受，坚决整改。由于公司技术管理原因，应急处置不及时，导致平台出现大量色情低俗内容，造成恶劣影响。我们深感自责、深表歉意。”</td></tr><tr><td>整改措施</td><td>紧急招募安全专家、升级AI审核系统、加固认证机制</td></tr></tbody></table><h3 id="8-2-后续法规影响"><a href="#8-2-后续法规影响" class="headerlink" title="8.2 后续法规影响"></a>8.2 后续法规影响</h3><p><strong>《直播电商监督管理办法》于2026年2月1日正式实施</strong>，要求平台建立覆盖主播审核、内容监控、违规处置的全流程管理体系。合规不再是临时应付，而是必须内嵌进日常运营的制度性要求。</p><h3 id="8-3-对普通用户的警示"><a href="#8-3-对普通用户的警示" class="headerlink" title="8.3 对普通用户的警示"></a>8.3 对普通用户的警示</h3><ul><li>✅ 开启账号二次验证 &#x2F; 设备锁</li><li>✅ 定期修改密码，清理历史登录设备</li><li>✅ 取消第三方应用的可疑授权</li><li>✅ 不点击不明链接（<strong>本次事件中病毒链接传言已被微信官方辟谣</strong>，但仍应保持警惕）</li><li>✅ 不泄露验证码给任何人</li><li>✅ 遇到违规内容留存截图证据</li></ul><h3 id="8-4-最后的话"><a href="#8-4-最后的话" class="headerlink" title="8.4 最后的话"></a>8.4 最后的话</h3><p>快手12·22事件看似是一次意外的审核失控，实则是<strong>行业发展到了某个阶段的必然考验</strong>。</p><p>当我们把所有线索拼在一起——<strong>时间的巧合、内容的对应、手法的专业、先兆的被忽略、内部的”无力抵抗”</strong>——这幅图景指向一个令人不安的结论：</p><blockquote><p><strong>这很可能不是一次随机的技术攻击，而是黑灰产业链对国家”清朗”专项行动的一次有组织反击。</strong></p></blockquote><p>它同时暴露了一个更深层的结构性矛盾：</p><blockquote><p><strong>纯靠技术手段想彻底’打穿’头部平台，其实不是那么容易的，基本上可能性很小。许多攻击之所以最终造成破坏，是技术威力与管理漏洞的叠加。</strong> —— 吴崖斌，网络安全顾问</p></blockquote><p>在网络空间的安全博弈中，攻击者和防御者都在进化。区别在于——<strong>攻击者只需要成功一次，而防御者必须每次都成功。</strong></p><p>而这一次，防御方输了。但输的方式告诉我们：<strong>问题不只出在技术上，更出在管理上、资源分配上、以及对”看不见的防线”的重视程度上。</strong></p><p>只有将内容安全置于业务发展的优先位置，把风险防控落实到产品设计、技术架构与运营流程的每一个环节，才能避免下一个”12·22”的到来。</p><hr><p><em>本文基于公开资料整理撰写，截至2026年8月14日。事件后续进展请以官方通报为准。</em></p>]]>
    </content>
    <id>http://example.com/2026/08/14/20260814/2026081401/</id>
    <link href="http://example.com/2026/08/14/20260814/2026081401/"/>
    <published>2026-08-14T14:48:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>2025年12月22日晚22时，国内头部短视频平台快手遭遇了一场堪称”P0级事故”的大规模网络攻击。约1.7万个僵尸账号同步开播，色情、暴力、恐怖内容如潮水般涌入直播间，单场观看量逼近10万人。平台被迫无差别关停全部直播频道，股价单日蒸发超百亿港元]]>
    </summary>
    <title>快手12·22攻击事件深度复盘：当黑灰产撞上&quot;清朗&quot;行动——一场精心策划的报复？</title>
    <updated>2026-08-14T15:48:52.689Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="网络安全" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="社会工程学" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="社会工程学" scheme="http://example.com/tags/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="黑客组织" scheme="http://example.com/tags/%E9%BB%91%E5%AE%A2%E7%BB%84%E7%BB%87/"/>
    <content>
      <![CDATA[<blockquote><p>从 Colonial Pipeline 到 Change Healthcare，一个黑客帝国的两世轮回<br>从无名之辈到撼动美国能源命脉——勒索软件帝国的兴衰史</p></blockquote><p><strong>发布时间：2026年8月11日 | 阅读时长：约15分钟 | 标签：网络安全 &#x2F; 勒索软件 &#x2F; APT分析</strong></p><hr><blockquote><p>“我们是非政治化的，不参与地缘政治……我们的目标是赚钱，而不是给社会制造问题。” —— DarkSide 组织声明（2021年5月）</p></blockquote><p>这句话出自一个在不到一年时间内勒索超过<strong>9000万美元</strong>、攻击了美国最大燃油管道运营商的黑客组织之口。而它的”继任者”——BlackCat（又名 ALPHV）——在接下来的三年里，将把勒索软件的破坏力推向新的高度，从拉斯维加斯赌场到美国医疗支付系统，无一幸免。</p><p>这不是两个独立的故事。这是<strong>同一个犯罪帝国在不同面具下的连续叙事</strong>。</p><hr><h2 id="一、第一幕：DarkSide-的崛起"><a href="#一、第一幕：DarkSide-的崛起" class="headerlink" title="一、第一幕：DarkSide 的崛起"></a>一、第一幕：DarkSide 的崛起</h2><h3 id="1-1-组织概况"><a href="#1-1-组织概况" class="headerlink" title="1.1 组织概况"></a>1.1 组织概况</h3><table><thead><tr><th>属性</th><th>信息</th></tr></thead><tbody><tr><td><strong>组织名称</strong></td><td>DarkSide（黑暗面）</td></tr><tr><td><strong>组织性质</strong></td><td>网络犯罪黑客组织 &#x2F; 勒索软件即服务（RaaS）</td></tr><tr><td><strong>所在地</strong></td><td>东欧（据信为俄罗斯）</td></tr><tr><td><strong>官方语言</strong></td><td>俄语</td></tr><tr><td><strong>首次被发现</strong></td><td>2020年8月</td></tr><tr><td><strong>政治立场声明</strong></td><td>自称”非政治化”（apolitical）</td></tr><tr><td><strong>总勒索收入</strong></td><td>超过 9000 万美元</td></tr><tr><td><strong>已知受害者数量</strong></td><td>至少 47 个</td></tr><tr><td><strong>平均勒索金额</strong></td><td>约 190 万美元</td></tr></tbody></table><p>DarkSide 是一个专业的网络犯罪组织，采用<strong>勒索软件即服务（Ransomware as a Service, RaaS）<strong>商业模式运营。该组织被广泛认为位于俄罗斯或东欧地区。值得注意的是，与美国政府直接赞助的高级持续性威胁（APT）组织不同，DarkSide 被认为</strong>不是由国家情报机构直接运营的</strong>，而是”在俄罗斯蓬勃发展的众多营利性勒索软件组织之一”，至少得到了俄罗斯当局的<strong>隐性许可</strong>——只要不攻击俄罗斯本土目标。</p><blockquote><p><strong>核心定位</strong></p><p>DarkSide 是一个<strong>纯粹出于利润驱动</strong>的网络犯罪集团，通过两种方式赚钱：（1）直接勒索实体企业；（2）向其他犯罪分子出售勒索软件工具和服务。卡巴斯基公司威胁勘探主管弗拉基米尔·库斯科夫曾评价道：”DarkSide 执着于’大猎杀’，其目标为大型公司和组织。”</p></blockquote><hr><h3 id="1-2-“发家史”：从-REvil-分支到独立帝国"><a href="#1-2-“发家史”：从-REvil-分支到独立帝国" class="headerlink" title="1.2 “发家史”：从 REvil 分支到独立帝国"></a>1.2 “发家史”：从 REvil 分支到独立帝国</h3><p>根据 Cybereason 和多家安全公司的追踪分析，<strong>DarkSide 最初只有一名核心黑客</strong>，此人曾受雇于臭名昭著的勒索软件服务提供商 <strong>REvil</strong>（又名 Sodinokibi）。这名黑客在 REvil 积累了丰富的网络犯罪经验后，决定自立门户，开发了与 REvil 共享代码基础的勒索软件新变种。</p><h4 id="与-REvil-的渊源"><a href="#与-REvil-的渊源" class="headerlink" title="与 REvil 的渊源"></a>与 REvil 的渊源</h4><p>两家组织的关联证据包括：</p><ul><li><strong>代码相似性</strong>：DarkSide 使用的勒索软件代码与 REvil 高度相似</li><li><strong>勒索信结构</strong>：使用格式几乎相同的勒索通知模板</li><li><strong>地理检查代码</strong>：使用完全相同的代码逻辑来检测并规避前苏联国家（CIS）的目标</li><li><strong>推测关系</strong>：安全专家认为 DarkSide 可能是 REvil 的**分支（offshoot）**或紧密合作伙伴</li></ul><p>2020年11月，DarkSide 开始雇佣自己的分支机构（Affiliates）执行某些阶段的攻击，包括部署恶意载荷。至此，一个独立的勒索软件帝国正式成型。</p><hr><h3 id="1-3-商业模式：RaaS-特许经营体系"><a href="#1-3-商业模式：RaaS-特许经营体系" class="headerlink" title="1.3 商业模式：RaaS 特许经营体系"></a>1.3 商业模式：RaaS 特许经营体系</h3><p>DarkSide 采用的是一种类似**商业特许经营（franchise）**的 RaaS 模式：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">                ┌─────────────────────┐</span><br><span class="line">                │    DarkSide 核心     │</span><br><span class="line">                │ 开发平台/技术支持/媒关 │</span><br><span class="line">                └──────────┬──────────┘</span><br><span class="line">                           │</span><br><span class="line">          ┌────────────────┼────────────────┐</span><br><span class="line">          │ 提供平台+技术支持   收益分成        │</span><br><span class="line">          ▼                 ▼                │</span><br><span class="line">┌─────────────────┐  ┌─────────────────┐     │</span><br><span class="line">│   合作伙伴 A     │  │   合作伙伴 B     │◄────┘</span><br><span class="line">│   UNC2628       │  │ UNC2465/UNC2659  │</span><br><span class="line">└────────┬────────┘  └────────┬────────┘</span><br><span class="line">         │                    │</span><br><span class="line">         ▼                    ▼</span><br><span class="line">┌─────────────────────────────────────┐</span><br><span class="line">│  企业受害者 / 基础设施 / 数据泄露网站  │</span><br><span class="line">└─────────────────────────────────────┘</span><br></pre></td></tr></table></figure><h4 id="收益分成机制"><a href="#收益分成机制" class="headerlink" title="收益分成机制"></a>收益分成机制</h4><table><thead><tr><th>勒索金额范围</th><th>DarkSide 抽成比例</th><th>合作伙伴获得</th></tr></thead><tbody><tr><td>低于 50 万美元</td><td><strong>25%</strong></td><td>75%</td></tr><tr><td>超过 500 万美元</td><td><strong>10%</strong></td><td>90%</td></tr></tbody></table><p>这种分层激励设计鼓励合作伙伴追求更高的勒索金额。根据区块链分析公司 Chainalysis 的数据，DarkSide 在最初七个月内至少带来了<strong>6000万美元</strong>的收入，其中仅2021年第一季度就达到<strong>4600万美元</strong>。</p><h4 id="专业运营特征"><a href="#专业运营特征" class="headerlink" title="专业运营特征"></a>专业运营特征</h4><p>令人惊讶的是，DarkSide 的运营专业化程度堪比正规科技公司：</p><ul><li>拥有<strong>新闻发布室（Press Room）</strong>，定期发布公告</li><li>运营<strong>邮件订阅列表（Mailing List）</strong></li><li>提供<strong>24&#x2F;7 受害者热线（Victim Hotline）</strong>，甚至通过在线聊天提供”客户支持”</li><li>合作伙伴可通过管理面板创建针对特定受害者的<strong>定制版本</strong></li><li>负责处理媒体关系和公关事务</li></ul><h4 id="关键数据一览"><a href="#关键数据一览" class="headerlink" title="关键数据一览"></a>关键数据一览</h4><table><thead><tr><th>指标</th><th>数值</th></tr></thead><tbody><tr><td>总勒索收入</td><td>$90M+</td></tr><tr><td>已知受害者</td><td>47+</td></tr><tr><td>平均勒索金额</td><td>$1.9M</td></tr><tr><td>美国检测次数</td><td>&gt;500</td></tr></tbody></table><hr><h3 id="1-4-攻击手法与技术特点"><a href="#1-4-攻击手法与技术特点" class="headerlink" title="1.4 攻击手法与技术特点"></a>1.4 攻击手法与技术特点</h3><h4 id="标准攻击流程"><a href="#标准攻击流程" class="headerlink" title="标准攻击流程"></a>标准攻击流程</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">阶段1：初始入侵</span><br><span class="line">├── 钓鱼邮件 / 利用合法服务（如 Shopify）投递</span><br><span class="line">├── LNK 文件链接到恶意 URL</span><br><span class="line">└── 部署基于 PowerShell 的后门（如 SMOKEDHAM.NET）</span><br><span class="line"></span><br><span class="line">阶段2：环境渗透</span><br><span class="line">├── 绕过 UAC 控制（使用 CMSTPLUA COM 接口）</span><br><span class="line">├── 地理位置检查 → 跳过前苏联国家</span><br><span class="line">├── 收集凭据（文件/内存/域控制器/LDAP）</span><br><span class="line">├── 建立持久化后门（Cobalt Strike BEACON）</span><br><span class="line">├── 横向移动 + NGROK 隧道建立</span><br><span class="line">└── 删除日志以掩盖踪迹</span><br><span class="line"></span><br><span class="line">阶段3：勒索部署</span><br><span class="line">├── 清除环境（删除回收站、卸载安全软件）</span><br><span class="line">├── 删除备份（含影子副本 Shadow Copies）</span><br><span class="line">├── 使用 Salsa20 + RSA 加密文件</span><br><span class="line">├── 留下勒索信 README.&#123;userid&#125;.TXT</span><br><span class="line">└── 通过 Tor 网站指导支付（BTC/Monero）</span><br></pre></td></tr></table></figure><h4 id="地理位置白名单机制"><a href="#地理位置白名单机制" class="headerlink" title="地理位置白名单机制"></a>地理位置白名单机制</h4><p>DarkSide 内置了一套精密的地理规避系统，通过检查系统语言设置来避免攻击以下<strong>15种语言</strong>对应的地区：</p><p><code>俄语</code> <code>乌克兰语</code> <code>白俄罗斯语</code> <code>塔吉克语</code> <code>亚美尼亚语</code> <code>阿塞拜疆语</code> <code>格鲁吉亚语</code> <code>哈萨克语</code> <code>吉尔吉斯语</code> <code>土库曼语</code> <code>乌兹别克语</code> <code>鞑靼语</code> <code>摩尔多瓦罗马尼亚语</code> <code>叙利亚阿拉伯语</code></p><h4 id="“有选择的”攻击目标"><a href="#“有选择的”攻击目标" class="headerlink" title="“有选择的”攻击目标"></a>“有选择的”攻击目标</h4><p>DarkSide 公开宣称<strong>不会攻击</strong>以下类型的目标：</p><ul><li>🏥 医疗中心 &#x2F; 医院</li><li>🏫 学校 &#x2F; 教育机构</li><li>🤝 非营利组织 &#x2F; 慈善机构</li><li>🏛️ 政府机构</li></ul><blockquote><p>但安全专家指出，这种”边界感”更像是一种<strong>风险规避策略</strong>而非道德底线——攻击这些高敏感目标会引来执法部门的全力追查，不利于长期经营。<br>所以并不是该组织善良 而是因为攻击这些目标会带来不必要的麻烦</p></blockquote><hr><h4 id="技术创新：股票做空模式"><a href="#技术创新：股票做空模式" class="headerlink" title="技术创新：股票做空模式"></a>技术创新：股票做空模式</h4><blockquote><p><strong>业界首创：勒索+做空双重获利</strong></p><p>DarkSide 是<strong>第一家真正实施做空上市公司股票策略的勒索软件组织</strong>。2021年4月20日，该组织在其数据泄露网站上公开写道：</p><p>“我们的团队和合作伙伴加密了许多纳斯达克和其他证券交易所上市公司的数据。如果公司拒绝支付赎金，我们准备公布攻击信息，从而在股价下跌中获利。”</p><p>这意味着 DarkSide 不仅从赎金中获利，还试图通过操纵资本市场获得<strong>第二重收益</strong>。</p></blockquote><hr><h3 id="1-5-标志性事件：Colonial-Pipeline-攻击"><a href="#1-5-标志性事件：Colonial-Pipeline-攻击" class="headerlink" title="1.5 标志性事件：Colonial Pipeline 攻击"></a>1.5 标志性事件：Colonial Pipeline 攻击</h3><blockquote><p><strong>2021年5月7日：改变一切的一天</strong></p></blockquote><table><thead><tr><th>项目</th><th>详情</th></tr></thead><tbody><tr><td><strong>攻击日期</strong></td><td>2021年5月7日</td></tr><tr><td><strong>攻击者</strong></td><td>DarkSide（FBI 正式确认）</td></tr><tr><td><strong>受害者</strong></td><td>Colonial Pipeline（美国最大燃油管道运营商）</td></tr><tr><td><strong>影响范围</strong></td><td>管道被迫关闭，供应美国东海岸 <strong>45% 的燃料</strong></td></tr><tr><td><strong>事件定性</strong></td><td>“迄今为止对美国关键基础设施最严重的网络攻击”</td></tr><tr><td><strong>勒索金额</strong></td><td>约 75 BTC（近 <strong>500万美元</strong>），Colonial Pipeline 已支付</td></tr><tr><td><strong>赎金追回</strong></td><td>美国司法部于6月追回约 <strong>230万美元</strong></td></tr></tbody></table><p>攻击发生后，美国东海岸多个州出现燃油恐慌性抢购，加油站排起长龙，油价飙升。拜登总统宣布进入紧急状态，并放松了对公路运输燃油的限制。这次攻击向世界证明了一件事：<strong>勒索软件已经具备了造成国家级影响的能力</strong>。</p><hr><h3 id="1-6-“罗宾汉”人设与公关策略"><a href="#1-6-“罗宾汉”人设与公关策略" class="headerlink" title="1.6 “罗宾汉”人设与公关策略"></a>1.6 “罗宾汉”人设与公关策略</h3><p>DarkSide 曾试图打造一种”数字罗宾汉”的形象。2020年10月13日，该组织公开声称已将部分勒索所得捐赠给慈善机构：</p><ul><li>向 Children International 捐赠约 <strong>0.88 BTC</strong>（当时价值约 1万美元）</li><li>向 The Water Project 捐赠约 <strong>0.88 BTC</strong>（当时价值约 1万美元）</li></ul><blockquote><p>威胁情报公司首席安全策略师乔恩·迪马吉奥评价道：”当 Darkside 宣称要捐款时，几乎全球的网络新闻媒体都对此做出了报道，这基本上是一个 <strong>2万美元成本的营销</strong>，让它名声大噪。”</p></blockquote><hr><h2 id="二、转折点：DarkSide-的”解散”与-BlackCat-的诞生"><a href="#二、转折点：DarkSide-的”解散”与-BlackCat-的诞生" class="headerlink" title="二、转折点：DarkSide 的”解散”与 BlackCat 的诞生"></a>二、转折点：DarkSide 的”解散”与 BlackCat 的诞生</h2><h3 id="2-1-DarkSide-时间线"><a href="#2-1-DarkSide-时间线" class="headerlink" title="2.1 DarkSide 时间线"></a>2.1 DarkSide 时间线</h3><ul><li><strong>📅 2021年5月7日</strong> — Colonial Pipeline 遭受攻击，管道关闭。全球目光聚焦 DarkSide。</li><li><strong>📅 2021年5月11日</strong> — FBI 和 CISA 联合发布安全警报（AA21-131）。</li><li><strong>📅 2021年5月13日</strong> — 拜登总统表示将采取行动”破坏其运作能力”。美国陆军第780军事情报旅转发 Recorded Future 关于 DarkSide 服务器丢失的报告。</li><li><strong>📅 2021年5月14日</strong> — <strong>DarkSide 用俄语发布声明</strong>，声称因”来自美国的压力”关闭运营，失去对支付服务器和博客的访问权限，资金被提取到未知账户。</li><li><strong>📅 2021年11月4日</strong> — <strong>美国国务院悬赏 1000万美元</strong>征集 DarkSide 领导者信息，另悬赏 500万美元征集参与攻击的犯罪成员信息。</li></ul><blockquote><p>⚠️ <strong>专家警告：解散可能是伪装</strong></p><ul><li>DarkSide 宣称的解散可能是<strong>转移审查视线的诡计</strong></li><li>该团伙很可能以<strong>不同名称恢复活动</strong></li><li>这是网络犯罪网络的常见策略：<strong>关闭 → 复活 → 重新品牌化</strong></li></ul></blockquote><hr><h3 id="2-2-BlackCat-ALPHV-登场：同一伙人的新面孔"><a href="#2-2-BlackCat-ALPHV-登场：同一伙人的新面孔" class="headerlink" title="2.2 BlackCat&#x2F;ALPHV 登场：同一伙人的新面孔"></a>2.2 BlackCat&#x2F;ALPHV 登场：同一伙人的新面孔</h3><p>就在专家们的警告话音未落之际，<strong>2021年11月中旬</strong>——距离 DarkSide 宣布解散仅仅<strong>6个月</strong>——一个新的勒索软件家族被 MalwareHunterTeam 的研究人员首次发现。</p><p>它叫 <strong>BlackCat</strong>，又名 <strong>ALPHV</strong> 或 <strong>Noberus</strong>。</p><table><thead><tr><th>属性</th><th>DarkSide</th><th>BlackCat&#x2F;ALPHV</th></tr></thead><tbody><tr><td><strong>首次出现</strong></td><td>2020年8月</td><td>2021年11月</td></tr><tr><td><strong>编程语言</strong></td><td>未公开</td><td><strong>Rust</strong>（跨平台、高性能）</td></tr><tr><td><strong>母体组织</strong></td><td>REvil 分支</td><td><strong>FIN7, DarkSide, BlackMatter</strong></td></tr><tr><td><strong>运作模式</strong></td><td>RaaS</td><td>RaaS（升级版）</td></tr><tr><td><strong>数据泄露站</strong></td><td>暗网</td><td><strong>公网</strong>（行业首创）</td></tr><tr><td><strong>勒索策略</strong></td><td>双重勒索</td><td><strong>双重 + 三重勒索（含 DDoS 威胁）</strong></td></tr><tr><td><strong>悬赏金额</strong></td><td>$1000万（领导者）</td><td><strong>$1000万（领导者）+ $500万（参与者）</strong></td></tr><tr><td><strong>最终状态</strong></td><td>2021年5月”解散”</td><td>2024年3月宣布关闭</td></tr></tbody></table><h4 id="FBI-的官方定调"><a href="#FBI-的官方定调" class="headerlink" title="FBI 的官方定调"></a>FBI 的官方定调</h4><p><strong>2022年4月</strong>，FBI 发布公告，一锤定音：</p><blockquote><p>“BlackCat&#x2F;ALPHV 的许多开发者和洗钱者与已解散的 <strong>DarkSide 和 BlackMatter</strong> 存在关联。”</p></blockquote><p>安全专家的推测得到了官方证实：<strong>BlackCat 就是 DarkSide 的更名重组</strong>，甚至可能同时吸收了 2021 年底被瓦解的 <strong>REvil</strong> 网络犯罪集团的残余力量。这解释了为什么 BlackCat 从诞生之初就展现出远超一般新生组织的成熟度和攻击力。</p><hr><h2 id="三、第二幕：BlackCat-ALPHV-的疯狂扩张"><a href="#三、第二幕：BlackCat-ALPHV-的疯狂扩张" class="headerlink" title="三、第二幕：BlackCat&#x2F;ALPHV 的疯狂扩张"></a>三、第二幕：BlackCat&#x2F;ALPHV 的疯狂扩张</h2><h3 id="3-1-技术升级：Rust-编写的新一代武器"><a href="#3-1-技术升级：Rust-编写的新一代武器" class="headerlink" title="3.1 技术升级：Rust 编写的新一代武器"></a>3.1 技术升级：Rust 编写的新一代武器</h3><p>BlackCat 的勒索软件使用 <strong>Rust 语言</strong>编写，这是一个关键的技术选择：</p><ul><li><strong>跨平台能力</strong>：Rust 原生支持 Windows、Linux 和 macOS（甚至 VMWare ESXi），这意味着 BlackCat 可以攻击比 DarkSide 更广泛的目标类型</li><li><strong>性能优势</strong>：加密速度更快，大规模文件加密效率更高</li><li><strong>检测规避</strong>：相比传统的 C&#x2F;C++ 勒索软件，Rust 编写的恶意软件在静态分析时更难被识别</li><li><strong>模块化设计</strong>：便于快速迭代和定制化</li></ul><p>此外，BlackCat 引入了多项创新战术：</p><table><thead><tr><th>技术&#x2F;战术</th><th>说明</th></tr></thead><tbody><tr><td><strong>公网数据泄露网站</strong></td><td>行业首创——不再局限于暗网，将受害数据摘录发布在任何浏览器可访问的公网站点，增加曝光压力</td></tr><tr><td><strong>域名仿冒（Typosquatting）</strong></td><td>模仿受害者官网域名创建数据发布站点，进一步施压</td></tr><tr><td><strong>三重勒索</strong></td><td>在双重勒索（加密+数据泄露）基础上增加 <strong>DDoS 攻击威胁</strong></td></tr><tr><td><strong>Emotet 僵尸网络</strong></td><td>利用全球最大的僵尸网络之一作为初始入侵入口（2022年9月起）</td></tr><tr><td><strong>Log4J 漏洞利用</strong></td><td>使用 Log4Shell 在内网横向传播</td></tr><tr><td><strong>GPO 滥用</strong></td><td>通过组策略对象批量分发恶意软件并禁用安全控制</td></tr><tr><td><strong>ExMatter 数据窃取工具</strong></td><td>在部署勒索前先用专业工具窃取敏感数据</td></tr><tr><td><strong>SEO 投毒 &#x2F; 恶意广告</strong></td><td>劫持搜索引擎关键词（如”WinSCP 下载”），通过 Bing&#x2F;Google 广告诱导用户下载恶意安装包</td></tr><tr><td><strong>垃圾代码 + 加密字符串</strong></td><td>反分析和反检测技术</td></tr></tbody></table><h4 id="标准攻击流程（BlackCat-版）"><a href="#标准攻击流程（BlackCat-版）" class="headerlink" title="标准攻击流程（BlackCat 版）"></a>标准攻击流程（BlackCat 版）</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">阶段1：初始入侵（多种入口）</span><br><span class="line">├── 通过初始访问代理（IAB）购买被盗凭证</span><br><span class="line">├── Emotet 僵尸网络投递</span><br><span class="line">├── SEO 投毒 / 恶意广告（如伪造 WinSCP 安装包）</span><br><span class="line">│   └── 包含 Cobalt Strike Beacon 后门</span><br><span class="line">├── 利用 Log4Shell (CVE-2021-44228)</span><br><span class="line">└── 钓鱼邮件 / LNK 文件</span><br><span class="line"></span><br><span class="line">阶段2：环境渗透与持久化</span><br><span class="line">├── 获得管理员权限</span><br><span class="line">├── Cobalt Strike Beacon 建立 C2 通道</span><br><span class="line">├── 侦察 / 数据窃取（ExMatter 工具）</span><br><span class="line">├── GPO 滥用 → 批量分发恶意软件 + 禁用安全控制</span><br><span class="line">├── 横向移动</span><br><span class="line">└── 设置后门</span><br><span class="line"></span><br><span class="line">阶段3：勒索部署</span><br><span class="line">├── 网络发现 → 寻找更多感染目标</span><br><span class="line">├── 删除卷影副本（Volume Shadow Copies）</span><br><span class="line">├── Rust 编写的加密器高速加密文件</span><br><span class="line">├── 投放勒索信 → 要求 BTC/Monero 支付</span><br><span class="line">└── 三重威胁：加密 + 数据泄露 + DDoS</span><br></pre></td></tr></table></figure><hr><h3 id="3-2-2022-年：全球肆虐"><a href="#3-2-2022-年：全球肆虐" class="headerlink" title="3.2 2022 年：全球肆虐"></a>3.2 2022 年：全球肆虐</h3><p>在整个 2022 年，BlackCat 展现出了惊人的攻击频率和野心，目标遍布全球：</p><table><thead><tr><th>受害者类型</th><th>代表案例</th></tr></thead><tbody><tr><td><strong>奢侈品</strong></td><td>Moncler（盟可睐）</td></tr><tr><td><strong>航空服务</strong></td><td>Swissport（瑞士机场地服）</td></tr><tr><td><strong>高等教育</strong></td><td>北卡罗来纳农工州立大学、佛罗里达国际大学、比萨大学、明斯特理工大学</td></tr><tr><td><strong>政府</strong></td><td>奥地利克恩顿州、亚历山大市、<strong>某欧洲政府</strong>（索要 $500 万赎金）</td></tr><tr><td><strong>教育</strong></td><td>里贾纳公立学校</td></tr><tr><td><strong>游戏&#x2F;娱乐</strong></td><td>万代南梦宫（Bandai Namco）、JAKKS Pacific</td></tr><tr><td><strong>能源&#x2F;制造</strong></td><td>Creos、EPM</td></tr><tr><td><strong>科技&#x2F;航空</strong></td><td>Accelya、GSE、NJVC</td></tr></tbody></table><p>截至 <strong>2023年5月</strong>，BlackCat 自出现以来估计已在全球范围内攻击了超过 <strong>350 名受害者</strong>，成为当时<strong>最活跃的勒索软件组织之一</strong>。</p><hr><h3 id="3-3-2023-年：Sphynx-变种与标志性攻击"><a href="#3-3-2023-年：Sphynx-变种与标志性攻击" class="headerlink" title="3.3 2023 年：Sphynx 变种与标志性攻击"></a>3.3 2023 年：Sphynx 变种与标志性攻击</h3><h4 id="Sphynx-变种发布"><a href="#Sphynx-变种发布" class="headerlink" title="Sphynx 变种发布"></a>Sphynx 变种发布</h4><p><strong>2023年2月</strong>，BlackCat 发布了名为 <strong>“Sphynx”（斯芬克斯）</strong> 的新变种，重点提升了<strong>加密速度</strong>和<strong>隐蔽性</strong>。这一升级使 BlackCat 的攻击效率进一步提升。</p><h4 id="Reddit-数据泄露事件"><a href="#Reddit-数据泄露事件" class="headerlink" title="Reddit 数据泄露事件"></a>Reddit 数据泄露事件</h4><p><strong>2023年6月</strong>，BlackCat 声称对当年2月的 Reddit 系统入侵负责：</p><ul><li><strong>窃取数据量</strong>：80 GB（压缩后）</li><li><strong>勒索金额</strong>：450 万美元</li><li><strong>特殊之处</strong>：此次攻击<strong>未涉及数据加密</strong>，属于纯数据勒索（Data Extortion），表明 BlackCat 的战术日益灵活多变</li></ul><h4 id="⭐-MGM-Resorts-Caesars-Entertainment-攻击"><a href="#⭐-MGM-Resorts-Caesars-Entertainment-攻击" class="headerlink" title="⭐ MGM Resorts &amp; Caesars Entertainment 攻击"></a>⭐ MGM Resorts &amp; Caesars Entertainment 攻击</h4><p>这是 BlackCat 历史上最具轰动性的攻击事件之一。</p><p><strong>背景</strong>：执行此次攻击的是 <strong>Scattered Spider</strong>——一个主要由英国和美国黑客组成的团伙，被认为是 ALPHV 的关联组织（部分媒体甚至猜测它是 ALPHV 的子组织）。他们与 ALPHV 核心团队合作，于 <strong>2023年9月</strong> 同时瞄准了拉斯维加斯最大的两家赌场运营商。</p><table><thead><tr><th>项目</th><th>Caesars Entertainment</th><th>MGM Resorts International</th></tr></thead><tbody><tr><td><strong>勒索金额</strong></td><td>$3000 万</td><td>（未公开）</td></tr><tr><td><strong>实际支付</strong></td><td><strong>$1500 万</strong></td><td><strong>拒绝支付</strong></td></tr><tr><td><strong>应对措施</strong></td><td>支付赎金恢复运营</td><td><strong>关闭所有系统数周</strong></td></tr><tr><td><strong>后续影响</strong></td><td>—</td><td>在线业务（含 BetMGM 体育博彩）长期中断</td></tr><tr><td><strong>财务损失</strong></td><td>$1500 万赎金</td><td><strong>Q3 财报损失约 $1 亿</strong></td></tr></tbody></table><blockquote><p>这次攻击的影响极为深远：MGM 因拒绝支付赎金而被迫关闭系统数周，不仅影响了其博彩业务，还导致其在线体育博彩平台 BetMGM 长期瘫痪。最终 MGM 2023 年第三季度因此遭受了约 <strong>1亿美元</strong> 的财务损失——这个数字远超 Caesars 支付的 1500 万美元赎金，成为”是否支付赎金”困境的经典案例。</p></blockquote><p>其他 2023 年初受害者还包括：Grupo Estrategas EMM、NextGen Healthcare、Solar Industries India、Instituto Federal Do Pará、Lehigh Valley Health Network 等。</p><hr><h3 id="3-4-2023年12月：FBI-的重击"><a href="#3-4-2023年12月：FBI-的重击" class="headerlink" title="3.4 2023年12月：FBI 的重击"></a>3.4 2023年12月：FBI 的重击</h3><p><strong>2023年12月19日</strong>，FBI 对 BlackCat 发起了协调执法行动：</p><ul><li><strong>查封多个网站</strong>：BlackCat 的官方网站被替换为 FBI 的查封通知</li><li><strong>发布免费解密工具</strong>：FBI 同日发布了针对 BlackCat 勒索软件的解密工具，受害者可以<strong>无需支付赎金即可恢复文件</strong></li><li><strong>行动代号</strong>：针对 “Alphv Blackcat Ransomware” 的协调执法行动</li></ul><p>这是美国执法部门对勒索软件组织实施的最成功的打击行动之一。然而，正如 DarkSide 的故事所预示的那样——<strong>这并不是终点</strong>。</p><p>截至 <strong>2024年2月</strong>，美国国务院再次悬赏：</p><ul><li><strong>$1000 万</strong>：征集能够识别或定位 ALPHV&#x2F;BlackCat 头目的线索</li><li><strong>$500 万</strong>：征集参与 ALPHV 勒索攻击人员的线索</li></ul><hr><h3 id="3-5-2024-年：最后的疯狂与落幕"><a href="#3-5-2024-年：最后的疯狂与落幕" class="headerlink" title="3.5 2024 年：最后的疯狂与落幕"></a>3.5 2024 年：最后的疯狂与落幕</h3><h4 id="Change-Healthcare-攻击（联合健康集团）"><a href="#Change-Healthcare-攻击（联合健康集团）" class="headerlink" title="Change Healthcare 攻击（联合健康集团）"></a>Change Healthcare 攻击（联合健康集团）</h4><p><strong>2024年2月</strong>，BlackCat 发动了其历史上影响力最大的攻击——目标是 <strong>Change Healthcare</strong>，美国最大的医疗支付处理公司之一，隶属于联合健康集团（UnitedHealth Group）。</p><table><thead><tr><th>项目</th><th>详情</th></tr></thead><tbody><tr><td><strong>受害者</strong></td><td>Change Healthcare（UnitedHealth Group 子公司）</td></tr><tr><td><strong>行业地位</strong></td><td>美国医疗支付处理的关键基础设施</td></tr><tr><td><strong>攻击影响</strong></td><td>全美药房无法处理处方、医院无法获得付款、整个美国医疗支付体系陷入混乱</td></tr><tr><td><strong>勒索金额</strong></td><td><strong>$2200 万</strong>（Change Healthcare 已支付）</td></tr><tr><td><strong>攻击后果</strong></td><td>美国医疗系统大面积瘫痪持续数周</td></tr></tbody></table><p>这次攻击的影响力甚至<strong>超过了 Colonial Pipeline 事件</strong>——因为它直接影响到了每一个需要买药的美国人。</p><h4 id="内部纠纷与”跑路”疑云"><a href="#内部纠纷与”跑路”疑云" class="headerlink" title="内部纠纷与”跑路”疑云"></a>内部纠纷与”跑路”疑云</h4><p>然而，Change Healthcare 的 $2200 万赎金支付却引发了一场戏剧性的内部纠纷：</p><blockquote><p>BlackCat 核心团队与参与此次攻击的<strong>关联方（Affiliate）<strong>之间发生了</strong>付款分配争议</strong>。关联方认为自己应得的分成被黑猫核心扣留了。</p><p>2024年3月，一名 BlackCat 代表突然宣布：<strong>组织即将关闭，并将出售勒索软件产品的源代码。</strong></p></blockquote><p>安全社区普遍认为这是一次典型的<strong>退出骗局（Exit Scam）</strong>——核心开发者在拿到最大一笔赎金后，借”内部纠纷”之名行卷款跑路之实。</p><h4 id="香港消费者委员会攻击"><a href="#香港消费者委员会攻击" class="headerlink" title="香港消费者委员会攻击"></a>香港消费者委员会攻击</h4><p><strong>2024年5月</strong>，《英文虎报》（The Standard）报道香港消费者委员会的服务器和终端设备遭到 ALPHV 勒索软件攻击。这表明即使在宣布关闭之后，BlackCat 的残留影响仍在继续。</p><h4 id="最终状态"><a href="#最终状态" class="headerlink" title="最终状态"></a>最终状态</h4><p>截至 <strong>2025年初</strong>，BlackCat&#x2F;ALPHV <strong>显然已经消失</strong>。但考虑到 DarkSide → BlackMatter → BlackCat 的更名链条，安全专家们并没有放松警惕——<strong>下一个名字会是什么？</strong></p><hr><h2 id="四、完整时间线：从-DarkSide-到-BlackCat"><a href="#四、完整时间线：从-DarkSide-到-BlackCat" class="headerlink" title="四、完整时间线：从 DarkSide 到 BlackCat"></a>四、完整时间线：从 DarkSide 到 BlackCat</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">2020</span><br><span class="line">  └─ 8月   DarkSide 首次被发现</span><br><span class="line">  └─ 10月  DarkSide 公开&quot;慈善捐赠&quot;，打造罗宾汉人设</span><br><span class="line">  └─ 11月  DarkSide 开始雇佣附属机构（Affiliates）</span><br><span class="line"></span><br><span class="line">2021</span><br><span class="line">  ├─ 4月   DarkSide 宣布股票做空策略（业界首创）</span><br><span class="line">  ├─ 5月7日 ⚡ Colonial Pipeline 攻击（改变一切的一天）</span><br><span class="line">  ├─ 5月11日 FBI &amp; CISA 发布 AA21-131 安全警报</span><br><span class="line">  ├─ 5月13日 拜登承诺采取行动；美军网络部队介入</span><br><span class="line">  ├─ 5月14日 DarkSide 宣布&quot;因美国压力&quot;关闭运营</span><br><span class="line">  ├─ 11月4日 美国国务院悬赏 $1000万（+$500万）</span><br><span class="line">  └─ 11月中 ⭐ BlackCat/ALPHV 首次出现（仅6个月后！）</span><br><span class="line"></span><br><span class="line">2022</span><br><span class="line">  ├─ 4月   FBI 正式确认 BlackCat 与 DarkSide/BlackMatter 的关联</span><br><span class="line">  ├─ 全年  全球300+组织遭攻击（Moncler/Swissport/万代/大学/政府...）</span><br><span class="line">  ├─ 5月   某欧洲政府被攻击（$500万赎金）</span><br><span class="line">  └─ 9月   BlackCat 开始利用 Emotet 僵尸网络</span><br><span class="line"></span><br><span class="line">2023</span><br><span class="line">  ├─ 2月   Sphynx 变种发布（速度+隐蔽性升级）</span><br><span class="line">  ├─ 2月   Reddit 被入侵（80GB数据/$450万勒索）</span><br><span class="line">  ├─ 5月   累计受害者超过350个</span><br><span class="line">  ├─ 6月   BlackCat 认领 Reddit 攻击</span><br><span class="line">  ├─ 9月   ⭐ MGM Resorts &amp; Caesars Entertainment 攻击</span><br><span class="line">  │         （Caesars付$1500万 / MGM拒付损失$1亿）</span><br><span class="line">  └─ 12月19日 🔥 FBI 查封网站 + 发布免费解密工具</span><br><span class="line"></span><br><span class="line">2024</span><br><span class="line">  ├─ 2月   ⭐⭐ Change Healthcare 攻击（$2200万赎金）</span><br><span class="line">  │         美国医疗支付系统瘫痪</span><br><span class="line">  ├─ 2月   美国国务院再次悬赏 $1000万+$500万</span><br><span class="line">  ├─ 3月   BlackCat 代表宣布关闭（疑似 Exit Scam）</span><br><span class="line">  ├─ 5月   香港消费者委员会遭 ALPHV 攻击</span><br><span class="line">  └─ 2025初 BlackCat/ALPHV 显然消失……但故事结束了吗？</span><br></pre></td></tr></table></figure><hr><h2 id="五、DarkSide-vs-BlackCat：进化对比"><a href="#五、DarkSide-vs-BlackCat：进化对比" class="headerlink" title="五、DarkSide vs BlackCat：进化对比"></a>五、DarkSide vs BlackCat：进化对比</h2><table><thead><tr><th>维度</th><th>DarkSide (2020-2021)</th><th>BlackCat&#x2F;ALPHV (2021-2024)</th></tr></thead><tbody><tr><td><strong>编程语言</strong></td><td>未公开（推测 C&#x2F;C++）</td><td><strong>Rust</strong>（跨平台）</td></tr><tr><td><strong>目标数量</strong></td><td>47+ 已知受害者</td><td><strong>350+</strong> 全球受害者</td></tr><tr><td><strong>单笔最高赎金</strong></td><td>~$500万（Colonial）</td><td><strong>$3000万</strong>（Caesars）</td></tr><tr><td><strong>总营收</strong></td><td>~$9000万</td><td><strong>远超 DarkSide</strong>（仅 Change Healthcare 一单 $2200万）</td></tr><tr><td><strong>勒索策略</strong></td><td>双重勒索</td><td><strong>三重勒索（+DDoS）</strong></td></tr><tr><td><strong>数据泄露</strong></td><td>暗网</td><td><strong>公网</strong>（行业首创）</td></tr><tr><td><strong>初始访问</strong></td><td>钓鱼&#x2F;Shopify&#x2F;供应链</td><td><strong>IAB&#x2F;Emotet&#x2F;SEO投毒&#x2F;Log4j&#x2F;GPO</strong></td></tr><tr><td><strong>金融创新</strong></td><td>股票做空</td><td>继承 + 更大规模</td></tr><tr><td><strong>最高调攻击</strong></td><td>Colonial Pipeline（能源）</td><td><strong>Change Healthcare（医疗）+ MGM&#x2F;Caesars（博彩）</strong></td></tr><tr><td><strong>执法回应</strong></td><td>服务器丢失 + 悬赏</td><td><strong>FBI 查封 + 免费解密工具 + 悬赏</strong></td></tr><tr><td><strong>结局</strong></td><td>“解散”→ 更名 BlackCat</td><td><strong>内部纠纷→ 关闭→ 消失</strong>？</td></tr></tbody></table><hr><h2 id="六、已识别的威胁活动集群"><a href="#六、已识别的威胁活动集群" class="headerlink" title="六、已识别的威胁活动集群"></a>六、已识别的威胁活动集群</h2><h3 id="DarkSide-时期"><a href="#DarkSide-时期" class="headerlink" title="DarkSide 时期"></a>DarkSide 时期</h3><p>网络安全公司 Mandiant（原 FireEye）记录了至少 <strong>5个威胁活动集群</strong>与 DarkSide 存在关联：</p><table><thead><tr><th>集群代号</th><th>角色&#x2F;行为特征</th></tr></thead><tbody><tr><td><strong>UNC2628</strong></td><td>主要附属机构；与其他 RaaS 供应商（REvil、Netwalker）形成联盟；利用钓鱼邮件和合法服务植入 PowerShell 后门 SMOKEDHAM.NET</td></tr><tr><td><strong>UNC2659</strong></td><td>主要附属机构之一</td></tr><tr><td><strong>UNC2465</strong></td><td>负责部署除 DarkSide 以外的恶意软件；对 Dahua 安防厂商发起<strong>供应链攻击</strong>（植入 SmartPSS 木马化安装包）；24小时内建立 NGROK 隧道进行横向移动；五天后部署键盘记录器、Cobalt Strike BEACON 并转储 LSASS 内存收集凭据</td></tr></tbody></table><h3 id="BlackCat-时期"><a href="#BlackCat-时期" class="headerlink" title="BlackCat 时期"></a>BlackCat 时期</h3><table><thead><tr><th>组织&#x2F;集群</th><th>说明</th></tr></thead><tbody><tr><td><strong><a href="https://zapcoman.github.io/2026/07/16/2026071601/">Scattered Spider</a></strong></td><td>ALPHV 的主要关联组织，主要由英美黑客组成；部分媒体推测为 ALPHV 子组织；主导了 MGM&#x2F;Caesars 攻击</td></tr><tr><td><strong>FIN7</strong></td><td>BlackCat 的母体组织之一（与 DarkSide 并列）；FIN7 本身是一个自2015年起活跃的高阶网络犯罪组织</td></tr><tr><td><strong>初始访问代理（IAB）</strong></td><td>BlackCat 高度依赖 IAB 提供的被盗凭证作为主要入口</td></tr></tbody></table><hr><h2 id="七、启示与反思"><a href="#七、启示与反思" class="headerlink" title="七、启示与反思"></a>七、启示与反思</h2><h3 id="对企业的启示"><a href="#对企业的启示" class="headerlink" title="对企业的启示"></a>对企业的启示</h3><ol><li><strong>外围防御远远不够</strong>：从 Colonial Pipeline 到 Change Healthcare，事实证明端点监测、零信任架构和不可变备份才是真正的防线</li><li><strong>备份是最后防线</strong>：DarkSide 和 BlackCat 都会主动删除含影子副本在内的所有备份，必须实施<strong>离线备份&#x2F;不可变备份&#x2F;3-2-1策略</strong></li><li><strong>供应链安全至关重要</strong>：UNC2465 对 Dahua 的供应链攻击、BlackCat 利用 Emotet 僵尸网络和 SEO 投毒——第三方软件和服务已成为最重要的攻击入口之一</li><li><strong>勒索响应预案必不可少</strong>：MGM 拒付赎金损失 $1 亿 vs Caesars 支付 $1500 万——无论哪种选择，都需要提前制定应急响应计划</li><li><strong>关注新兴入口</strong>：Log4Shell、GPO 滥用、SEO 投毒等非传统攻击向量正在被广泛利用</li></ol><h3 id="对行业的启示"><a href="#对行业的启示" class="headerlink" title="对行业的启示"></a>对行业的启示</h3><ol><li><strong>RaaS 极大地降低了犯罪门槛</strong>：像 SaaS 一样，RaaS 使得不具备技术开发能力的犯罪分子也能发起大规模勒索攻击</li><li><strong>关键基础设施是最高价值目标</strong>：能源（Colonial）、医疗（Change Healthcare）、金融（银行）、博彩（MGM&#x2F;Caesars）——没有哪个行业是安全的</li><li><strong>公网数据泄露成为新常态</strong>：BlackCat 开创的公网泄露模式已被其他组织效仿，企业必须假设 breached data will be public</li><li><strong>国际执法协作仍需加强</strong>：跨国网络犯罪的打击需要各国政府的密切配合，尤其是俄罗斯等”网络犯罪避风港”国家的合作</li><li><strong>加密货币监管刻不容缓</strong>：Bitcoin 和 Monero 等隐私加密货币为勒索资金的洗白提供了便利</li><li><strong>解密工具是双刃剑</strong>：FBI 发布的 BlackCat 解密工具固然帮助了受害者，但也可能刺激犯罪组织加速技术迭代</li></ol><h3 id="一个未解的问题"><a href="#一个未解的问题" class="headerlink" title="一个未解的问题"></a>一个未解的问题</h3><blockquote><p>当一个勒索软件组织可以被”查封”、可以”解散”、可以”关闭”——然后仅仅6个月后以新名字重新出现，并且变得更强大、更猖狂、更富有——我们到底是在”打击犯罪”，还是在玩一场永远打不完的打地鼠游戏？</p></blockquote><hr><h2 id="八、关键要点总结"><a href="#八、关键要点总结" class="headerlink" title="八、关键要点总结"></a>八、关键要点总结</h2><h3 id="DarkSide-时代（2020-2021）"><a href="#DarkSide-时代（2020-2021）" class="headerlink" title="DarkSide 时代（2020-2021）"></a>DarkSide 时代（2020-2021）</h3><ol><li><strong>专业化程度极高</strong>：完整的 RaaS 商业模式、客服系统、新闻发布渠道</li><li><strong>选择性攻击</strong>：地理位置白名单避开 CIS 国家，公开承诺不攻击敏感目标</li><li><strong>技术成熟</strong>：Salsa20+RSA 加密、地理位置感知、供应链攻击</li><li><strong>与 REvil 关系密切</strong>：共享代码基础，可能是其分支</li><li><strong>Colonial Pipeline 是转折点</strong>：引发美国政府强力反击</li><li><strong>“解散”是伪装</strong>：以 BlackCat 身份复活</li></ol><h3 id="BlackCat-ALPHV-时代（2021-2024）"><a href="#BlackCat-ALPHV-时代（2021-2024）" class="headerlink" title="BlackCat&#x2F;ALPHV 时代（2021-2024）"></a>BlackCat&#x2F;ALPHV 时代（2021-2024）</h3><ol start="7"><li><strong>技术全面升级</strong>：Rust 语言编写、跨平台、Sphynx 变种</li><li><strong>战术创新</strong>：公网数据泄露站、三重勒索（+DDoS）、SEO 投毒</li><li><strong>攻击规模空前</strong>：350+ 受害者、遍及全球各行业</li><li><strong>标志性攻击</strong>：MGM&#x2F;Caesars（$1500万-$1亿）、Change Healthcare（$2200万、全美医疗瘫痪）</li><li><strong>FBI 成功打击后又复活</strong>：2023年12月查封 → 2024年2月 Change Healthcare 攻击</li><li><strong>结局成谜</strong>：内部纠纷导致的关闭？还是又一次精心策划的退出骗局？</li><li><strong>悬赏仍在继续</strong>：美国国务院 $1000万 + $500万 悬赏依然有效</li></ol><h3 id="核心结论"><a href="#核心结论" class="headerlink" title="核心结论"></a>核心结论</h3><blockquote><p><strong>DarkSide 没有真正消失——它只是换了一副面孔，变成了更强大、更危险、更富有的 BlackCat。而 BlackCat 的消失，也许只是下一次变身的开始。</strong></p></blockquote><hr><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><h3 id="DarkSide-相关"><a href="#DarkSide-相关" class="headerlink" title="DarkSide 相关"></a>DarkSide 相关</h3><ul><li>Wikipedia: DarkSide (hacker_group)</li><li>FreeBuf: 《勒索组织DarkSide”发家史”》（yannichen, 2021.06.23）</li><li>纽约华人商会: 《美国国务院提供$1000万悬赏”DarkSide”领导者信息》（2021.11.04）</li><li>网络安全与网络战: 《华尔街日报：参与攻击美国关键基础设施的网络犯罪团伙DarkSide已被解散！》（rekingshui, 2021.05.18）</li><li>FBI &amp; CISA Security Alert AA21-131</li><li>Chainalysis 区块链分析报告</li><li>Mandiant (FireEye) 威胁情报报告</li><li>Cybereason 安全研究报告</li><li>Kaspersky 威胁勘探报告</li></ul><h3 id="BlackCat-ALPHV-相关"><a href="#BlackCat-ALPHV-相关" class="headerlink" title="BlackCat&#x2F;ALPHV 相关"></a>BlackCat&#x2F;ALPHV 相关</h3><ul><li>Wikipedia: BlackCat (cyber gang)</li><li>FBI Flash Alert (2022年4月): BlackCat&#x2F;ALPHV 勒索软件警告</li><li>FBI Press Release (2023年12月19日): ALPHV&#x2F;BlackCat 勒索软件查封行动</li><li>U.S. Department of State Rewards Program (2024年2月): $10M &#x2F; $5M 悬赏</li><li>The Hacker News: [Scattered Spider](<a href="https://zapcoman.github.io/2026/07/16/2026071601/">Scattered Spider</a>) 与 ALPHV 的合作关系分析</li><li>The Standard (Hong Kong): 香港消费者委员会 ALPHV 攻击报道（2024.05）</li><li>MalwareHunterTeam: BlackCat 首次发现报告（2021.11）</li><li>Recorded Future &#x2F; Mandiant: BlackCat 威胁情报追踪报告</li></ul><hr><p><em>⚠️ 本文仅供网络安全研究与教育目的使用</em></p><p><em>作者声明：本文内容基于公开资料整理，不构成任何安全建议或法律意见</em></p>]]>
    </content>
    <id>http://example.com/2026/08/11/20260811/2026081103/</id>
    <link href="http://example.com/2026/08/11/20260811/2026081103/"/>
    <published>2026-08-11T13:48:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>从 Colonial Pipeline 到 Change Healthcare，一个黑客帝国的两世轮回<br>从无名之辈到撼动美国能源命脉——勒索软件帝国的兴衰史</p>
</blockquote>
<p><strong>发布时间：2026年8月1]]>
    </summary>
    <title>黑客组织 - DarkSide</title>
    <updated>2026-08-11T14:27:29.925Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="社会工程学" scheme="http://example.com/categories/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="社会工程学" scheme="http://example.com/tags/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="黑客组织" scheme="http://example.com/tags/%E9%BB%91%E5%AE%A2%E7%BB%84%E7%BB%87/"/>
    <content>
      <![CDATA[<blockquote><p><strong>警告：本文涉及网络犯罪、青少年剥削等敏感内容，旨在提高公众安全意识。</strong></p></blockquote><hr><h2 id="引言：一个隐藏在暗网中的犯罪帝国"><a href="#引言：一个隐藏在暗网中的犯罪帝国" class="headerlink" title="引言：一个隐藏在暗网中的犯罪帝国"></a>引言：一个隐藏在暗网中的犯罪帝国</h2><p>2025年7月23日，美国联邦调查局（FBI）发布了一份代号为 <strong>I-072325-PSA</strong> 的公共服务警报，向全球公众发出紧急警告：一个名为 <strong>“The Com”</strong>（全称 <strong>The Community</strong>，意为”社区”）的跨国网络犯罪组织正在迅速扩张，其下属的 <strong>Hacker Com</strong> 分支对青少年的网络安全构成了前所未有的威胁。</p><p>这不是一部好莱坞电影——这是正在发生的现实。</p><hr><h2 id="一、The-Com-是什么？"><a href="#一、The-Com-是什么？" class="headerlink" title="一、The Com 是什么？"></a>一、The Com 是什么？</h2><h3 id="1-1-组织概况"><a href="#1-1-组织概况" class="headerlink" title="1.1 组织概况"></a>1.1 组织概况</h3><table><thead><tr><th>属性</th><th>详情</th></tr></thead><tbody><tr><td><strong>全称</strong></td><td>The Community（社区）</td></tr><tr><td><strong>简称</strong></td><td>The Com &#x2F; 康</td></tr><tr><td><strong>性质</strong></td><td>跨国在线犯罪网络</td></tr><tr><td><strong>主要语言</strong></td><td>英语</td></tr><tr><td><strong>成员构成</strong></td><td>大量未成年人 + 成年犯罪分子</td></tr></tbody></table><p><strong>The Com</strong> 是一个高度组织化的国际性网络生态系统，由多个相互关联的犯罪子网络组成。根据互联网极端主义研究员 Marc-André Argentino 的研究，该组织如今已演变为一个类似于”互联网暴力和极端主义团体联盟”的庞大结构。</p><h3 id="1-2-三大分支架构"><a href="#1-2-三大分支架构" class="headerlink" title="1.2 三大分支架构"></a>1.2 三大分支架构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">┌─────────────────────────────────────────────────────────────┐</span><br><span class="line">│                     THE COM (The Community)                  │</span><br><span class="line">│                        跨国犯罪帝国                           │</span><br><span class="line">├──────────────┬──────────────────┬───────────────────────────┤</span><br><span class="line">│              │                   │                            │</span><br><span class="line">│  🔴 Hacker   │  💀 Sextortion    │  ⚡ IRL / Offline         │</span><br><span class="line">│     Com      │      Com          │       Com                  │</span><br><span class="line">│              │                   │                            │</span><br><span class="line">│ 网络攻击     │ 性勒索招募        │ 现实暴力                    │</span><br><span class="line">│ 数据窃取     │ 儿童剥削          │ 恐怖主义                    │</span><br><span class="line">│ 勒索软件     │ 自残诱导          │ 纵火/袭击                   │</span><br><span class="line">│ 加密货币盗窃 │ 血腥内容传播      │ 谋杀                        │</span><br><span class="line">└──────────────┴──────────────────┴───────────────────────────┘</span><br></pre></td></tr></table></figure><h4 id="分支详解："><a href="#分支详解：" class="headerlink" title="分支详解："></a>分支详解：</h4><table><thead><tr><th>分支</th><th>曾用名</th><th>核心职能</th><th>危险等级</th></tr></thead><tbody><tr><td><strong>Hacker Com</strong></td><td>Cyber Com</td><td>网络犯罪技术实施</td><td>🔴 极高</td></tr><tr><td><strong>Sextortion Com</strong></td><td>-</td><td>通过性勒索招募新成员、收集个人信息用于人肉搜索</td><td>🔴 极高</td></tr><tr><td><strong>IRL Com &#x2F; Offline Com</strong></td><td>-</td><td>线下暴力犯罪、恐怖袭击</td><td>🔴 极高</td></tr></tbody></table><hr><h2 id="二、Hacker-Com：技术最精湛的网络犯罪分支"><a href="#二、Hacker-Com：技术最精湛的网络犯罪分支" class="headerlink" title="二、Hacker Com：技术最精湛的网络犯罪分支"></a>二、Hacker Com：技术最精湛的网络犯罪分支</h2><h3 id="2-1-Hacker-Com-是谁？"><a href="#2-1-Hacker-Com-是谁？" class="headerlink" title="2.1 Hacker Com 是谁？"></a>2.1 Hacker Com 是谁？</h3><p><strong>Hacker Com</strong>（前身为 <strong>Cyber Com</strong>）是 The Com 技术能力最强的分支，由一群<strong>技术高超的网络犯罪分子</strong>组成。该组织的部分成员甚至与知名的<strong>勒索软件即服务（RaaS）</strong> 组织存在关联。</p><h4 id="核心特征："><a href="#核心特征：" class="headerlink" title="核心特征："></a>核心特征：</h4><ul><li>✅ <strong>技术导向</strong>：成员具备高级编程、网络安全攻防技能</li><li>✅ <strong>利益驱动</strong>：以加密货币盗窃为主要动机</li><li>✅ <strong>服务化运营</strong>：出售技术服务牟利（黑客工具、漏洞利用等）</li><li>✅ <strong>组织灵活</strong>：按需吸纳新成员或分裂重组</li></ul><h3 id="2-2-Hacker-Com-的犯罪手段一览"><a href="#2-2-Hacker-Com-的犯罪手段一览" class="headerlink" title="2.2 Hacker Com 的犯罪手段一览"></a>2.2 Hacker Com 的犯罪手段一览</h3><h4 id="🖥️-网络攻击类型"><a href="#🖥️-网络攻击类型" class="headerlink" title="🖥️ 网络攻击类型"></a>🖥️ 网络攻击类型</h4><table><thead><tr><th>攻击类型</th><th>描述</th><th>危害程度</th></tr></thead><tbody><tr><td><strong>DDoS 攻击</strong></td><td>分布式拒绝服务攻击</td><td>⭐⭐⭐⭐</td></tr><tr><td><strong>勒索软件攻击</strong></td><td>加密受害者数据并索要赎金</td><td>⭐⭐⭐⭐⭐</td></tr><tr><td><strong>网络钓鱼 (Phishing)</strong></td><td>伪造网站&#x2F;邮件窃取凭证</td><td>⭐⭐⭐⭐</td></tr><tr><td><strong>SIM 卡交换</strong></td><td>劫持手机号获取账户控制权</td><td>⭐⭐⭐⭐⭐</td></tr><tr><td><strong>计算机入侵</strong></td><td>未授权访问系统&#x2F;网络</td><td>⭐⭐⭐⭐⭐</td></tr><tr><td><strong>恶意软件开发</strong></td><td>开发并部署 RAT、木马等</td><td>⭐⭐⭐⭐⭐</td></tr></tbody></table><h4 id="🛠️-常用工具库"><a href="#🛠️-常用工具库" class="headerlink" title="🛠️ 常用工具库"></a>🛠️ 常用工具库</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">攻击工具箱</span><br><span class="line">├── 远程访问木马 (RAT)</span><br><span class="line">├── 网络钓鱼工具包 (Phishing Kit)</span><br><span class="line">├── VoIP 语音服务商（匿名通话）</span><br><span class="line">├── 语音调制器（变声伪装）</span><br><span class="line">├── VPN / 代理服务（隐藏IP）</span><br><span class="line">├── IP欺骗技术</span><br><span class="line">├── 加密货币混币/提现服务</span><br><span class="line">├── 直播平台（用于炫耀和招募）</span><br><span class="line">└── 加密电子邮件域名</span><br></pre></td></tr></table></figure><h3 id="2-3-从网络到现实：Swatting（虚假报警）"><a href="#2-3-从网络到现实：Swatting（虚假报警）" class="headerlink" title="2.3 从网络到现实：Swatting（虚假报警）"></a>2.3 从网络到现实：Swatting（虚假报警）</h3><p><strong>Swatting</strong> 是 Hacker Com 最臭名昭著的手段之一。</p><blockquote><p><strong>什么是 Swatting？</strong></p><p>Swatting 指的是伪造紧急报警电话（如谎报有人质、炸弹威胁），诱使特警队（SWAT）突袭目标住所的行为。这种”武器化执法”不仅浪费公共资源，更可能导致无辜人员伤亡。</p></blockquote><h4 id="Hacker-Com-如何使用-Swatting："><a href="#Hacker-Com-如何使用-Swatting：" class="headerlink" title="Hacker Com 如何使用 Swatting："></a>Hacker Com 如何使用 Swatting：</h4><ol><li><strong>转移注意力</strong>：在实施加密货币盗窃或企业入侵时，通过 Swatting 转移目标注意力</li><li><strong>内部报复</strong>：组织内成员发生冲突时互相 Swatting</li><li><strong>雇佣服务</strong>：部分子组织提供”付费 Swatting”服务，报酬为加密货币</li></ol><hr><h2 id="三、令人震惊的真相：大量未成年人是参与者也是受害者"><a href="#三、令人震惊的真相：大量未成年人是参与者也是受害者" class="headerlink" title="三、令人震惊的真相：大量未成年人是参与者也是受害者"></a>三、令人震惊的真相：大量未成年人是参与者也是受害者</h2><h3 id="3-1-为什么瞄准未成年人？"><a href="#3-1-为什么瞄准未成年人？" class="headerlink" title="3.1 为什么瞄准未成年人？"></a>3.1 为什么瞄准未成年人？</h3><p>FBI 在2025年的报告中揭示了一个令人不安的事实：</p><blockquote><p><strong>“The Com 故意雇用美国未成年人，因为他们在刑事司法系统中受到的处罚较轻。”</strong></p></blockquote><p>这是一个精心计算的策略：</p><ul><li>👶 <strong>法律保护</strong>：未成年人犯罪量刑从轻</li><li>🎮 <strong>易接触</strong>：通过游戏平台（Roblox、Minecraft）和社交软件（Discord、Telegram）接触</li><li>💻 <strong>技术天赋</strong>：许多青少年具备较强的计算机操作能力</li><li>🤝 <strong>易操控</strong>：缺乏社会经验的青少年更容易被洗脑和操控</li></ul><h3 id="3-2-招募流程：从”朋友”到”罪犯”"><a href="#3-2-招募流程：从”朋友”到”罪犯”" class="headerlink" title="3.2 招募流程：从”朋友”到”罪犯”"></a>3.2 招募流程：从”朋友”到”罪犯”</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">阶段1: 建立&quot;友谊&quot;</span><br><span class="line">   ↓ 发送温暖、浪漫的信息</span><br><span class="line">阶段2: 获取信任</span><br><span class="line">   ↓ 受害者开始分享私人信息</span><br><span class="line">阶段3: 胁迫升级</span><br><span class="line">   ↓ 要求发送私密照片/视频</span><br><span class="line">阶段4: 全面控制</span><br><span class="line">   ↓ 勒索、自残要求、暴力内容</span><br><span class="line">阶段5: &quot;转化&quot;</span><br><span class="line">   ↓ 受害者变成新的招募者</span><br></pre></td></tr></table></figure><h3 id="3-3-那些被摧毁的青春"><a href="#3-3-那些被摧毁的青春" class="headerlink" title="3.3 那些被摧毁的青春"></a>3.3 那些被摧毁的青春</h3><p>据加拿大皇家骑警（RCMP）披露，The Com 的受害者被迫实施了以下令人发指的行为：</p><ul><li>⚠️ <strong>割腕自残</strong></li><li>⚠️ <strong>伤害自己的宠物</strong></li><li>⚠️ <strong>拍摄将纸条放在死动物上的照片</strong></li><li>⚠️ <strong>用自己的血在墙上写字</strong></li><li>⚠️ <strong>拍摄伤口特写</strong></li></ul><p>如果拒绝服从？威胁随之而来：</p><ul><li>❌ <strong>Swatting（虚假报警）</strong></li><li>❌ <strong>家人会受到伤害</strong></li><li>❌ <strong>私密照片泄露给亲友</strong></li></ul><hr><h2 id="四、重大案件回顾"><a href="#四、重大案件回顾" class="headerlink" title="四、重大案件回顾"></a>四、重大案件回顾</h2><h3 id="4-1-高调网络攻击事件"><a href="#4-1-高调网络攻击事件" class="headerlink" title="4.1 高调网络攻击事件"></a>4.1 高调网络攻击事件</h3><table><thead><tr><th>时间</th><th>目标</th><th>影响</th></tr></thead><tbody><tr><td><strong>2022年</strong></td><td>HubSpot, Activision, Okta, DoorDash, Cloudflare, Twilio</td><td>大规模数据泄露</td></tr><tr><td><strong>2023年</strong></td><td>Reddit, MailChimp, 凯撒娱乐, Coinbase, Clorox, Riot Games, <strong>米高梅</strong></td><td>米高梅损失约 <strong>1亿美元</strong></td></tr><tr><td><strong>2024年8月</strong></td><td>Bitcoin 窃取案</td><td>18岁成员 Veer Chetal 窃取 <strong>2.43亿美元</strong> 比特币</td></tr></tbody></table><blockquote><p>米高梅集团在2023年遭受的攻击被认为是 The Com 与 BlackCat&#x2F;ALPHV 勒索组织结盟后实施的，直接经济损失高达约 <strong>1亿美元</strong>。</p></blockquote><h3 id="4-2-暴力犯罪事件"><a href="#4-2-暴力犯罪事件" class="headerlink" title="4.2 暴力犯罪事件"></a>4.2 暴力犯罪事件</h3><table><thead><tr><th>时间</th><th>事件</th><th>详情</th></tr></thead><tbody><tr><td><strong>2021年1月6日</strong></td><td>美国国会大厦袭击</td><td>成员 Riley Williams 窃取佩洛西笔记本电脑，意图出售给俄罗斯情报机构</td></tr><tr><td><strong>2024年7-10月</strong></td><td>瑞典随机袭击</td><td>The Com 成员在 Hässelby 对6名路人实施了无端袭击</td></tr><tr><td><strong>2025年1月</strong></td><td>国际联合抓捕</td><td>Interpol-Europol 行动逮捕2名 CVLT（新纳粹附属组织）成员</td></tr><tr><td><strong>2025年7月</strong></td><td>明尼苏达持刀袭击</td><td>20岁 The Com 成员在公园刺伤女子20刀</td></tr><tr><td><strong>2025年8月</strong></td><td>Roblox Spawnism 邪教</td><td>儿童被诱骗将游戏符号刻入皮肤，相信可以”死后重生”</td></tr></tbody></table><h3 id="4-3-执法行动时间线"><a href="#4-3-执法行动时间线" class="headerlink" title="4.3 执法行动时间线"></a>4.3 执法行动时间线</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">2011 ── Allison Nixon (Unit 221B) 开始调查 The Com</span><br><span class="line">  │</span><br><span class="line">2021 ── 美/英/德/罗/巴五国联合调查，多人被起诉</span><br><span class="line">  │</span><br><span class="line">2023.09 ── FBI 发布关于 764 组织（The Com 关联）的警告</span><br><span class="line">  │</span><br><span class="line">2025.01 ── Interpol-Europol 联合行动逮捕 CVLT 成员</span><br><span class="line">  │</span><br><span class="line">2025.02 ── 英国 21 岁成员因敲诈未成年少女被判有罪</span><br><span class="line">  │</span><br><span class="line">2025.07 ── FBI 再次发布关于 Hacker Com 和 IRL Com 的警告 ⚠️</span><br><span class="line">  │</span><br><span class="line">2025.07 ── 明尼苏达 SR1 成员因持刀袭击被捕</span><br><span class="line">  │</span><br><span class="line">2025.08 ── Roblox Spawnism 邪教事件曝光</span><br><span class="line">  │</span><br><span class="line">2026.06-07 ── Interpol 标记 4,340 个关联网站要求删除 🌍</span><br></pre></td></tr></table></figure><hr><h2 id="五、内部生态：金钱、背叛与暴力循环"><a href="#五、内部生态：金钱、背叛与暴力循环" class="headerlink" title="五、内部生态：金钱、背叛与暴力循环"></a>五、内部生态：金钱、背叛与暴力循环</h2><h3 id="5-1-以加密货币为核心的动机链"><a href="#5-1-以加密货币为核心的动机链" class="headerlink" title="5.1 以加密货币为核心的动机链"></a>5.1 以加密货币为核心的动机链</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">加密货币盗窃 → 成员炫耀财富 → 引起他人嫉妒/贪婪</span><br><span class="line">      ↓                              ↓</span><br><span class="line">  内部冲突 ←────────────────── SIM交换/网络入侵</span><br><span class="line">      ↓</span><br><span class="line">  人身勒索（绑架、酷刑、威胁家人）</span><br><span class="line">      ↓</span><br><span class="line">  IRL Com 暴力行为升级</span><br></pre></td></tr></table></figure><p><strong>关键发现</strong>：Hacker Com 内部的名声和地位完全取决于两个指标：</p><ol><li>💻 <strong>技术水平</strong>（技能集）</li><li>💰 <strong>账户余额</strong>（加密货币持有量）</li></ol><p>成员经常在屏幕共享中炫耀数百万美元的盗窃收益——这恰恰使他们成为下一个目标。</p><h3 id="5-2-分支间的协作关系"><a href="#5-2-分支间的协作关系" class="headerlink" title="5.2 分支间的协作关系"></a>5.2 分支间的协作关系</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">┌────────────────────────────────────────────────────────┐</span><br><span class="line">│                    成员流动路径                          │</span><br><span class="line">│                                                        │</span><br><span class="line">│  IRL Com ──→ 结识 Hacker Com 成员                      │</span><br><span class="line">│     │           │                                      │</span><br><span class="line">│     │           ↓                                      │</span><br><span class="line">│     │      学习网络技能                                 │</span><br><span class="line">│     │           │                                      │</span><br><span class="line">│     └──────────→ 参与 Hacker Com 犯罪活动               │</span><br><span class="line">│                                                        │</span><br><span class="line">│  Hacker Com ──→ 用获利资金参与 Extort Com               │</span><br><span class="line">│                 （儿童性剥削）                           │</span><br><span class="line">└────────────────────────────────────────────────────────┘</span><br></pre></td></tr></table></figure><hr><h2 id="六、关联极端主义组织"><a href="#六、关联极端主义组织" class="headerlink" title="六、关联极端主义组织"></a>六、关联极端主义组织</h2><p>The Com 并非孤立存在，它与多个极端主义团体存在深度关联：</p><table><thead><tr><th>组织名称</th><th>性质</th><th>与 The Com 的关系</th></tr></thead><tbody><tr><td><strong>764</strong></td><td>新纳粹儿童剥削网络</td><td>The Com 多名领导人同时是 764 成员；Offline Com 中最大最有影响力的团体</td></tr><tr><td><strong>CVLT</strong></td><td>暴力新纳粹组织</td><td>The Com 附属，胁迫16名未成年人发送色情内容并自残</td></tr><tr><td><strong>No Lives Matter</strong></td><td>极端主义团体</td><td>The Com 成员组织</td></tr><tr><td><strong>Maniacs Murder Cult</strong></td><td>邪教式暴力团体</td><td>The Com 成员组织</td></tr><tr><td><strong>Satanic Front</strong></td><td>撒旦教极端组织</td><td>IRL Com 成员组织</td></tr><tr><td><strong>BlackCat &#x2F; ALPHV</strong></td><td>勒索软件组织</td><td>2023年宣布结盟</td></tr><tr><td><strong>俄罗斯&#x2F;乌克兰光头党</strong></td><td>极端民族主义</td><td>有 confirmed 关联</td></tr></tbody></table><blockquote><p>⚠️ <strong>特别关注</strong>：美国政府效率部门（DOGE）的高级雇员 <strong>Edward Coristine</strong> 曾被曝出是 The Com 的前成员，引发了关于国家安全审查的广泛讨论。</p></blockquote><hr><h2 id="七、数据背后的趋势"><a href="#七、数据背后的趋势" class="headerlink" title="七、数据背后的趋势"></a>七、数据背后的趋势</h2><h3 id="7-1-犯罪数量激增"><a href="#7-1-犯罪数量激增" class="headerlink" title="7.1 犯罪数量激增"></a>7.1 犯罪数量激增</h3><p>根据英国国家犯罪局（NCA）的数据：</p><table><thead><tr><th>年份</th><th>相对犯罪指数</th><th>变化</th></tr></thead><tbody><tr><td>2022</td><td>100%（基准）</td><td>—</td></tr><tr><td>2024</td><td><strong>400%</strong></td><td>📈 <strong>增长4倍</strong></td></tr></tbody></table><h3 id="7-2-全球执法响应"><a href="#7-2-全球执法响应" class="headerlink" title="7.2 全球执法响应"></a>7.2 全球执法响应</h3><ul><li>🌍 <strong>2026年6-7月</strong>：Interpol 标记了 <strong>4,340个</strong> 与 The Com 相关的网站要求删除</li><li>🤝 多国联合执法行动已成为常态</li><li>📢 FBI 连续发布多次公开警告</li></ul><hr><h2 id="九、总结与展望"><a href="#九、总结与展望" class="headerlink" title="九、总结与展望"></a>九、总结与展望</h2><h3 id="The-Com-的本质"><a href="#The-Com-的本质" class="headerlink" title="The Com 的本质"></a>The Com 的本质</h3><p>The Com 不是一个普通的黑客团伙——它是一个<strong>自我繁殖的犯罪生态系统</strong>：</p><ol><li>🔄 <strong>自我复制</strong>：受害者转化为加害者</li><li>🌐 <strong>跨国运作</strong>：利用互联网突破地理边界</li><li>👶 <strong>瞄准弱势群体</strong>：系统性利用未成年人</li><li>💰 <strong>多重动机</strong>：金钱、意识形态、权力欲交织</li><li>⚡ <strong>线上线下联动</strong>：网络冲突升级为实体暴力</li></ol><h3 id="挑战"><a href="#挑战" class="headerlink" title="挑战"></a>挑战</h3><ul><li>🕵️ <strong>身份隐匿</strong>：成员多使用 VPN、加密货币、虚假身份</li><li>⚖️ <strong>司法管辖权</strong>：跨国犯罪增加引渡难度</li><li>🔄 <strong>组织韧性</strong>：打掉一部分，另一部分继续运作</li></ul><h2 id="免责声明"><a href="#免责声明" class="headerlink" title="免责声明"></a>免责声明</h2><p>本文基于公开信息编写，旨在提高公众对网络犯罪的认知和安全意识。文中提到的组织、人物和事件均来源于 FBI、Europol、Interpol 等执法机构的官方通报以及维基百科等开源情报渠道。如发现任何可疑活动，请立即拨打110报警电话</p><hr><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="https://www.fbi.gov/">FBI Public Service Alert I-072325-PSA</a> (2025年7月23日)</li><li><a href="https://en.wikipedia.org/wiki/The_Com">Wikipedia: The Com</a></li><li>Unit 221B Cybersecurity Research (Allison Nixon)</li><li>RCMP (加拿大皇家骑警) 公开报告</li><li>UK National Crime Agency (NCA) 数据</li><li>Interpol 公开声明 (2026)</li><li>Europol 联合行动报告</li></ol><hr>]]>
    </content>
    <id>http://example.com/2026/08/11/20260811/2026081102/</id>
    <link href="http://example.com/2026/08/11/20260811/2026081102/"/>
    <published>2026-08-11T07:08:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p><strong>警告：本文涉及网络犯罪、青少年剥削等敏感内容，旨在提高公众安全意识。</strong></p>
</blockquote>
<hr>
<h2 id="引言：一个隐藏在暗网中的犯罪帝国"><a href="#引言：一个隐藏在暗网中的犯]]>
    </summary>
    <title>深度解析：The Com 网络犯罪帝国与 Hacker Com 的崛起</title>
    <updated>2026-08-11T09:45:14.436Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="网络安全" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="社会工程学" scheme="http://example.com/tags/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="AI" scheme="http://example.com/tags/AI/"/>
    <category term="人脸识别" scheme="http://example.com/tags/%E4%BA%BA%E8%84%B8%E8%AF%86%E5%88%AB/"/>
    <content>
      <![CDATA[<blockquote><p>深度学习的 deepfake 技术不仅可以用来伪造人脸进入公司内部区域，还可以用以攻击个人，对个人进行深度的定制骗局进行精准诈骗。这不是科幻电影，而是正在发生的现实。</p></blockquote><hr><h1 id="DEEPFAKE-攻防实战"><a href="#DEEPFAKE-攻防实战" class="headerlink" title="DEEPFAKE 攻防实战"></a>DEEPFAKE 攻防实战</h1><h2 id="2-560万美元转给了一场假会议"><a href="#2-560万美元转给了一场假会议" class="headerlink" title="2,560万美元转给了一场假会议"></a>2,560万美元转给了一场假会议</h2><p>2024年1月，香港。Arup的财务人员参加了一场视频会议。屏幕那头，CFO和几位同事表情自然，声音熟悉，聊的是例行转账审批。没人觉得不对。会后，财务照常分15笔把钱转了出去。</p><p>后来才发现，那场会议里坐在屏幕上的”人”，<strong>全是伪造的</strong>。CFO没开过这个会，同事也没参加。<strong>2,560万美元，转给了不存在的人。</strong></p><p><strong>Arup 损失$2,560万（HK$200M）｜15笔转账｜香港办公室</strong></p><hr><h2 id="威胁已经规模化"><a href="#威胁已经规模化" class="headerlink" title="威胁已经规模化"></a>威胁已经规模化</h2><table><thead><tr><th>指标</th><th>数据</th><th>来源</th></tr></thead><tbody><tr><td><strong>$893M</strong></td><td>FBI 首次追踪 AI 欺诈损失</td><td>2025 · 美国</td></tr><tr><td><strong>+680%</strong></td><td>AI 音频 Deepfake 同比增长</td><td>2024 · Pindrop</td></tr><tr><td><strong>11%</strong></td><td>占全球欺诈活动比例</td><td>2026 · Sumsub</td></tr></tbody></table><p>Signicat 调查了7个欧洲国家的1,200名反欺诈负责人，<strong>Deepfake欺诈三年增长2,137%</strong>，渗透率从0.1%升至6.5%。Gartner 2026年报告显示，302名企业安全负责人中62%遭遇过Deepfake攻击。Keepnet的数据更直接：<strong>80%的企业没有任何应对计划。</strong></p><p>这份报告按<strong>技术原理、攻击案例、检测能力、防御框架、法规合规</strong>展开，结尾给出一份可直接落地的20项检查清单。</p><hr><h1 id="第一章-Deepfake-技术原理拆解"><a href="#第一章-Deepfake-技术原理拆解" class="headerlink" title="第一章 Deepfake 技术原理拆解"></a>第一章 Deepfake 技术原理拆解</h1><h2 id="Deepfake-三大生成技术架构"><a href="#Deepfake-三大生成技术架构" class="headerlink" title="Deepfake 三大生成技术架构"></a>Deepfake 三大生成技术架构</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">┌─────────────────────────────────────────────────────────────────────┐</span><br><span class="line">│  第一代 2017-2020                                                    │</span><br><span class="line">│  ┌─────────────────────────────────────────────────────────────┐   │</span><br><span class="line">│  │ 自动编码器 Autoencoder                                       │   │</span><br><span class="line">│  │ 原理：共享编码器压缩至潜空间，双解码器重建源脸与目标脸          │   │</span><br><span class="line">│  │ 优势：可控性强，单人换脸效果稳定                              │   │</span><br><span class="line">│  │ 代表工具：DeepFaceLab · FaceSwap                             │   │</span><br><span class="line">│  └─────────────────────────────────────────────────────────────┘   │</span><br><span class="line">├─────────────────────────────────────────────────────────────────────┤</span><br><span class="line">│  第二代 2018-2022                                                    │</span><br><span class="line">│  ┌─────────────────────────────────────────────────────────────┐   │</span><br><span class="line">│  │ GAN 生成对抗网络                                              │   │</span><br><span class="line">│  │ 原理：生成器造假，判别器辨真，对抗训练至平衡                    │   │</span><br><span class="line">│  │ 优势：生成质量高，可合成不存在的面孔                           │   │</span><br><span class="line">│  │ 代表工具：StyleGAN · StyleGAN2                               │   │</span><br><span class="line">│  └─────────────────────────────────────────────────────────────┘   │</span><br><span class="line">├─────────────────────────────────────────────────────────────────────┤</span><br><span class="line">│  第三代 2022至今                                                     │</span><br><span class="line">│  ┌─────────────────────────────────────────────────────────────┐   │</span><br><span class="line">│  │ 扩散模型 Diffusion                                           │   │</span><br><span class="line">│  │ 原理：正向逐步加噪，反向学习去噪还原                            │   │</span><br><span class="line">│  │ 优势：多样性极高，文本可控生成                                │   │</span><br><span class="line">│  │ 代表工具：Stable Diffusion · Midjourney                      │   │</span><br><span class="line">│  └─────────────────────────────────────────────────────────────┘   │</span><br><span class="line">└─────────────────────────────────────────────────────────────────────┘</span><br></pre></td></tr></table></figure><p><strong>语音克隆仅需 20-30 秒目标音频即可生成逼真语音</strong></p><p>换脸只是入门。Deepfake的攻击面从静态图片一直延伸到实时视频通话，语音、表情、全身都能伪造。三代技术各有侧重。</p><h3 id="自动编码器：换脸的起点"><a href="#自动编码器：换脸的起点" class="headerlink" title="自动编码器：换脸的起点"></a>自动编码器：换脸的起点</h3><p>2017年deepfakes在Reddit发布第一段换脸视频，用的就是自动编码器。编码器把人脸压缩到一组潜变量，再用两个解码器分别重建。训练完成后，把目标脸的解码器接到源脸的编码器上，完成换脸。DeepFaceLab至今仍是开源换脸的主力工具。短板是逐帧处理、速度慢，且需要大量目标人脸数据。</p><h3 id="GAN：把假做真"><a href="#GAN：把假做真" class="headerlink" title="GAN：把假做真"></a>GAN：把假做真</h3><p>生成对抗网络让生成器和判别器互相博弈。生成器不断造假，判别器不断挑刺，直到生成器产出的样本判别器分不出真假。StyleGAN系列能合成照片级的不存在面孔。通病是训练不稳定、容易模式坍缩，生成多样性受限。</p><blockquote><p>这样通过 <code>AI</code> 与 <code>AI</code> 之间的红蓝对抗 让真正的识别器更加难以鉴别真假 </p></blockquote><h3 id="扩散模型：当前的主力"><a href="#扩散模型：当前的主力" class="headerlink" title="扩散模型：当前的主力"></a>扩散模型：当前的主力</h3><p>扩散模型把生成拆成两步：正向把图像逐步加噪到纯随机，反向学习如何一步步去噪。Stable Diffusion和Midjourney都基于这条路线。它生成的多样性远超前两代，文本可控性强，代价是采样慢、算力消耗大。2022年之后，扩散模型基本接管了生成式内容的主流。</p><h2 id="八种主要攻击形态"><a href="#八种主要攻击形态" class="headerlink" title="八种主要攻击形态"></a>八种主要攻击形态</h2><table><thead><tr><th>序号</th><th>攻击形态</th><th>描述</th></tr></thead><tbody><tr><td>1</td><td><strong>换脸 Face Swap</strong></td><td>替换视频或图像中的人脸</td></tr><tr><td>2</td><td><strong>语音克隆 Voice Cloning</strong></td><td>复制目标音色与语调</td></tr><tr><td>3</td><td><strong>说话头 Talking Head</strong></td><td>驱动静态照片开口说话</td></tr><tr><td>4</td><td><strong>表情重演 Reenactment</strong></td><td>迁移表情到目标脸</td></tr><tr><td>5</td><td><strong>全身合成 Full Body</strong></td><td>生成完整虚拟人物</td></tr><tr><td>6</td><td><strong>文生视频 Text-to-Video</strong></td><td>文本直接生成视频</td></tr><tr><td>7</td><td><strong>实时换脸 Live Swap</strong></td><td>视频通话实时替换</td></tr><tr><td>8</td><td><strong>语音转换 Voice Conversion</strong></td><td>实时改变说话人身份</td></tr></tbody></table><hr><h1 id="第二章-攻击全景与真实案例"><a href="#第二章-攻击全景与真实案例" class="headerlink" title="第二章 攻击全景与真实案例"></a>第二章 攻击全景与真实案例</h1><h2 id="Deepfake-攻击案例时间线"><a href="#Deepfake-攻击案例时间线" class="headerlink" title="Deepfake 攻击案例时间线"></a>Deepfake 攻击案例时间线</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">2019 ──────────────────────────────────────────────────────────► 2025</span><br><span class="line"></span><br><span class="line">2019年 · 英国能源公司                          2025年初 · 新加坡</span><br><span class="line">AI模仿德国母公司CEO声音致电                      跨国公司Zoom全假人会议</span><br><span class="line">财务人员按指令转款                                全员伪造，财务转账</span><br><span class="line">$243,000                                         $499,000</span><br><span class="line"></span><br><span class="line">         2024.1                    2024.7</span><br><span class="line">         Arup 香港                 法拉利</span><br><span class="line">         伪造CFO视频会议            WhatsApp冒充CEO Benedetto Vigna AI语音</span><br><span class="line">         15笔转账                   高管识破 → 成功阻止</span><br><span class="line">         整场全是假人</span><br><span class="line">         $2,560万</span><br><span class="line"></span><br><span class="line">═══════════════════════════════════════════════════════════════</span><br><span class="line">2019 → 2024：语音克隆 → 全息视频伪造</span><br><span class="line">2024 · +680%：AI音频Deepfake同比 Pindrop</span><br><span class="line">2026 · 占比 11%：Deepfake占全球欺诈 Sumsub</span><br><span class="line">FBI · $893M：2025首次追踪AI欺诈损失</span><br></pre></td></tr></table></figure><p>四个案例串起一条升级线：<strong>2019年还只是电话里的声音克隆，2024年进化到整场视频会议伪造，2025年已经能做到全员假人。<strong>攻击成本在降，欺骗维度在升。法拉利那次之所以成功阻止，是因为高管要求对方回答一个只有内部才知道的问题——这是</strong>带外验证</strong>的现实样本。</p><h2 id="暗网-Deepfake-即服务（DaaS）"><a href="#暗网-Deepfake-即服务（DaaS）" class="headerlink" title="暗网 Deepfake 即服务（DaaS）"></a>暗网 Deepfake 即服务（DaaS）</h2><p>攻击门槛在塌方。Kaspersky GReAT 2025年9月的暗网监测显示，一段Deepfake视频标价50美元，语音克隆30美元，相比早期跌幅达400倍。Group-IB 2026年1月发现，合成身份套件只要5美元。小团伙甚至个人都能发起过去只有国家级对手才能实施的攻击。</p><table><thead><tr><th>服务类型</th><th>价格</th><th>来源</th></tr></thead><tbody><tr><td>Deepfake 视频</td><td>$50&#x2F;段</td><td>Kaspersky 2025.9</td></tr><tr><td>语音克隆</td><td>$30&#x2F;段</td><td>Kaspersky 2025.9</td></tr><tr><td>合成身份套件</td><td>$5</td><td>Group-IB 2026.1</td></tr><tr><td><strong>整体价格跌幅</strong></td><td><strong>400倍</strong></td><td>Kaspersky</td></tr></tbody></table><hr><h2 id="核心威胁场景一：伪造人脸进入公司内部区域"><a href="#核心威胁场景一：伪造人脸进入公司内部区域" class="headerlink" title="核心威胁场景一：伪造人脸进入公司内部区域"></a>核心威胁场景一：伪造人脸进入公司内部区域</h2><p>随着企业数字化转型的推进，越来越多的公司采用<strong>人脸识别门禁系统</strong>、<strong>远程身份核验</strong>、<strong>视频会议授权</strong>等技术手段。而Deepfake技术正在让这些安全防线形同虚设。</p><p><strong>典型攻击路径：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">攻击者收集目标员工面部数据（社交媒体、公开照片）</span><br><span class="line">        ↓</span><br><span class="line">使用Deepfake技术生成高质量伪造人脸/视频</span><br><span class="line">        ↓</span><br><span class="line">攻击人脸识别门禁系统或远程身份核验系统</span><br><span class="line">        ↓</span><br><span class="line">以合法员工身份进入公司内部区域/获取系统权限</span><br><span class="line">        ↓</span><br><span class="line">窃取机密数据、破坏基础设施、或进一步渗透内网</span><br></pre></td></tr></table></figure><p><strong>真实案例：</strong></p><ul><li>2026年4月，美国司法部公布”笔记本农场”案件：朝鲜IT人员利用AI和换脸技术掩盖身份，<strong>进入100多家企业</strong>并产生超500万美元收入。</li><li>法拉利公司曾成功阻止类似攻击——高管要求对方回答一个只有内部才知道的问题。这是**带外验证（Out-of-Band Verification）**的现实样本。</li></ul><p><strong>核心风险点：</strong></p><ul><li>人脸识别门禁系统被绕过</li><li>远程办公身份核验失效</li><li>视频会议中的”同事”可能是伪造的</li><li>高价值交易审批被冒充</li></ul><hr><h2 id="核心威胁场景二：针对个人的深度定制骗局"><a href="#核心威胁场景二：针对个人的深度定制骗局" class="headerlink" title="核心威胁场景二：针对个人的深度定制骗局"></a>核心威胁场景二：针对个人的深度定制骗局</h2><p>这是更隐蔽、更普遍的威胁。攻击者可以利用Deepfake技术对个人进行<strong>精准画像+深度定制</strong>，实施高度个性化的诈骗。</p><p><strong>攻击流程：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">第一步：信息收集</span><br><span class="line">├── 社交媒体公开数据（朋友圈、微博、抖音等）</span><br><span class="line">├── 泄露的个人数据（历史泄露库）</span><br><span class="line">├── 通讯录关系链（通过社工获取）</span><br><span class="line">└── 声音样本（公开视频、语音消息）</span><br><span class="line"></span><br><span class="line">第二步：深度定制</span><br><span class="line">├── 克隆亲友声音（音色、语调、口癖）</span><br><span class="line">├── 生成&quot;亲友&quot;视频通话形象</span><br><span class="line">├── 编造个性化诈骗剧本（基于真实经历）</span><br><span class="line">└── 选择最佳攻击时机（如深夜、情绪低落时）</span><br><span class="line"></span><br><span class="line">第三步：实施诈骗</span><br><span class="line">├── 视频通话借钱（&quot;出车祸了&quot;&quot;急需周转&quot;）</span><br><span class="line">├── 冒充领导/熟人要求转账</span><br><span class="line">├── 制造紧急情况压缩思考时间</span><br><span class="line">└── 利用情感纽带降低警惕性</span><br></pre></td></tr></table></figure><p><strong>为什么这种骗局如此有效？</strong></p><ol><li><strong>多模态协同攻击</strong>：同时伪造视觉（脸）、听觉（声音）、内容（个性化剧本），可信度极高</li><li><strong>利用社交工程学</strong>：基于真实关系链和信息，击中情感软肋</li><li><strong>时间压力</strong>：制造紧急情况，不给受害者核实的时间</li><li><strong>认知偏差</strong>：”我亲眼看到、亲耳听到，还能有假？”</li></ol><blockquote><p><strong>最可怕的不是技术本身，而是技术与社会工程学的结合。</strong> 当Deepfake遇上精准画像，每个人都是潜在受害者。</p></blockquote><hr><h1 id="第三章-检测技术深度拆解"><a href="#第三章-检测技术深度拆解" class="headerlink" title="第三章 检测技术深度拆解"></a>第三章 检测技术深度拆解</h1><h2 id="三大检测技术路径对比"><a href="#三大检测技术路径对比" class="headerlink" title="三大检测技术路径对比"></a>三大检测技术路径对比</h2><table><thead><tr><th>检测路径</th><th>代表方案</th><th>检测原理</th><th>准确率</th><th>特点</th></tr></thead><tbody><tr><td><strong>生理信号检测</strong></td><td>rPPG · FakeCatcher</td><td>检测面部血流量变化还原脉搏信号</td><td>94-98%</td><td>Intel FakeCatcher，毫秒级响应</td></tr><tr><td><strong>频域分析</strong></td><td>FFT · DWT</td><td>检测上采样留下的高频周期伪影</td><td>GAN 70-85% &#x2F; 扩散 40-60%</td><td>扩散模型生成更自然，难检测</td></tr><tr><td><strong>语音反欺骗</strong></td><td>MFCC · 呼吸模式</td><td>AI模型常忽略呼吸节奏等细节</td><td>95-97%</td><td>ASVspoof EER约2-5%</td></tr></tbody></table><p>检测和生成在赛跑。目前主流的检测路径分三类，思路各异，效果也分场景。</p><h3 id="生理信号检测：找活人的证据"><a href="#生理信号检测：找活人的证据" class="headerlink" title="生理信号检测：找活人的证据"></a>生理信号检测：找活人的证据</h3><p>真人脸有血液流动，皮肤颜色随心跳发生极微小变化，人眼看不见但相机能捕捉。rPPG（远程光电容积描记）就是把这些微小色变还原成脉搏信号。伪造脸没有真实的血流，这一生理特征对不上。Intel的FakeCatcher把这条路做到了毫秒级，准确率94-98%。</p><h3 id="频域分析：找生成的痕迹"><a href="#频域分析：找生成的痕迹" class="headerlink" title="频域分析：找生成的痕迹"></a>频域分析：找生成的痕迹</h3><p>真实图像在频域有自然的高频分布，生成图像上采样会留下周期性伪影。FFT和DWT能把这种痕迹提取出来。对GAN类生成物有效，准确率70-85%。但扩散模型生成得更自然，频域伪影更弱，准确率掉到40-60%——<strong>技术代差直接体现在检测难度上</strong>。</p><h3 id="语音反欺骗：找呼吸的破绽"><a href="#语音反欺骗：找呼吸的破绽" class="headerlink" title="语音反欺骗：找呼吸的破绽"></a>语音反欺骗：找呼吸的破绽</h3><p>AI语音模型合成时常常忽略呼吸声、吞咽声、停顿节奏这些生理细节。分析MFCC系数和呼吸模式可以识别合成语音，准确率95-97%。ASVspoof挑战赛的等错误率（EER）约2-5%，是语音反欺骗领域的公认基准。</p><h2 id="检测路径与代表方案"><a href="#检测路径与代表方案" class="headerlink" title="检测路径与代表方案"></a>检测路径与代表方案</h2><table><thead><tr><th>检测路径</th><th>代表方案</th><th>关键指标</th></tr></thead><tbody><tr><td>生理信号 rPPG</td><td>Intel FakeCatcher</td><td>94-98% · 毫秒级</td></tr><tr><td>频域分析</td><td>FFT &#x2F; DWT</td><td>GAN 70-85% &#x2F; 扩散 40-60%</td></tr><tr><td>语音反欺骗</td><td>Pindrop &#x2F; ASVspoof</td><td>EER 2-5% · 95-97%</td></tr><tr><td><strong>深度学习检测</strong></td><td><strong>TextIn CNN+ViT</strong></td><td><strong>域内90%+ &#x2F; 域外85%+</strong></td></tr></tbody></table><h3 id="商业化检测方案：TextIn人脸伪造图像检测"><a href="#商业化检测方案：TextIn人脸伪造图像检测" class="headerlink" title="商业化检测方案：TextIn人脸伪造图像检测"></a>商业化检测方案：TextIn人脸伪造图像检测</h3><p>AI技术，尤其是最近的GAN和Diffusion Model，能够创造出难以用肉眼辨识的虚假图片。这些图片的高仿真度使得它们在虚假宣传、非法取证、网络诈骗等场合被滥用的风险急剧上升，进一步加剧了社会对于图像真实性的质疑。</p><p>面对这些挑战，开发先进的AI造假检测技术，通过科技手段进行对抗极其必要。</p><p><strong>TextIn人脸伪造图像检测基于CNN+ViT技术框架实现</strong>，核心在于通过大批量不同来源、生成技术的Deepfake样本训练，提供随伪造技术同步成长的防伪手段：</p><ul><li><strong>训练域内90%+识别准确率</strong></li><li><strong>域外85%+识别准确率</strong>：对从未见过的Deepfake技术生成的伪造人脸样本，也具备强大的AI泛化性能</li><li>支持SaaS在线体验和私有化部署</li><li>可通过API接入任意工作流：身份认证、视频会议、内容审核等</li><li>对人像进行实时分析，返回检测结果，并在存在风险时告警，自动触发后续保护机制</li></ul><blockquote><p><strong>点击 <a href="https://www.textin.cn/">TextIn官网</a> 或”阅读原文”，即可在线体验人脸伪造图像检测</strong></p></blockquote><p>随着人工智能技术的飞速发展，Deepfake技术所带来的挑战和风险也日益凸显。然而，正如TextIn所展示的，<strong>技术的进步同样为我们提供了对抗这些风险的有力工具</strong>——这正是”道高一丈”的有力证明。</p><h2 id="实验室数据漂亮，真实世界是另一回事"><a href="#实验室数据漂亮，真实世界是另一回事" class="headerlink" title="实验室数据漂亮，真实世界是另一回事"></a>实验室数据漂亮，真实世界是另一回事</h2><table><thead><tr><th>场景</th><th>准确率</th></tr></thead><tbody><tr><td>干净数据</td><td><strong>94.7%</strong> ← 实验室准确率</td></tr><tr><td>失真后（压缩转码二次拍摄）</td><td><strong>67.8%</strong></td></tr><tr><td><strong>退化率</strong></td><td><strong>26.9%</strong> ← Frontiers in AI 2026.6</td></tr></tbody></table><p>压缩、转码、二次拍摄、二次转发都会让检测准确率断崖式下降。iProov 2025年的测试更直接：让人跨格式识别Deepfake，<strong>准确率仅0.1%</strong>。</p><p><strong>任何宣称高准确率的方案，都该问一句——经过微信压缩和转发之后还剩多少。</strong></p><hr><h1 id="第四章-企业防御四支柱框架"><a href="#第四章-企业防御四支柱框架" class="headerlink" title="第四章 企业防御四支柱框架"></a>第四章 企业防御四支柱框架</h1><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">┌─────────────────────────────────────────────────────────────────────┐</span><br><span class="line">│                    四支柱防御框架                                     │</span><br><span class="line">├──────────────┬──────────────┬──────────────┬────────────────────────┤</span><br><span class="line">│   支柱一     │   支柱二     │   支柱三     │       支柱四           │</span><br><span class="line">│    验证      │    技术      │    人员      │        响应             │</span><br><span class="line">├──────────────┼──────────────┼──────────────┼────────────────────────┤</span><br><span class="line">│ 带外验证协议 │ AI对抗AI检测 │ 心理安全工程 │ 响应手册               │</span><br><span class="line">│ 独立通道验证 │ 活体检测     │ 密钥短语验证 │ 取证验证流程           │</span><br><span class="line">│ 大于$1万转账 │ 眨眼转头3D   │ 高价值交易   │ 技术隔离预案           │</span><br><span class="line">│ 动态密钥短语 │ rPPG心率验证 │ AI模拟训练   │ 危机沟通机制           │</span><br><span class="line">│ 一次性轮换   │ 会议接入检测 │ 红蓝对抗     │ 冻结转账窗口           │</span><br><span class="line">└──────────────┴──────────────┴──────────────┴────────────────────────┘</span><br></pre></td></tr></table></figure><p>单点防御挡不住有准备的攻击者。把验证、技术、人员、响应四条线都立起来，才能让攻击者在每个环节都要付出更高成本。</p><h3 id="支柱一-·-验证"><a href="#支柱一-·-验证" class="headerlink" title="支柱一 · 验证"></a>支柱一 · 验证</h3><p>带外验证（OOB）是底线。超过1万美元的转账必须通过独立通道二次确认——不是回拨来电显示的号码，而是拨打已登记在案的号码。动态密钥短语每次交易使用一次性验证词，攻击者即便伪造了面孔和声音，也拿不到这个词。<strong>法拉利案例就是这套逻辑的现实版。</strong></p><h3 id="支柱二-·-技术"><a href="#支柱二-·-技术" class="headerlink" title="支柱二 · 技术"></a>支柱二 · 技术</h3><p>用AI对抗AI。在视频会议、客服电话、身份核验等入口部署检测工具。活体检测要求眨眼、转头或3D结构光验证，rPPG心率验证能确认屏幕背后是不是活人。检测工具要定期更新模型，跟上生成技术的迭代。</p><h3 id="支柱三-·-人员"><a href="#支柱三-·-人员" class="headerlink" title="支柱三 · 人员"></a>支柱三 · 人员</h3><p>心理安全工程解决的是权威压力下的盲从。高价值交易强制密钥短语验证，定期做AI模拟训练和红蓝对抗，建立可以质疑上级指令的文化和匿名上报通道。<strong>技术再强，最终按下转账键的是人。</strong></p><h3 id="支柱四-·-响应"><a href="#支柱四-·-响应" class="headerlink" title="支柱四 · 响应"></a>支柱四 · 响应</h3><p>事后比事前更关键。响应手册要写清分级流程；取证验证流程要保留原始文件、固定证据链；技术隔离预案要能在事发时快速冻结转账窗口；危机沟通机制要对内对外都有口径。<strong>响应的速度直接决定损失能不能追回。</strong></p><hr><h1 id="第五章-内容溯源与法规合规"><a href="#第五章-内容溯源与法规合规" class="headerlink" title="第五章 内容溯源与法规合规"></a>第五章 内容溯源与法规合规</h1><h2 id="内容溯源标准对比"><a href="#内容溯源标准对比" class="headerlink" title="内容溯源标准对比"></a>内容溯源标准对比</h2><table><thead><tr><th>标准</th><th>技术原理</th><th>特点</th><th>局限</th></tr></thead><tbody><tr><td><strong>C2PA 内容凭证</strong></td><td>加密签名清单嵌入文件元数据</td><td>Adobe&#x2F;OpenAI&#x2F;Microsoft&#x2F;Sony采用</td><td>元数据剥离后失效</td></tr><tr><td><strong>Google SynthID</strong></td><td>像素级扰动嵌入图像水印</td><td>99%+识别率，集成Imagen&#x2F;Gemini&#x2F;Vertex AI</td><td>依赖Google生态</td></tr></tbody></table><p><strong>2026 最佳实践 · 双重实施策略：同时添加水印（SynthID）+ 加密清单（C2PA）</strong></p><p>溯源是事前打标，法规是事后追责。两条线要并行。</p><p>C2PA把加密签名清单嵌进文件元数据，记录创作链路和编辑历史，Adobe、OpenAI、Microsoft、Sony都在用。短板是元数据一旦被剥离——压缩、转码、截图都会发生——验证就失败。Google SynthID走另一条路，把水印做进像素级扰动，对未修改图像识别率99%以上，集成在Imagen、Gemini、Vertex AI里，更抗篡改但依赖Google生态。<strong>2026年的最佳实践是两条都做：SynthID抗剥离，C2PA留链路。</strong></p><h2 id="中国：标识先行"><a href="#中国：标识先行" class="headerlink" title="中国：标识先行"></a>中国：标识先行</h2><p>《深度合成管理规定》2023年1月10日施行，要求深度合成内容显著标识。《AI生成合成内容标识办法》2025年9月1日施行，由网信办等四部门联合发布，明确显式标识加隐式标识的双重要求，是目前最成体系的AI内容标识制度。</p><h2 id="美国：联邦立法推进中"><a href="#美国：联邦立法推进中" class="headerlink" title="美国：联邦立法推进中"></a>美国：联邦立法推进中</h2><ul><li><strong>DEFIANCE法案 S.1837</strong>：2026年1月13日参议院通过，待众议院表决。法定赔偿$150,000-$250,000，追诉时效10年</li><li><strong>加州 AB 853</strong>：2025年10月13日签署，要求平台保留C2PA来源信息</li><li><strong>加州 AB 2655</strong>：被联邦法院以Section 230推翻，州级监管受联邦法律限制</li><li><strong>NIST SP 800-63-4</strong>（2024年初始公开草案）：要求远程生物特征验证具备PAD（呈现攻击检测）能力</li></ul><h2 id="欧盟：最严披露义务"><a href="#欧盟：最严披露义务" class="headerlink" title="欧盟：最严披露义务"></a>欧盟：最严披露义务</h2><p><strong>EU AI Act 第50条</strong>：2026年8月2日生效，要求披露AI生成内容。最高罚款1,500万欧元或全球年营收3%。已有工具宽限至2026年12月2日。<strong>这是目前处罚力度最大、生效时间最明确的AI透明度条款。</strong></p><hr><h1 id="第六章-防御检查清单"><a href="#第六章-防御检查清单" class="headerlink" title="第六章 防御检查清单"></a>第六章 防御检查清单</h1><p><strong>4类20项，可直接对照落地。</strong></p><h3 id="技术防护（5项）"><a href="#技术防护（5项）" class="headerlink" title="技术防护（5项）"></a>技术防护（5项）</h3><table><thead><tr><th>序号</th><th>检查项</th></tr></thead><tbody><tr><td>1</td><td>部署Deepfake检测API，覆盖图像&#x2F;视频&#x2F;语音</td></tr><tr><td>2</td><td>视频会议启用活体检测与会前身份核验</td></tr><tr><td>3</td><td>电话客服通道接入语音反欺骗</td></tr><tr><td>4</td><td>高价值交易系统接入rPPG心率验证</td></tr><tr><td>5</td><td>邮件附件强制内容溯源（C2PA）校验</td></tr></tbody></table><h3 id="流程制度（5项）"><a href="#流程制度（5项）" class="headerlink" title="流程制度（5项）"></a>流程制度（5项）</h3><table><thead><tr><th>序号</th><th>检查项</th></tr></thead><tbody><tr><td>6</td><td>建立带外验证协议，禁止仅凭单一通道指令转账</td></tr><tr><td>7</td><td>设定$1万以上转账强制独立验证阈值</td></tr><tr><td>8</td><td>制定一次性密钥短语制度，定期轮换</td></tr><tr><td>9</td><td>建立Deepfake事件响应手册与分级流程</td></tr><tr><td>10</td><td>设定转账可撤回冻结窗口（建议4小时）</td></tr></tbody></table><h3 id="人员培训（5项）"><a href="#人员培训（5项）" class="headerlink" title="人员培训（5项）"></a>人员培训（5项）</h3><table><thead><tr><th>序号</th><th>检查项</th></tr></thead><tbody><tr><td>11</td><td>全员Deepfake威胁意识培训，纳入入职流程</td></tr><tr><td>12</td><td>高价值岗位强制密钥短语验证训练</td></tr><tr><td>13</td><td>季度AI模拟钓鱼与Deepfake演练</td></tr><tr><td>14</td><td>建立质疑文化与匿名上报通道</td></tr><tr><td>15</td><td>高管授权验证白名单，限定可指令范围</td></tr></tbody></table><h3 id="合规溯源（5项）"><a href="#合规溯源（5项）" class="headerlink" title="合规溯源（5项）"></a>合规溯源（5项）</h3><table><thead><tr><th>序号</th><th>检查项</th></tr></thead><tbody><tr><td>16</td><td>对接C2PA内容凭证标准</td></tr><tr><td>17</td><td>评估SynthID水印集成到自有生成管线</td></tr><tr><td>18</td><td>跟踪EU AI Act第50条合规进度</td></tr><tr><td>19</td><td>落实中国深度合成与AI标识两项规定</td></tr><tr><td>20</td><td>按NIST SP 800-63-4部署远程生物特征PAD能力</td></tr></tbody></table><hr><h2 id="个人防护补充清单"><a href="#个人防护补充清单" class="headerlink" title="个人防护补充清单"></a>个人防护补充清单</h2><h3 id="基础防护措施"><a href="#基础防护措施" class="headerlink" title="基础防护措施"></a>基础防护措施</h3><table><thead><tr><th>序号</th><th>防护措施</th><th>具体操作</th></tr></thead><tbody><tr><td>1</td><td><strong>带外验证</strong></td><td>视频通话借钱时，挂断后拨打对方已知号码回确认</td></tr><tr><td>2</td><td><strong>动态挑战</strong></td><td>让对方转头看侧面、用手挡住半张脸——目前Deepfake对侧面轮廓和遮挡处理仍容易露馅</td></tr><tr><td>3</td><td><strong>设置暗号</strong></td><td>与亲友约定只有彼此知道的验证词</td></tr><tr><td>4</td><td><strong>保护个人数据</strong></td><td>减少在公开平台分享高清正面照、语音消息</td></tr><tr><td>5</td><td><strong>保持怀疑</strong></td><td>即使”亲眼看到、亲耳听到”，涉及金钱时也要多方核实</td></tr><tr><td>6</td><td><strong>时间缓冲</strong></td><td>遇到”紧急”转账要求，主动要求”半小时后再处理”，给自己留出冷静期</td></tr><tr><td>7</td><td><strong>关注标识</strong></td><td>注意内容是否标注了深度合成来源（中国已要求强制标识）</td></tr></tbody></table><h3 id="普通人识别Deepfake的8个实用方法（鹏信科技-·-2024年11月）"><a href="#普通人识别Deepfake的8个实用方法（鹏信科技-·-2024年11月）" class="headerlink" title="普通人识别Deepfake的8个实用方法（鹏信科技 · 2024年11月）"></a>普通人识别Deepfake的8个实用方法（鹏信科技 · 2024年11月）</h3><p>Deepfake技术的日益精进使得伪造视频或音频变得越来越难以分辨。以下是一些实用的方法，能够帮助普通人提高对Deepfake内容的警觉性和识别能力：</p><h4 id="1-观察脸部边缘和肤色"><a href="#1-观察脸部边缘和肤色" class="headerlink" title="1. 观察脸部边缘和肤色"></a>1. 观察脸部边缘和肤色</h4><p>仔细观察<strong>脸部的边缘、面部的肤色</strong>是否与头部或身体其他部位相匹配？脸部的边缘是锐利还是模糊？脖子和下巴周围是否有不自然的痕迹？</p><h4 id="2-检查嘴唇同步"><a href="#2-检查嘴唇同步" class="headerlink" title="2. 检查嘴唇同步"></a>2. 检查嘴唇同步</h4><p>观察<strong>嘴唇动作</strong>是否与音频完美匹配？比如某个文字发音需要闭嘴，但Deepfake中的嘴巴却没有完全闭合；如果无法同步匹配，那很可能就是Deepfake。</p><h4 id="3-观察牙齿清晰度"><a href="#3-观察牙齿清晰度" class="headerlink" title="3. 观察牙齿清晰度"></a>3. 观察牙齿清晰度</h4><p><strong>牙齿</strong>是否清晰可见，如果模糊不清并且与现实生活中的外观不一致，很可能为伪造视频。目前的算法可能还不足以生成单个牙齿的清晰轮廓。</p><h4 id="4-注意眨眼动作"><a href="#4-注意眨眼动作" class="headerlink" title="4. 注意眨眼动作"></a>4. 注意眨眼动作</h4><p><strong>眨眼动作</strong>是否不自然，过于频繁或过于稀少的眨眼都可能表明视频被篡改，眨眼动作可能会显得过于机械或缺乏变化，眨眼动作是否与其他动作和表情协调一致。</p><h4 id="5-测试90度侧脸"><a href="#5-测试90度侧脸" class="headerlink" title="5. 测试90度侧脸"></a>5. 测试90度侧脸</h4><p><strong>人脸转到90度时会出现漏洞</strong>——这些深度伪造模型由于没有足够多的数据做人脸轮廓数据训练，因而不能转换面部的不同边界，或者执行必要的修复。针对视频通话，可通过该方式来识别。</p><h4 id="6-提问私人问题"><a href="#6-提问私人问题" class="headerlink" title="6. 提问私人问题"></a>6. 提问私人问题</h4><p>视频通话中，我们可以问一些只有对方知道的<strong>私人问题</strong>，如家庭地址、生日、宠物名字等。如果对方在回答时表现出犹豫或无法准确回答，那么这很可能是一个诈骗行为。</p><h4 id="7-挥手和按压测试"><a href="#7-挥手和按压测试" class="headerlink" title="7. 挥手和按压测试"></a>7. 挥手和按压测试</h4><ul><li><strong>挥手测试</strong>：在视频电话时，挥手会干扰面部的数据，导致伪造的人脸出现抖动、闪现或其他异常情况</li><li><strong>按压测试</strong>：让对方尝试按压鼻子或脸部，观察其面部变化。真人的鼻子在被按压时会发生变形，但AI生成的鼻子则不会</li></ul><h4 id="8-验证信息来源"><a href="#8-验证信息来源" class="headerlink" title="8. 验证信息来源"></a>8. 验证信息来源</h4><p><strong>关注信息的来源和可靠性</strong>。如果视频或音频来自一个不受信任或可疑的来源，那么其真实性就值得怀疑。通过验证信息的发布渠道、查看其他权威来源的报道或分析，可以帮助判断其真实性。</p><blockquote><p><strong>当我们踏入AI以假乱真的时代，确保真实性的挑战似乎比任何时候都更加艰巨。</strong></p><p>在面对Deepfake诈骗横行的当下，无论是企业还是普通人，都需时刻保持警惕，不断提升自身的防范意识与能力。企业应当加强技术研发，利用先进技术手段筑起坚实的防线，同时要完善内部管理，确保流程无懈可击。而普通人则要提高辨别真伪的能力，不轻易相信未经核实的信息，学会从多个角度、多个渠道进行验证。</p><p>只有这样，我们才能在Deepfake技术带来的挑战中立于不败之地，共同营造一个更加安全、可信的社会环境。</p></blockquote><hr><h2 id="别等出事再想对策"><a href="#别等出事再想对策" class="headerlink" title="别等出事再想对策"></a>别等出事再想对策</h2><p>检测技术在进步，伪造技术也没闲着。今天能拦住的，明天可能就绕过去了。验证、技术、人员、响应，四条线哪条断了都会出问题。<strong>多一道关卡，攻击者就多一份成本。</strong></p><hr><h1 id="究竟是伪造魔高一尺，还是识别道高一丈？"><a href="#究竟是伪造魔高一尺，还是识别道高一丈？" class="headerlink" title="究竟是伪造魔高一尺，还是识别道高一丈？"></a>究竟是伪造魔高一尺，还是识别道高一丈？</h1><p>这是一个值得深思的问题。</p><h2 id="伪造的”魔”：攻击优势"><a href="#伪造的”魔”：攻击优势" class="headerlink" title="伪造的”魔”：攻击优势"></a>伪造的”魔”：攻击优势</h2><table><thead><tr><th>优势维度</th><th>具体表现</th></tr></thead><tbody><tr><td><strong>成本断崖式下跌</strong></td><td>Deepfake视频从数千美元降至50美元，跌幅400倍</td></tr><tr><td><strong>技术门槛消失</strong></td><td>一台普通电脑+开源代码即可生成，无需专业技能</td></tr><tr><td><strong>攻击面持续扩大</strong></td><td>从静态图片→实时视频通话→多模态协同攻击</td></tr><tr><td><strong>迭代速度极快</strong></td><td>新模型（Seedance2.0、Image2、Hunyuan-Video等）不断涌现</td></tr><tr><td><strong>真实环境退化</strong></td><td>压缩、转码、二次拍摄让检测准确率断崖下降</td></tr></tbody></table><p><strong>香港警方的数据更令人警醒</strong>：</p><ul><li>去年7-9月，有8张被盗的香港身份证被用于申请了90份贷款、登记了54个银行账户</li><li>至少有20次，AI Deepfake被用来模仿身份证上的人像，以骗过面部识别程序</li><li>AI技术的不当使用还可能导致个人身份信息的泄露和滥用——通过对个人照片、社交媒体动态和其他公开数据的分析，AI可以生成极为逼真的人脸图像，用于身份冒充，对个人隐私和数据安全构成严重威胁</li></ul><h2 id="识别的”道”：防御反击"><a href="#识别的”道”：防御反击" class="headerlink" title="识别的”道”：防御反击"></a>识别的”道”：防御反击</h2><p>但防御方并非束手无策。检测技术正在形成体系化反击：</p><h3 id="技术层面：四条路径并进"><a href="#技术层面：四条路径并进" class="headerlink" title="技术层面：四条路径并进"></a>技术层面：四条路径并进</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">┌─────────────────────────────────────────────────────────────────────┐</span><br><span class="line">│                    检识技术四条路径                                   │</span><br><span class="line">├──────────────┬──────────────┬──────────────┬────────────────────────┤</span><br><span class="line">│  生理信号    │   频域分析    │  语音反欺骗  │    深度学习检测        │</span><br><span class="line">│  rPPG/Fake   │  FFT/DWT     │  MFCC/呼吸   │    CNN+ViT (TextIn)    │</span><br><span class="line">│  Catcher     │              │  模式分析    │                        │</span><br><span class="line">├──────────────┼──────────────┼──────────────┼────────────────────────┤</span><br><span class="line">│ 94-98%准确率 │ GAN 70-85%   │ 95-97%准确率 │ 域内90%+ / 域外85%+   │</span><br><span class="line">│ 找活人证据   │ 找生成痕迹   │ 找呼吸破绽   │ 强泛化能力             │</span><br><span class="line">└──────────────┴──────────────┴──────────────┴────────────────────────┘</span><br></pre></td></tr></table></figure><h3 id="产业层面：鉴真基础设施成型"><a href="#产业层面：鉴真基础设施成型" class="headerlink" title="产业层面：鉴真基础设施成型"></a>产业层面：鉴真基础设施成型</h3><ul><li><strong>国投智能</strong>：”天擎”多模态大模型底座，可识别近500种伪造手段，准确率93%+</li><li><strong>TextIn</strong>：CNN+ViT框架，域外85%+泛化能力，SaaS+私有化双模式</li><li><strong>Intel FakeCatcher</strong>：毫秒级rPPG检测，94-98%准确率</li><li><strong>美亚鉴真小程序</strong>：接入全国27省120+反诈政务平台</li></ul><h3 id="制度层面：法规全球同步推进"><a href="#制度层面：法规全球同步推进" class="headerlink" title="制度层面：法规全球同步推进"></a>制度层面：法规全球同步推进</h3><ul><li>中国：《深度合成管理规定》（2023）+《AI标识办法》（2025）</li><li>美国：DEFIANCE法案参议院通过，NIST PAD标准制定中</li><li>欧盟：EU AI Act第50条生效，最高罚款1500万欧元或年营收3%</li></ul><h2 id="谁占上风？现实判断"><a href="#谁占上风？现实判断" class="headerlink" title="谁占上风？现实判断"></a>谁占上风？现实判断</h2><table><thead><tr><th>维度</th><th>当前态势</th><th>趋势</th></tr></thead><tbody><tr><td><strong>技术代差</strong></td><td>伪造略领先</td><td>检测在追赶，但扩散模型加大难度</td></tr><tr><td><strong>成本对比</strong></td><td>攻击成本更低</td><td>检测部署成本在下降（SaaS&#x2F;API）</td></tr><tr><td><strong>时间窗口</strong></td><td>攻击者有先手</td><td>检测响应速度在提升（毫秒级）</td></tr><tr><td><strong>规模化</strong></td><td>攻击已规模化</td><td>检测也在规模化（数十亿次调用）</td></tr><tr><td><strong>生态成熟度</strong></td><td>攻击生态更成熟</td><td>防御生态快速成型</td></tr></tbody></table><p><strong>结论：这是一场动态博弈，没有永久赢家。</strong></p><ul><li>短期看，<strong>伪造仍占优势</strong>——新技术总是先于检测出现</li><li>中期看，<strong>攻防趋于平衡</strong>——产业投入加大，检测能力体系化</li><li>长期看，<strong>制度+技术+意识三重防线</strong>将大幅提升攻击成本</li></ul><blockquote><p><strong>关键认知：目标不是”彻底消灭Deepfake”，而是将攻击成本提高到让大多数攻击者望而却步的程度。</strong></p></blockquote><p>正如TextIn所展示的：<strong>技术的进步同样为我们提供了对抗这些风险的有力工具。</strong> TextIn人脸伪造图像检测技术的出现，正是这一对抗的典范。</p><p>在这个充满挑战与机遇的时代，检测技术的不断进步和完善，将更加精准、更加智能，成为我们守护网络安全、保护个人隐私的坚强盾牌，创造更加安全、更加真实的数字世界。</p><hr><h1 id="行业洞察：鉴真技术成为数字信任基础设施"><a href="#行业洞察：鉴真技术成为数字信任基础设施" class="headerlink" title="行业洞察：鉴真技术成为数字信任基础设施"></a>行业洞察：鉴真技术成为数字信任基础设施</h1><p>根据Biometric Update与Goode Intelligence联合发布的《The Deepfake Fraud Detection Market 2026》报告：</p><ul><li>全球深度伪造欺诈检测收入将从2026年的<strong>30.27亿美元</strong>增至2028年的<strong>61.20亿美元</strong></li><li>两年复合增长率约<strong>42.2%</strong></li><li><strong>核心拐点</strong>：深度伪造检测正从”内容安全工具”转变为”身份基础设施中的风险控制能力”</li></ul><p>国内方面，<strong>国投智能</strong>已在深度伪造鉴定领域深耕多年，构建起覆盖<strong>图像与证件、视频与实时通信、语音与电话</strong>三大对象的全面检测鉴定体系：</p><ul><li>自研”天擎”公共安全多模态大模型底座</li><li>六类异构专家引擎融合架构（视觉伪影、自监督特征、频域水印、元数据、编辑痕迹、扩散反演轨迹）</li><li>可识别近<strong>500种</strong>伪造生成手段，平均准确率超<strong>93%</strong></li><li>“美亚鉴真”微信小程序已接入全国<strong>27个省级、超120个</strong>反诈政务平台</li></ul><p><strong>行业正在经历一条与早期人脸识别相似的演进路径：先由厂商自报准确率，随后出现独立基准、实验室测试、标准化指标和采购门槛。</strong></p><hr><h1 id="未来展望：我们该如何重建数字信任？"><a href="#未来展望：我们该如何重建数字信任？" class="headerlink" title="未来展望：我们该如何重建数字信任？"></a>未来展望：我们该如何重建数字信任？</h1><h2 id="技术趋势"><a href="#技术趋势" class="headerlink" title="技术趋势"></a>技术趋势</h2><ol><li><strong>从单模态走向多模态</strong>：攻击者会组合证件、人脸、声音、文本和行为，防御体系必须做相同层级的融合</li><li><strong>从云端离线走向边缘实时</strong>：实时会议和电话要求亚秒级判断，端侧SDK将成为标配</li><li><strong>从已知生成器转向开放集</strong>：实验室高分不等于真实环境稳定，困难样本积累成为核心竞争力</li><li><strong>从风险分数走向可解释证据</strong>：高风险业务需要回答”为什么判定、哪个片段异常”</li><li><strong>从被动鉴伪延伸到来源证明</strong>：内容凭证（C2PA&#x2F;SynthID）与深度检测形成分工</li></ol><h2 id="社会层面的应对"><a href="#社会层面的应对" class="headerlink" title="社会层面的应对"></a>社会层面的应对</h2><ol><li><strong>提升全民意识</strong>：Deepfake不再是遥远的技术话题，而是每个人都可能遇到的现实威胁</li><li><strong>建立质疑文化</strong>：在企业内部和家庭关系中，建立”可以质疑”的安全文化</li><li><strong>推动技术创新</strong>：支持国产鉴真技术发展，减少对单一技术路线的依赖</li><li><strong>完善法律法规</strong>：跟上技术迭代速度，让违法者付出代价</li></ol><hr><h1 id="结语：信任需要新的基石"><a href="#结语：信任需要新的基石" class="headerlink" title="结语：信任需要新的基石"></a>结语：信任需要新的基石</h1><p>Deepfake技术的本质，是<strong>用AI瓦解”眼见为实”这个人类社会的底层信任假设</strong>。</p><p>对企业而言，这意味着传统的身份认证体系需要重构；对个人而言，这意味着我们需要建立新的信任验证习惯。</p><p><strong>当”看见”和”听见”不再足以构成信任，证明真实、识别欺诈并完成可信处置，将成为下一代安全产品的基础能力。</strong></p><p>而我们每个人能做的，是从现在开始——<strong>保持怀疑，多方核实，永远不要让”亲眼所见”成为唯一的判断依据。</strong></p><hr>]]>
    </content>
    <id>http://example.com/2026/08/11/20260811/2026081101/</id>
    <link href="http://example.com/2026/08/11/20260811/2026081101/"/>
    <published>2026-08-11T06:19:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>深度学习的 deepfake 技术不仅可以用来伪造人脸进入公司内部区域，还可以用以攻击个人，对个人进行深度的定制骗局进行精准诈骗。这不是科幻电影，而是正在发生的现实。</p>
</blockquote>
<hr>
<h1 id="DEEPFAKE-]]>
    </summary>
    <title>当&quot;看见&quot;不再可信：Deepfake技术如何攻破企业与个人防线</title>
    <updated>2026-08-11T06:32:32.914Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="网络安全" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="隐私保护" scheme="http://example.com/tags/%E9%9A%90%E7%A7%81%E4%BF%9D%E6%8A%A4/"/>
    <category term="Bluetooth LE Mesh" scheme="http://example.com/tags/Bluetooth-LE-Mesh/"/>
    <category term="Nostr Protocol" scheme="http://example.com/tags/Nostr-Protocol/"/>
    <category term="Noise Framework" scheme="http://example.com/tags/Noise-Framework/"/>
    <category term="P2P" scheme="http://example.com/tags/P2P/"/>
    <category term="E2E Encryption" scheme="http://example.com/tags/E2E-Encryption/"/>
    <category term="iOS / macOS / Android" scheme="http://example.com/tags/iOS-macOS-Android/"/>
    <content>
      <![CDATA[<hr><h2 id="1-它是什么？"><a href="#1-它是什么？" class="headerlink" title="1. 它是什么？"></a>1. 它是什么？</h2><p><strong>bitchat</strong> 是一款由 <em>permissionlesstech</em> 团队开发并开源的去中心化点对点聊天应用。它的核心理念可以用三个词概括：<strong>无服务器、无互联网、无许可</strong>。设备之间通过蓝牙直接组网通信，不需要任何中心化基础设施，不需要注册账号，不需要手机号，甚至不需要有网络信号。</p><p>项目自称 <em>“IRC vibes”</em>，这个定位非常精准。如果你经历过 IRC 的黄金年代——90 年代末到 2000 年代初，那时候的互联网聊天就是这样的：匿名加入一个频道，和陌生人天南海北地聊，关掉窗口一切消失，没有历史记录，没有算法推荐，没有”你可能认识的人”。bitchat 把这种纯粹的体验搬到了蓝牙网络上，而且增加了一个 IRC 从未拥有的能力：<strong>离线可用</strong>。</p><p>但 bitchat 不止于此。与早期版本的纯蓝牙 Mesh 不同，如今的 bitchat 已经演进为<strong>双传输层架构</strong>——蓝牙 Mesh 负责离线本地通信，Nostr 协议负责互联网可达的全局通信。这意味着它不仅能让你在没有信号的野外和附近的人聊天，还能在有网络时连接全球的 bitchat 用户。</p><blockquote><p><strong>一句话总结</strong>：bitchat &#x3D; 蓝牙 Mesh 离线通信 + Nostr 协议全球通信 + Noise 端到端加密 + 零注册零服务器，面向隐私、应急和去中心化场景的开源即时通讯应用。</p></blockquote><h3 id="想象这样一个场景"><a href="#想象这样一个场景" class="headerlink" title="想象这样一个场景"></a>想象这样一个场景</h3><p>bitchat 没有基站、没有服务器、没有验证码，连手机号都用不上，纯靠空气里乱飞的 2.4GHz 无线电波。某地大型活动突然”断网”，人群里有人把”出口在西北角”这条消息丢进 bitchat，不到三分钟，几千部手机像多米诺骨牌一样完成复制，恐慌瞬间降温。</p><p>原理并不玄乎：低功耗蓝牙（BLE）一次能蹦 100 米，每部手机都是一个小小驿站。消息被拆成加密碎片，像裹了黑袋子的快递，谁接力谁都不知道里面装的是情书还是欠条。理论上，从洛杉矶到纽约 3940 公里，需要 3.94 万个”百米冲刺”，只要人口够密，节点够多，情书就能在几小时内”蛙跳”抵达。沙漠、草原、深山老林这种”手机荒”地带，快递也会堵车，但城市里最不缺的就是”人肉蓝牙桩”——地铁闸机口、奶茶店、共享单车站台，处处都是潜在”中继小哥”。</p><p>速度呢？同城基本秒到，跨省几分钟到几小时不等。听上去比 5G”拉胯”，可别忘了，它天生免疫”拔网线”。当运营商基站罢工、当 Wi-Fi 路由死机、当某些地区进入”维护模式”，它反而越战越勇——节点越多，带宽越大，像极了那句老话：”人民战争海洋深。”</p><p>隐私方面，bitchat 把”社恐”写进基因：没有账号，没有头像，没有云端备份，连聊天记录都只存在自己手机里。别人想查？先破解端到端加密，再凑齐整条蓝牙链路上每一部手机，难度堪比把撒出去的盐一颗颗捡回来。</p><p>打开手机蓝牙，把消息扔进人潮，让陌生人帮你完成一次”赛博传烽”——这份浪漫，0 元不限量，还附赠满满成就感。</p><p><strong>标签</strong>：<code>Bluetooth LE Mesh</code> <code>Nostr Protocol</code> <code>Noise Framework</code> <code>P2P</code> <code>E2E Encryption</code> <code>iOS / macOS / Android</code></p><hr><h2 id="2-双传输层架构"><a href="#2-双传输层架构" class="headerlink" title="2. 双传输层架构"></a>2. 双传输层架构</h2><p>bitchat 最独特的设计在于它的<strong>双传输层（Dual Transport）架构</strong>。两个传输层实现同一个 <code>Transport</code> 接口，由一个 <code>MessageRouter</code> 统一协调：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">      ┌─────────────────────────┐</span><br><span class="line">      │    Application Layer     │</span><br><span class="line">      └────────────┬────────────┘</span><br><span class="line">                   │</span><br><span class="line">      ┌────────────▼────────────┐</span><br><span class="line">      │      MessageRouter       │</span><br><span class="line">      │  (智能传输选择 &amp; 路由)    │</span><br><span class="line">      └─────┬─────────────┬──────┘</span><br><span class="line">            │             │</span><br><span class="line">Bluetooth 优先          Nostr 回退</span><br><span class="line">      ┌─────▼─────┐ ┌─────▼─────┐</span><br><span class="line">      │  BLE Mesh │ │  Nostr    │</span><br><span class="line">      │  Network  │ │  Protocol │</span><br><span class="line">      │           │ │           │</span><br><span class="line">      │ · GATT    │ │ · 440+    │</span><br><span class="line">      │   Central │ │   中继    │</span><br><span class="line">      │   +Periph │ │ · geohash │</span><br><span class="line">      │ · 7 hops  │ │   频道    │</span><br><span class="line">      │ · Noise XX│ │ · XChaCha │</span><br><span class="line">      └───────────┘ └───────────┘</span><br><span class="line">     离线·本地通信   在线·全球通信</span><br></pre></td></tr></table></figure><p>路由器的决策逻辑非常清晰：</p><ol><li><strong>蓝牙优先</strong> —— 当蓝牙链路可用时，直接使用 Noise 加密会话发送，最快且最私密</li><li><strong>Nostr 回退</strong> —— 蓝牙不可达时，通过接收者的 Nostr 公钥，经全球中继网络路由</li><li><strong>智能排队</strong> —— 当两者都不可用时，消息进入队列，等待传输恢复后自动投递</li></ol><hr><h2 id="3-蓝牙-Mesh-网络"><a href="#3-蓝牙-Mesh-网络" class="headerlink" title="3. 蓝牙 Mesh 网络"></a>3. 蓝牙 Mesh 网络</h2><p>bitchat 没有使用蓝牙技术联盟（Bluetooth SIG）官方的 Mesh 规范——那套规范主要面向 IoT 场景（智能灯泡、传感器网络），协议栈较重，对移动设备的功耗和延迟并不友好。相反，bitchat 在 BLE（低功耗蓝牙）的 GATT 和广播通道之上，自己实现了一套轻量级的 Mesh 路由协议。</p><h3 id="每台设备既是客户端，也是中继"><a href="#每台设备既是客户端，也是中继" class="headerlink" title="每台设备既是客户端，也是中继"></a>每台设备既是客户端，也是中继</h3><p>在 bitchat 的 Mesh 网络中，每台设备同时扮演 <strong>GATT Central</strong> 和 <strong>Peripheral</strong> 两个角色。当你发送一条消息，它不只发给附近的设备，还会被范围内的其他设备接收并转发，像接力赛一样一跳一跳地传递出去。只要中间有足够的设备作为中继，消息可以传递到远超蓝牙标称距离（通常 10-30 米）的地方。</p><h3 id="受控泛洪-去重"><a href="#受控泛洪-去重" class="headerlink" title="受控泛洪 + 去重"></a>受控泛洪 + 去重</h3><p>bitchat 采用了”受控泛洪 + 去重”的经典 Mesh 策略，但做了精细的调优：</p><ul><li><strong>TTL 控制</strong>：消息初始 TTL 为 7。密集网络（≥6 条连接）时 TTL 被限制为 5，稀疏链路（≤2 条连接）则保留完整深度</li><li><strong>去重</strong>：LRU 去重集（1000 条目，5 分钟过期），按发送者、时间戳、类型和负载摘要作为键值丢弃重复消息</li><li><strong>抖动</strong>：中继节点等待随机 10-220ms（密集网络时更长），让去重机制经常能抢占先机</li><li><strong>扇出子集</strong>：广播消息只发送给确定性子集（约 log₂(连接度)），而非全部链路，大幅降低冗余流量</li></ul><h3 id="分片重组"><a href="#分片重组" class="headerlink" title="分片重组"></a>分片重组</h3><p>超过链路 MTU 的数据包会被切分为约 469 字节的分片，每个分片独立中继，在接收端重组（支持 128 个并发重组，30 秒超时，1 MiB 上限）。这意味着即使大文件也能在蓝牙 Mesh 中可靠传输。</p><h3 id="拓扑感知路由"><a href="#拓扑感知路由" class="headerlink" title="拓扑感知路由"></a>拓扑感知路由</h3><p>公告消息携带最多 10 个直连邻居 ID，为每个节点构建一张浅层拓扑图（60 秒新鲜度）。当存在双向确认的路径时，消息会沿路径源路由；否则回退到泛洪。这种混合策略在效率和健壮性之间取得了平衡。</p><hr><h2 id="4-Nostr-协议层"><a href="#4-Nostr-协议层" class="headerlink" title="4. Nostr 协议层"></a>4. Nostr 协议层</h2><p>当蓝牙不可用时，bitchat 通过 <strong>Nostr 协议</strong>实现全球通信。Nostr（Notes and Other Stuff Transmitted by Relays）是一个简单、开放的协议，通过一组分布式中继节点传递消息。bitchat 内置了 440+ 个全球中继节点的目录，支持按地理位置智能选择。</p><h3 id="位置频道（Location-Channels）"><a href="#位置频道（Location-Channels）" class="headerlink" title="位置频道（Location Channels）"></a>位置频道（Location Channels）</h3><p>bitchat 利用 <strong>geohash</strong>（地理哈希）编码创建了分层的位置频道系统，这是一种将经纬度编码为短字符串的方法：</p><table><thead><tr><th>频道级别</th><th>Geohash 精度</th><th>覆盖范围</th></tr></thead><tbody><tr><td><code>block</code></td><td>7 字符</td><td>城市街区级</td></tr><tr><td><code>neighborhood</code></td><td>6 字符</td><td>社区&#x2F;区域级</td></tr><tr><td><code>city</code></td><td>5 字符</td><td>城市级</td></tr><tr><td><code>province</code></td><td>4 字符</td><td>省&#x2F;州级</td></tr><tr><td><code>region</code></td><td>2 字符</td><td>国家&#x2F;大区域级</td></tr></tbody></table><p>这意味着你可以加入你所在街区的频道和邻居聊天，也可以切换到城市级频道参与更广泛的讨论。每个 geohash 区域使用独立的临时加密密钥，增强了隐私性。</p><h3 id="私有信封"><a href="#私有信封" class="headerlink" title="私有信封"></a>私有信封</h3><p>bitchat 的 Nostr 私有消息使用了一套<strong>自有的加密信封格式</strong>，而非标准的 NIP-17&#x2F;NIP-44&#x2F;NIP-59。它复用了 NIP-17 的 kind 编号但不兼容：</p><ul><li>内部消息（kind 14）被加密后放入发送者签名的封缄（kind 13）</li><li>封缄再次被加密放入由一次性密钥签名的公开信封（kind 1059）</li><li>每个加密字段使用 <code>v2:</code> 前缀 + base64url 编码的 24 字节 nonce + XChaCha20-Poly1305 密文 + 16 字节认证标签</li><li>密钥来自 secp256k1 ECDH 和 HKDF-SHA256 派生</li><li>时间戳随机偏移 ±15 分钟，防止时间关联分析</li></ul><blockquote><p>⚠️ <strong>注意</strong>：Nostr 路径<strong>没有前向保密（Forward Secrecy）</strong>——如果接收者的静态 Nostr 私钥被泄露，存储在中继上的加密信封可能被解密。这是当前设计的已知限制，prekey 方案正在规划中。</p></blockquote><hr><h2 id="5-加密与安全"><a href="#5-加密与安全" class="headerlink" title="5. 加密与安全"></a>5. 加密与安全</h2><p>在去中心化通信中，端到端加密不是可选项，而是必选项。bitchat 使用 <strong>Noise Protocol Framework</strong>（Noise 协议框架）——这与 Signal、WhatsApp 等使用的加密基础框架同源，但实现了更精细的控制。</p><h3 id="实时会话：Noise-XX-模式"><a href="#实时会话：Noise-XX-模式" class="headerlink" title="实时会话：Noise XX 模式"></a>实时会话：Noise XX 模式</h3><p>已连接的设备之间使用 Noise <code>XX</code> 握手模式建立会话，提供双向认证和前向保密：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">// Noise XX 握手模式</span><br><span class="line">XX:</span><br><span class="line">  -&gt; e                              // 发起方发送临时密钥</span><br><span class="line">  &lt;- e, ee, s, es                   // 响应方发送临时密钥 + 加密的静态密钥</span><br><span class="line">  -&gt; s, se                          // 发起方发送加密的静态密钥，完成握手</span><br><span class="line"></span><br><span class="line">// 加密原语</span><br><span class="line">DH:     X25519 (Curve25519)</span><br><span class="line">Cipher: ChaCha20-Poly1305 (AEAD)</span><br><span class="line">Hash:   SHA-256</span><br><span class="line">KDF:    HKDF-SHA256</span><br><span class="line"></span><br><span class="line">// 自动重新密钥：每 1 小时或 10,000 条消息</span><br></pre></td></tr></table></figure><p>三步握手提供：<strong>双向认证</strong>（双方确认对方身份）、<strong>身份隐藏</strong>（身份密钥在握手过程中被加密）、<strong>抗密钥泄露冒充</strong>（即使你的密钥泄露，也无法冒充其他人）。</p><h3 id="离线封缄：Noise-X-模式"><a href="#离线封缄：Noise-X-模式" class="headerlink" title="离线封缄：Noise X 模式"></a>离线封缄：Noise X 模式</h3><p>对于离线投递的”信使邮件”，bitchat 使用单向 Noise <code>X</code> 模式封缄——发送者身份在密文内部认证，但<strong>这条路径没有前向保密</strong>。如果接收者的静态密钥被泄露，未投递的密封邮件可能暴露。</p><h3 id="加密层级总结"><a href="#加密层级总结" class="headerlink" title="加密层级总结"></a>加密层级总结</h3><table><thead><tr><th>路径</th><th>加密方案</th><th>前向保密</th></tr></thead><tbody><tr><td>蓝牙实时会话</td><td>Noise XX (X25519 &#x2F; ChaCha20-Poly1305)</td><td>✅ 有</td></tr><tr><td>信使离线邮件</td><td>Noise X (静态密钥封缄)</td><td>❌ 无</td></tr><tr><td>Nostr 私有消息</td><td>XChaCha20-Poly1305 + secp256k1 ECDH</td><td>❌ 无</td></tr><tr><td>公开广播消息</td><td>Ed25519 签名（明文 + 签名验证）</td><td>— 公开内容</td></tr></tbody></table><hr><h2 id="6-存储转发：消息一定能送达"><a href="#6-存储转发：消息一定能送达" class="headerlink" title="6. 存储转发：消息一定能送达"></a>6. 存储转发：消息一定能送达</h2><p>bitchat 面临的核心挑战是：<strong>接收者此刻不在范围内怎么办？</strong> 为此，它设计了一套四层存储转发机制：</p><h3 id="📮-发送者发件箱"><a href="#📮-发送者发件箱" class="headerlink" title="📮 发送者发件箱"></a>📮 发送者发件箱</h3><p>未投递的私信保留在本地（每对等方 100 条，24 小时 TTL），重新连接时自动重发，直到收到投递&#x2F;已读回执或达到 8 次重试上限。发件箱以 ChaChaPoly 密钥密封持久化到磁盘，即使应用被杀也只存密文。</p><h3 id="🏃-信使系统（Couriers）"><a href="#🏃-信使系统（Couriers）" class="headerlink" title="🏃 信使系统（Couriers）"></a>🏃 信使系统（Couriers）</h3><p>当没有传输路径时，消息被封缄后交给最多 3 个可能遇到接收者的连接节点携带。信使看不到发送者、接收者和内容——唯一的路由信息是一个 16 字节的每日轮换标签。</p><p>信使系统借鉴了 DTN（延迟容忍网络）中的 <strong>spray-and-wait</strong> 策略：</p><ul><li><strong>不透明寻址</strong>：信使只能看到一个 16 字节的轮换标签（接收者静态密钥和 UTC 日期的 HMAC），不知道发送者、接收者和内容</li><li><strong>信任分级</strong>：互信好友可存放 5 个信封，任何签名验证的节点可存放 2 个，且好友邮件有保留配额</li><li><strong>扩散传播</strong>：信封携带副本预算（初始 4，上限 8），信使遇到其他合格信使时交出一半预算，让邮件在移动的人群中扩散而非只骑在一个人身上</li><li><strong>投递确认</strong>：收到接收者的直接公告时投递并删除；收到中继公告时定向泛洪一份副本</li></ul><blockquote><p>想象一下：你在音乐节上给朋友发消息，朋友不在蓝牙范围内。你的消息被密封后交给了附近的 3 个陌生人携带。当其中任何一个人走到你朋友附近时，消息就会自动投递——而这些陌生人全程看不到消息内容，甚至不知道消息是发给谁的。</p></blockquote><h3 id="🔄-公共历史同步"><a href="#🔄-公共历史同步" class="headerlink" title="🔄 公共历史同步"></a>🔄 公共历史同步</h3><p>公共广播消息缓存 1000 条，节点之间每 ~15 秒用紧凑的 GCS 过滤器对账同步，保留窗口 6 小时。走过两个分区的设备会把房间最近历史带给错过的人。缓存持久化到磁盘，设备重启后仍然可服务。</p><h3 id="🌐-Nostr-邮箱"><a href="#🌐-Nostr-邮箱" class="headerlink" title="🌐 Nostr 邮箱"></a>🌐 Nostr 邮箱</h3><p>bitchat 私有信封存储在 Nostr 中继上，客户端重连时回溯 24 小时，覆盖双方设备同时离线的场景——只要任一方触及互联网就能收信。</p><hr><h2 id="7-身份与隐私"><a href="#7-身份与隐私" class="headerlink" title="7. 身份与隐私"></a>7. 身份与隐私</h2><h3 id="密钥即身份"><a href="#密钥即身份" class="headerlink" title="密钥即身份"></a>密钥即身份</h3><p>bitchat 没有账号系统。每台设备在 Keychain 中保存两对长期密钥：</p><ul><li><strong>Curve25519 静态密钥</strong> —— 用于 Noise 密钥协商，其 SHA-256 指纹是设备的稳定身份标识</li><li><strong>Ed25519 签名密钥</strong> —— 用于数据包签名</li></ul><p>在 Mesh 网络中，设备以 8 字节的短 Peer ID 出现——它是 Noise 静态密钥 SHA-256 指纹的前 8 字节。这个 ID <strong>不是临时的</strong>：它在会话、重启和保留 Keychain 的重装之间保持稳定，只有在紧急擦除更换身份时才会改变。</p><h3 id="紧急擦除（Panic-Wipe）"><a href="#紧急擦除（Panic-Wipe）" class="headerlink" title="紧急擦除（Panic Wipe）"></a>紧急擦除（Panic Wipe）</h3><p>bitchat 提供了一个极端的隐私保护功能：<strong>三击触发紧急擦除</strong>，立即清除所有数据——身份密钥、好友列表、携带的信使邮件、加密发件箱、公共历史归档和度量指标。一切归零，仿佛你从未存在。</p><h3 id="Tor-集成"><a href="#Tor-集成" class="headerlink" title="Tor 集成"></a>Tor 集成</h3><p>bitchat 内置了 <strong>Arti</strong>（Rust 编写的下一代 Tor 客户端）集成。启用后，所有 Nostr 流量通过 Tor 网络路由，进一步隐藏 IP 地址和网络元数据。这是从蓝牙层的物理隐私到互联网层的网络隐私的完整覆盖。</p><blockquote><p>💡 <strong>诚实的隐私披露</strong>：bitchat 的白皮书坦诚地指出了当前设计的隐私弱点：8 字节 Peer ID 派生自不轮换的密钥，公告以明文发布静态密钥和昵称，因此被动监听者可以枚举参与者并跨地点追踪设备。轮换的空中身份是正在规划的改进方向。</p></blockquote><hr><h2 id="8-核心功能一览"><a href="#8-核心功能一览" class="headerlink" title="8. 核心功能一览"></a>8. 核心功能一览</h2><table><thead><tr><th>功能</th><th>说明</th></tr></thead><tbody><tr><td>💬 <strong>IRC 风格命令</strong></td><td>支持 <code>/slap</code>、<code>/msg</code>、<code>/who</code> 等经典 IRC 命令，怀旧感拉满</td></tr><tr><td>🎙️ <strong>语音消息 &amp; PTT</strong></td><td>支持语音笔记录制和 Push-to-Talk（按讲）模式，语音帧通过 Mesh 分片传输</td></tr><tr><td>🖼️ <strong>媒体传输</strong></td><td>图片和文件通过 Mesh 分片传输（1 MiB 上限），接收前需明确确认</td></tr><tr><td>📍 <strong>地理频道</strong></td><td>基于 geohash 的分层位置频道，从街区到国家级别自由切换</td></tr><tr><td>🔑 <strong>好友验证</strong></td><td>通过 QR 码扫描进行面对面身份验证，将昵称绑定到指纹</td></tr><tr><td>⚡ <strong>Cashu 集成</strong></td><td>支持 Cashu（基于 Lightning Network 的代币协议）的解码与处理</td></tr><tr><td>🔒 <strong>紧急擦除</strong></td><td>三击清除所有数据，一键归零，关键时刻的保命功能</td></tr><tr><td>🌐 <strong>Tor 路由</strong></td><td>内置 Arti 引擎，所有 Nostr 流量可选通过 Tor 匿名路由</td></tr><tr><td>⚡ <strong>LZ4 压缩</strong></td><td>消息使用 LZ4 压缩，自适应电池模式优化功耗</td></tr></tbody></table><hr><h2 id="9-应用场景"><a href="#9-应用场景" class="headerlink" title="9. 应用场景"></a>9. 应用场景</h2><p>bitchat 的价值不止于”极客玩具”。在以下场景中，它可能是目前最优雅的解决方案：</p><h3 id="灾难应急通信"><a href="#灾难应急通信" class="headerlink" title="灾难应急通信"></a>灾难应急通信</h3><p>地震、洪水等灾害发生后，蜂窝网络往往在数小时内瘫痪。救援队伍和受灾群众可以使用 bitchat 建立临时通信网络——不需要任何预先部署，打开应用就能通信。Mesh 的自组网特性意味着人越多网络越强壮。</p><h3 id="户外活动与探险"><a href="#户外活动与探险" class="headerlink" title="户外活动与探险"></a>户外活动与探险</h3><p>徒步、登山、滑雪——这些场景通常没有手机信号。传统方案是对讲机，但 bitchat 通过 Mesh 中继扩展通信距离，支持文字消息传递坐标、路线等精确信息，远比对讲机的语音通话可靠。</p><h3 id="大型活动与集会"><a href="#大型活动与集会" class="headerlink" title="大型活动与集会"></a>大型活动与集会</h3><p>音乐节、体育赛事——当数万人聚集，蜂窝网络因过载而崩溃。bitchat 不依赖蜂窝网络，人越多 Mesh 网络反而越强壮（中继节点更多），天然适合高密度人群场景。</p><h3 id="隐私敏感通信"><a href="#隐私敏感通信" class="headerlink" title="隐私敏感通信"></a>隐私敏感通信</h3><p>记者、律师、活动人士——bitchat 的”无服务器”架构意味着没有元数据可以被收集，没有通信记录可以被调取。可选的 Tor 路由进一步隐藏网络踪迹。在”数据即权力”的时代，这种架构本身就是一种立场。</p><hr><h2 id="10-代码库巡礼"><a href="#10-代码库巡礼" class="headerlink" title="10. 代码库巡礼"></a>10. 代码库巡礼</h2><p>bitchat 是一个工程上非常成熟的项目。以下是来自实际源码仓库的数据：</p><table><thead><tr><th>维度</th><th>数据</th></tr></thead><tbody><tr><td>开发语言</td><td>Swift（主）+ Rust（Arti Tor 引擎）</td></tr><tr><td>源代码文件</td><td>266 个 Swift 源文件</td></tr><tr><td>测试文件</td><td>201 个测试文件（~1,964 个测试用例）</td></tr><tr><td>代码总量</td><td>~42,722 行 Swift 代码</td></tr><tr><td>测试覆盖率</td><td>全套测试在迁移过程中全程绿色</td></tr><tr><td>Nostr 中继</td><td>441 个全球中继节点（含 GPS 坐标）</td></tr><tr><td>许可证</td><td>公共领域（Unlicense）—— 完全自由使用</td></tr><tr><td>平台</td><td>iOS 16+ &#x2F; macOS 13+ &#x2F; Android（Play Store）</td></tr><tr><td>架构文档</td><td>13 篇专题设计文档（含白皮书 v2.0）</td></tr></tbody></table><h3 id="架构演进"><a href="#架构演进" class="headerlink" title="架构演进"></a>架构演进</h3><p>bitchat 的代码库经历了清晰的架构演进。从 V1 的单体 <code>BLEService</code>（8,300+ 行的”上帝对象”），到 V2 的应用层解耦（引入 <code>AppRuntime</code> 组合根、<code>ConversationStore</code> 和多个特性模型），再到 V3 的传输层重构（将 BLE 拆分为 <code>BLELinkLayer</code> + Mesh Engine + 特性模块的分层架构），每一步都有详细的设计文档记录。</p><p>特别值得注意的是项目的**模拟器优先（Simulator-First）**测试策略：通过 <code>SimulatedLinkLayer</code> 实现 CB-free 的多节点测试拓扑，5 个确定性多节点测试在 ~40ms 内完成——包括公告&#x2F;绑定收敛、端到端 Noise 建立、线型拓扑中继、重复泛洪去重和紧急轮换单槽重绑等场景。模拟器上线第一天就发现了一个真实 bug。</p><h3 id="工程严谨度"><a href="#工程严谨度" class="headerlink" title="工程严谨度"></a>工程严谨度</h3><p>从代码库可以看出项目团队对工程质量的极高要求：</p><ul><li><strong>队列契约测试</strong>：<code>BLEQueueContractTests</code> 通过 grep 源码确保只有 <code>onEngine</code> 可以同步进入引擎队列，传输代码永远不会同步分发到主队列</li><li><strong>SwiftLint + Periphery</strong>：代码风格和死代码检测自动化</li><li><strong>Justfile 构建系统</strong>：标准化构建、测试和运行流程</li><li><strong>构建验证文档</strong>：详细的 <code>VERIFYING-A-BUILD.md</code> 指导用户如何验证源码与发布哈希清单的一致性</li><li><strong>隐私评估文档</strong>：独立的 <code>privacy-assessment.md</code> 坦诚披露已知隐私局限</li></ul><h3 id="快速上手"><a href="#快速上手" class="headerlink" title="快速上手"></a>快速上手</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 使用 Xcode 打开</span></span><br><span class="line">open bitchat.xcodeproj</span><br><span class="line"></span><br><span class="line"><span class="comment"># macOS Debug 构建（无需签名）</span></span><br><span class="line">xcodebuild -project bitchat.xcodeproj -scheme <span class="string">&quot;bitchat (macOS)&quot;</span> \</span><br><span class="line">  -configuration Debug CODE_SIGNING_ALLOWED=NO build</span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行完整 SwiftPM 测试套件</span></span><br><span class="line">swift <span class="built_in">test</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># iOS 模拟器测试</span></span><br><span class="line">xcodebuild -project bitchat.xcodeproj -scheme <span class="string">&quot;bitchat (iOS)&quot;</span> \</span><br><span class="line">  -sdk iphonesimulator \</span><br><span class="line">  -destination <span class="string">&#x27;platform=iOS Simulator,name=iPhone 17&#x27;</span> <span class="built_in">test</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用 just 命令</span></span><br><span class="line">brew install just</span><br><span class="line">just check</span><br><span class="line">just run</span><br></pre></td></tr></table></figure><hr><h2 id="11-未来已来"><a href="#11-未来已来" class="headerlink" title="11. 未来已来"></a>11. 未来已来</h2><h3 id="当前挑战"><a href="#当前挑战" class="headerlink" title="当前挑战"></a>当前挑战</h3><blockquote><p> <strong>物理限制</strong>：蓝牙 Mesh 的多跳传输依赖节点密度。在人口稀疏的郊区或野外，设备之间的距离可能远超蓝牙通信范围，Mesh 网络就会断裂。bitchat 不是”无限距离通信”，它的覆盖范围本质上受制于”你附近有多少人也在用 bitchat”。</p></blockquote><blockquote><p><strong>元数据暴露</strong>：8 字节 Peer ID 派生自不轮换的密钥，公告以明文发布静态密钥和昵称。被动监听者可以枚举参与者并跨地点追踪设备，甚至通过公告中携带的邻居列表获取局部邻接图。这是白皮书坦诚承认的”设计中最薄弱的部分”。</p></blockquote><blockquote><p><strong>内容审核</strong>：去中心化意味着没有中心节点来过滤违法内容。这在隐私保护上是绝对的优势。bitchat 目前的选择是”不处理”。</p></blockquote><h3 id="未来路线图"><a href="#未来路线图" class="headerlink" title="未来路线图"></a>未来路线图</h3><p>bitchat 的白皮书和架构文档列出了明确的改进方向：</p><ul><li><strong>Prekey 前向保密</strong> —— 为信使邮件引入 prekey 机制，消除离线路径的前向保密缺失</li><li><strong>轮换空中身份</strong> —— 纪元轮换 Peer ID，静态密钥在加密握手内披露，好友通过共享密钥派生标签相互识别</li><li><strong>非 Noise 包填充</strong> —— 当前只有 Noise 帧有填充，其他包类型的负载长度可观测</li><li><strong>多跳信使路由</strong> —— 基于相遇历史的多跳信使路由</li><li><strong>后量子就绪</strong> —— 混合握手模式和 Kyber 集成计划</li><li><strong>多设备支持</strong> —— 会话备份&#x2F;恢复和组消息原语</li></ul><blockquote><p>未来的胡思乱想： 在解决了安全性和隐私性的问题之后 如果这东西中的某一个节点可以作为梯子 也许可以作为 一个不受任何政府监管的备用网络&#x2F;隐秘网络。</p></blockquote><hr><h2 id="结语：通信正在回归设备本身"><a href="#结语：通信正在回归设备本身" class="headerlink" title="结语：通信正在回归设备本身"></a>结语：通信正在回归设备本身</h2><p>过去十五年，我们习惯了把一切交给云端——消息存在服务器上，联系人存在服务器上，聊天记录存在服务器上。bitchat 说：<strong>不，通信可以只发生在设备之间。</strong></p><p>bitchat 的爆发可能只是一个开始。随着蓝牙 6.0 规范的推进和 UWB（超宽带）技术在手机上的普及，”设备直连通信”的体验将会大幅提升。未来的 bitchat 可能不再局限于蓝牙，而是融合 Wi-Fi Aware、UWB 甚至卫星直连等多种无线技术，构建一个真正的多模态 Mesh 网络。</p><p>更长远地看，bitchat 代表的是一种趋势：<strong>通信正在从”云服务”回归”设备本身”</strong>。如果这种理念成为主流，改变的不仅是一个聊天工具，而是整个通信产业的架构逻辑。</p><blockquote><p><em>It’s the side-groupchat.</em> —— bitchat</p></blockquote><hr><p><strong>参考链接</strong>：</p><ul><li><a href="https://github.com/permissionlesstech/bitchat">GitHub 仓库</a></li><li><a href="http://bitchat.free/">官网</a></li><li><a href="https://apps.apple.com/us/app/bitchat-mesh/id6748219622">App Store</a></li><li><a href="https://play.google.com/store/apps/details?id=com.bitchat.droid">Play Store</a></li><li><a href="https://github.com/permissionlesstech/bitchat/blob/main/WHITEPAPER.md">技术白皮书</a></li></ul><hr><p><em>本文基于 bitchat 源码仓库及公开资料编写 · 2026 年 8 月</em></p>]]>
    </content>
    <id>http://example.com/2026/08/04/20260715/2026080401/</id>
    <link href="http://example.com/2026/08/04/20260715/2026080401/"/>
    <published>2026-08-04T14:48:22.000Z</published>
    <summary>
      <![CDATA[<hr>
<h2 id="1-它是什么？"><a href="#1-它是什么？" class="headerlink" title="1. 它是什么？"></a>1. 它是什么？</h2><p><strong>bitchat</strong> 是一款由 <em>permissio]]>
    </summary>
    <title>bitchat：连服务器都没有，拿什么监管你？——去中心化、免疫审查的蓝牙 Mesh 即时聊天软件</title>
    <updated>2026-08-04T15:12:17.577Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="网络安全" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="GrapheneOS" scheme="http://example.com/tags/GrapheneOS/"/>
    <category term="手机取证" scheme="http://example.com/tags/%E6%89%8B%E6%9C%BA%E5%8F%96%E8%AF%81/"/>
    <category term="隐私保护" scheme="http://example.com/tags/%E9%9A%90%E7%A7%81%E4%BF%9D%E6%8A%A4/"/>
    <content>
      <![CDATA[<blockquote><p><strong>场景定位</strong>：一台不插 SIM 卡、不登实名账号的 GrapheneOS Pixel 7 隐私副机，所有流量经国内云服务器加密转发，对外只暴露云服务器身份。</p><p><strong>合规边界</strong>：全程纯国内网络，不跨境转发、不访问境外互联网，仅用于防范 App 画像、公共 WiFi 窃听与运营商 DNS 窥探。本方案对司法与国家级溯源<strong>无防护能力</strong>（链路全程实名），请勿用于任何违法或对抗调查的用途。</p></blockquote><h2 id=""><a href="#" class="headerlink" title=""></a><img src="/../images/2026072501/1b1caa21ec0c7998.jpg" alt="1b1caa21ec0c7998.jpg"></h2><h2 id="一、先想清楚：这套方案防得住谁"><a href="#一、先想清楚：这套方案防得住谁" class="headerlink" title="一、先想清楚：这套方案防得住谁"></a>一、先想清楚：这套方案防得住谁</h2><p>在动手之前，必须搞清楚威胁模型，否则容易把”加密转发”误当成”匿名隐身”。</p><table><thead><tr><th>威胁类型</th><th>是否有效</th><th>说明</th></tr></thead><tbody><tr><td>App 画像 &#x2F; 大数据杀熟</td><td>✅ 有效</td><td>随机 MAC + 权限隔离 + 沙盒</td></tr><tr><td>公共 WiFi 窃听 &#x2F; 中间人</td><td>✅ 有效</td><td>WireGuard 加密隧道</td></tr><tr><td>运营商 DNS 窥探 &#x2F; 劫持</td><td>✅ 有效</td><td>DoH 加密域名查询</td></tr><tr><td>基站三角定位</td><td>✅ 有效</td><td>全程不插 SIM 卡</td></tr><tr><td>云服务商</td><td>⚠️ 部分可见</td><td>HTTPS 下不见内容，但见目标 IP 与 SNI</td></tr><tr><td>司法调查 &#x2F; 国家级取证</td><td>⚠️ 部分可见</td><td>输入胁迫密码，可以瞬间擦除密钥 令手机内的数据变为无法被破解的乱码</td></tr><tr><td>登录实名账号</td><td>❌ 瞬间瓦解</td><td>一次实名登录即暴露身份</td></tr></tbody></table><p><strong>一句话</strong>：这套方案的实质是”对商业追踪 + 本地窃听的加密转发”，不是真正匿名。但它在这两层是有效的。</p><hr><h2 id="二、架构总览"><a href="#二、架构总览" class="headerlink" title="二、架构总览"></a>二、架构总览</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">[GrapheneOS 副机]  ──WiFi──▶  [随身 WiFi]  ──4G──▶  [国内公网]</span><br><span class="line">  不插 SIM / 不登实名账号                              │</span><br><span class="line">                                                      ▼</span><br><span class="line">                                         [国内云服务器 Ubuntu]</span><br><span class="line">                                            WireGuard 服务端</span><br><span class="line">                                                      │</span><br><span class="line">                                                      ▼</span><br><span class="line">                                              [国内目标网站 / App]</span><br></pre></td></tr></table></figure><p><strong>数据流向</strong>：手机 → 随身 WiFi → WireGuard 加密隧道 → 云服务器 → 目标。目标网站只能看到云服务器 IP，看不到你的真实位置与出口 IP。</p><hr><h2 id="三、第一步：选一台能刷-GrapheneOS-的手机"><a href="#三、第一步：选一台能刷-GrapheneOS-的手机" class="headerlink" title="三、第一步：选一台能刷 GrapheneOS 的手机"></a>三、第一步：选一台能刷 GrapheneOS 的手机</h2><h3 id="3-1-GrapheneOS-官方支持的机型"><a href="#3-1-GrapheneOS-官方支持的机型" class="headerlink" title="3.1 GrapheneOS 官方支持的机型"></a>3.1 GrapheneOS 官方支持的机型</h3><p>GrapheneOS <strong>只能安装在 Google Pixel 系列上</strong>——它需要 Titan&#x2F;Titan M2 安全芯片、可解锁的 bootloader 以及完整的 Verified Boot 链，这些是其他品牌无法复制的硬件前提。</p><p>截至 2026 年 7 月，官方生产支持（production support）列表：</p><table><thead><tr><th>世代</th><th>机型</th><th>支持周期</th><th>硬件内存标记(MTE)</th></tr></thead><tbody><tr><td>Pixel 10 系列</td><td>10 &#x2F; 10 Pro &#x2F; 10 Pro XL &#x2F; 10 Pro Fold</td><td>7 年</td><td>✅</td></tr><tr><td>Pixel 9 系列</td><td>9 &#x2F; 9 Pro &#x2F; 9 Pro XL &#x2F; 9 Pro Fold &#x2F; 9a</td><td>7 年</td><td>✅</td></tr><tr><td>Pixel 8 系列</td><td>8 &#x2F; 8 Pro &#x2F; 8a</td><td>7 年</td><td>✅</td></tr><tr><td>Pixel 7 系列</td><td>7 &#x2F; 7 Pro &#x2F; 7a</td><td>5 年</td><td>❌</td></tr><tr><td>Pixel 6 系列</td><td>6 &#x2F; 6 Pro &#x2F; 6a</td><td>5 年（已进入扩展支持期）</td><td>❌</td></tr></tbody></table><blockquote><p><strong>重要</strong>：Pixel 8 及以后采用 ARMv9 内核，支持硬件内存标记（MTE）。GrapheneOS 默认开启 MTE 防护基线，能拦截大量内存漏洞利用，这是 Pixel 7 及以前没有的特性。预算允许尽量上 8 代以后。</p></blockquote><h3 id="3-2-闲鱼-淘宝二手参考行情（2026-年-7-月）"><a href="#3-2-闲鱼-淘宝二手参考行情（2026-年-7-月）" class="headerlink" title="3.2 闲鱼 &#x2F; 淘宝二手参考行情（2026 年 7 月）"></a>3.2 闲鱼 &#x2F; 淘宝二手参考行情（2026 年 7 月）</h3><p>下表为综合国际二手行情换算与国内买手店零售价估算的区间，<strong>实际成交以当日闲鱼为准</strong>：</p><table><thead><tr><th>机型</th><th>推荐度</th><th>二手参考价（¥）</th><th>备注</th></tr></thead><tbody><tr><td>Pixel 6a</td><td>★</td><td>400 – 700</td><td>已进扩展支持，接近停更，不建议新购</td></tr><tr><td>Pixel 6 &#x2F; 6 Pro</td><td>★</td><td>400 – 800</td><td>同上</td></tr><tr><td>Pixel 7</td><td>★★</td><td>700 – 1200</td><td>5 年支持，性价比中</td></tr><tr><td>Pixel 7a</td><td>★★</td><td>700 – 1100</td><td>2028 年到期</td></tr><tr><td>Pixel 7 Pro</td><td>★★</td><td>1000 – 1600</td><td>5 年支持</td></tr><tr><td>Pixel 8</td><td>★★★</td><td>1200 – 1800</td><td>7 年 + MTE</td></tr><tr><td>Pixel 8a</td><td>★★★★</td><td>1300 – 1900</td><td><strong>性价比首选</strong>：MTE + 7 年支持</td></tr><tr><td>Pixel 8 Pro</td><td>★★★</td><td>1700 – 2500</td><td>7 年 + MTE</td></tr><tr><td>Pixel 9</td><td>★★★</td><td>2200 – 3200</td><td>7 年 + MTE</td></tr><tr><td>Pixel 9a</td><td>★★★★</td><td>2200 – 3000</td><td>性价比 + MTE</td></tr><tr><td>Pixel 9 Pro</td><td>★★★</td><td>2800 – 3800</td><td>7 年 + MTE</td></tr><tr><td>Pixel 9 Pro XL</td><td>★★★</td><td>3000 – 4200</td><td>7 年 + MTE</td></tr><tr><td>Pixel 10</td><td>★★★</td><td>4800 – 6700（零售）</td><td>2026 旗舰，二手少</td></tr></tbody></table><p><strong>要点</strong>：</p><ol><li><strong>版本</strong>：优先”解锁版&#x2F;无锁版”，避开美版运营商锁（Verizon&#x2F;AT&amp;T 版 bootloader 可能永久锁定）。日版、台版、欧版通常可解锁，但注意 5G 频段是否覆盖国内。</li><li><strong>OEM 可解锁</strong>：到手后先在 设置 → 开发者选项 里确认”OEM 解锁”开关能打开，打不开的不能刷。</li><li><strong>Pixel 5a 及更早</strong>：官方已不支持，别买。</li><li><strong>Pixel 6 系列</strong>：已进入扩展支持期，性价比虽低但即将停更，新购不建议。</li></ol><h3 id="3-3-刷入-GrapheneOS（Web-Installer）"><a href="#3-3-刷入-GrapheneOS（Web-Installer）" class="headerlink" title="3.3 刷入 GrapheneOS（Web Installer）"></a>3.3 刷入 GrapheneOS（Web Installer）</h3><p>官方提供浏览器一键刷入，最简单：</p><ol><li>电脑装 Chrome&#x2F;Chromium，访问 <code>https://grapheneos.org/install/web</code></li><li>手机开启开发者选项 → 打开 <strong>OEM 解锁</strong> 和 <strong>USB 调试</strong></li><li>手机进入 fastboot 模式（关机后按住 电源 + 音量减），USB 连电脑</li><li>浏览器点击 <strong>Unlock bootloader</strong>（会清空全部数据）</li><li>按页面提示自动下载并刷入最新 release</li><li>刷完点 <strong>Lock bootloader</strong> 重新锁定——<strong>这一步必须做</strong>，否则 Verified Boot 失效，安全性大打折扣</li><li>首次开机按引导配置</li></ol><hr><h2 id="四、第二步：GrapheneOS-系统级隐私加固"><a href="#四、第二步：GrapheneOS-系统级隐私加固" class="headerlink" title="四、第二步：GrapheneOS 系统级隐私加固"></a>四、第二步：GrapheneOS 系统级隐私加固</h2><p>这一步让手机在连上任何网络前就具备”隐身”基础。</p><h3 id="4-1-物理隔离基础"><a href="#4-1-物理隔离基础" class="headerlink" title="4.1 物理隔离基础"></a>4.1 物理隔离基础</h3><ul><li>全程<strong>不插入</strong>国内运营商 SIM 卡</li><li>仅通过 WiFi 连接随身 WiFi</li></ul><h3 id="4-2-切断设备指纹追踪"><a href="#4-2-切断设备指纹追踪" class="headerlink" title="4.2 切断设备指纹追踪"></a>4.2 切断设备指纹追踪</h3><ul><li><strong>随机 MAC</strong>：设置 → 网络和互联网 → WiFi → 随身 WiFi 齿轮 → 开启”使用随机 MAC”</li><li><strong>禁用连接性检查</strong>：设置 → 网络和互联网 → 连接性检查 → 设为”禁用”（防止后台向谷歌服务器发探测包，暴露你在用非标准安卓设备）</li><li><strong>传感器权限</strong>：设置 → 安全与隐私 → 更多隐私设置 → 传感器 → 全局拒绝（防 App 通过陀螺仪&#x2F;加速度计识别打字节奏与步态）</li></ul><h3 id="4-3-存储与沙盒隔离"><a href="#4-3-存储与沙盒隔离" class="headerlink" title="4.3 存储与沙盒隔离"></a>4.3 存储与沙盒隔离</h3><ul><li><strong>存储范围（Storage Scopes）</strong>：设置 → 安全与隐私 → 更多隐私设置 → 存储 → 强制开启。即使授权某 App 访问文件，它也只能看到明确授权的那几个，无法扫描全盘</li><li><strong>私密空间（Private Space）</strong>：把不信任的 App 放进去，拥有完全独立的加密密钥与隔离存储</li></ul><h3 id="4-4-权限管控"><a href="#4-4-权限管控" class="headerlink" title="4.4 权限管控"></a>4.4 权限管控</h3><ul><li>位置 &#x2F; 联系人 &#x2F; 麦克风：设为”仅在使用时允许”或直接拒绝</li><li>本地工具类 App：直接在应用信息里切断”网络”权限</li></ul><h3 id="4-5-胁迫密码（Duress-PIN）"><a href="#4-5-胁迫密码（Duress-PIN）" class="headerlink" title="4.5 胁迫密码（Duress PIN）"></a>4.5 胁迫密码（Duress PIN）</h3><p>设置 → 安全与隐私 → 胁迫密码。</p><p>输入胁迫密码会<strong>擦除加密密钥</strong>，设备内数据瞬间变为不可破解的乱码。注意：是擦除密钥，不是”硬件烧毁”，不要被网络上”底层硬件擦除指令”这类夸大说法误导。</p><hr><h2 id="五、第三步：Ubuntu-云服务器搭建-WireGuard（核心步骤）"><a href="#五、第三步：Ubuntu-云服务器搭建-WireGuard（核心步骤）" class="headerlink" title="五、第三步：Ubuntu 云服务器搭建 WireGuard（核心步骤）"></a>五、第三步：Ubuntu 云服务器搭建 WireGuard（核心步骤）</h2><p>这是整个架构的核心：在国内云服务器上搭一条加密隧道，手机所有流量经此转发。</p><h3 id="5-1-前置与合规边界"><a href="#5-1-前置与合规边界" class="headerlink" title="5.1 前置与合规边界"></a>5.1 前置与合规边界</h3><ul><li><strong>服务器</strong>：阿里云 &#x2F; 腾讯云 &#x2F; 华为云等国内云的 Ubuntu 20.04+ 实例</li><li>⚠️ <strong>严禁配置跨境转发</strong>：iptables&#x2F;nftables 的 MASQUERADE 规则只能指向国内出网网卡，不得转发至境外 IP 段。一旦被检测到跨境特征，实例会被立即封禁，且可能违法</li><li>该节点的真正价值：防公共 WiFi 窃听、防本地网络窥探、对商业目标隐藏真实出口 IP。对云服务商和司法<strong>仍实名可溯</strong></li></ul><h3 id="5-2-服务端安装"><a href="#5-2-服务端安装" class="headerlink" title="5.2 服务端安装"></a>5.2 服务端安装</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 更新系统</span></span><br><span class="line"><span class="built_in">sudo</span> apt update &amp;&amp; <span class="built_in">sudo</span> apt upgrade -y</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 安装 WireGuard</span></span><br><span class="line"><span class="built_in">sudo</span> apt install wireguard -y</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 开启内核 IP 转发</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;net.ipv4.ip_forward = 1&quot;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/sysctl.conf</span><br><span class="line"><span class="built_in">sudo</span> sysctl -p</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. 生成服务器密钥对</span></span><br><span class="line"><span class="built_in">cd</span> /etc/wireguard</span><br><span class="line"><span class="built_in">sudo</span> wg genkey | <span class="built_in">sudo</span> <span class="built_in">tee</span> server_privatekey | <span class="built_in">sudo</span> wg pubkey | <span class="built_in">sudo</span> <span class="built_in">tee</span> server_publickey</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 600 server_privatekey   <span class="comment"># 私钥权限收紧，防止泄露</span></span><br></pre></td></tr></table></figure><h3 id="5-3-编写服务端配置"><a href="#5-3-编写服务端配置" class="headerlink" title="5.3 编写服务端配置"></a>5.3 编写服务端配置</h3><p>先查出公网网卡名：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ip <span class="built_in">link</span> show   <span class="comment"># 通常是 eth0 / ens5 / enp0s3</span></span><br></pre></td></tr></table></figure><p>创建 <code>/etc/wireguard/wg0.conf</code>（<code>eth0</code> 替换为你的实际网卡名）：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Interface]</span></span><br><span class="line"><span class="attr">Address</span> = <span class="number">10.0</span>.<span class="number">0.1</span>/<span class="number">24</span></span><br><span class="line"><span class="attr">SaveConfig</span> = <span class="literal">true</span></span><br><span class="line"><span class="attr">PrivateKey</span> = &lt;粘贴 server_privatekey 内容&gt;</span><br><span class="line"><span class="attr">ListenPort</span> = <span class="number">51820</span></span><br><span class="line"><span class="attr">MTU</span> = <span class="number">1420</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 流量转发规则（eth0 换成你的实际网卡名）</span></span><br><span class="line"><span class="attr">PostUp</span> = iptables -A FORWARD -i wg0 -j ACCEPT<span class="comment">; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE</span></span><br><span class="line"><span class="attr">PostDown</span> = iptables -D FORWARD -i wg0 -j ACCEPT<span class="comment">; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE</span></span><br></pre></td></tr></table></figure><h3 id="5-4-启动与开机自启"><a href="#5-4-启动与开机自启" class="headerlink" title="5.4 启动与开机自启"></a>5.4 启动与开机自启</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> wg-quick@wg0</span><br><span class="line"><span class="built_in">sudo</span> systemctl start wg-quick@wg0</span><br><span class="line"><span class="built_in">sudo</span> systemctl status wg-quick@wg0</span><br><span class="line"><span class="comment"># 显示 active (running) 即服务端核心搭建成功</span></span><br></pre></td></tr></table></figure><h3 id="5-5-放行安全组（极易漏）"><a href="#5-5-放行安全组（极易漏）" class="headerlink" title="5.5 放行安全组（极易漏）"></a>5.5 放行安全组（极易漏）</h3><p>登录云控制台 → 安全组 &#x2F; 防火墙 → 添加<strong>入方向规则</strong>：放行 <strong>UDP 51820</strong> 端口。不放行，手机永远连不上服务器。</p><h3 id="5-6-持久化防火墙规则（可选）"><a href="#5-6-持久化防火墙规则（可选）" class="headerlink" title="5.6 持久化防火墙规则（可选）"></a>5.6 持久化防火墙规则（可选）</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install iptables-persistent -y</span><br><span class="line"><span class="built_in">sudo</span> netfilter-persistent save</span><br></pre></td></tr></table></figure><hr><h2 id="六、第四步：客户端对接（GrapheneOS-手机）"><a href="#六、第四步：客户端对接（GrapheneOS-手机）" class="headerlink" title="六、第四步：客户端对接（GrapheneOS 手机）"></a>六、第四步：客户端对接（GrapheneOS 手机）</h2><h3 id="6-1-生成客户端密钥"><a href="#6-1-生成客户端密钥" class="headerlink" title="6.1 生成客户端密钥"></a>6.1 生成客户端密钥</h3><p>在服务器上为手机生成专属密钥：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /etc/wireguard</span><br><span class="line"><span class="built_in">sudo</span> wg genkey | <span class="built_in">sudo</span> <span class="built_in">tee</span> client1_privatekey | <span class="built_in">sudo</span> wg pubkey | <span class="built_in">sudo</span> <span class="built_in">tee</span> client1_publickey</span><br></pre></td></tr></table></figure><h3 id="6-2-服务端添加-Peer"><a href="#6-2-服务端添加-Peer" class="headerlink" title="6.2 服务端添加 Peer"></a>6.2 服务端添加 Peer</h3><p>编辑 <code>/etc/wireguard/wg0.conf</code>，在文件末尾追加：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Peer]</span></span><br><span class="line"><span class="attr">PublicKey</span> = &lt;client1_publickey 内容&gt;</span><br><span class="line"><span class="attr">AllowedIPs</span> = <span class="number">10.0</span>.<span class="number">0.2</span>/<span class="number">32</span></span><br></pre></td></tr></table></figure><p>保存后重启服务：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart wg-quick@wg0</span><br></pre></td></tr></table></figure><h3 id="6-3-手机端配置"><a href="#6-3-手机端配置" class="headerlink" title="6.3 手机端配置"></a>6.3 手机端配置</h3><ul><li>从 F-Droid 或官方 GitHub 下载安装 WireGuard App</li><li>新建隧道，手动填入或导入以下配置：</li></ul><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Interface]</span></span><br><span class="line"><span class="attr">PrivateKey</span> = &lt;client1_privatekey 内容&gt;</span><br><span class="line"><span class="attr">Address</span> = <span class="number">10.0</span>.<span class="number">0.2</span>/<span class="number">32</span></span><br><span class="line"><span class="attr">DNS</span> = <span class="number">223.5</span>.<span class="number">5.5</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Peer]</span></span><br><span class="line"><span class="attr">PublicKey</span> = &lt;server_publickey 内容&gt;</span><br><span class="line"><span class="attr">Endpoint</span> = &lt;云服务器公网IP&gt;:<span class="number">51820</span></span><br><span class="line"><span class="attr">AllowedIPs</span> = <span class="number">0.0</span>.<span class="number">0.0</span>/<span class="number">0</span></span><br><span class="line"><span class="attr">PersistentKeepalive</span> = <span class="number">25</span></span><br></pre></td></tr></table></figure><blockquote><p><code>AllowedIPs = 0.0.0.0/0</code> 表示全局流量都走 VPN。</p></blockquote><h3 id="6-4-开启-Kill-Switch（关键）"><a href="#6-4-开启-Kill-Switch（关键）" class="headerlink" title="6.4 开启 Kill Switch（关键）"></a>6.4 开启 Kill Switch（关键）</h3><p>设置 → 网络和互联网 → VPN → 选中 WireGuard → 开启”始终开启” + 勾选”阻止无 VPN 的连接”。</p><p>这样 VPN 掉线时手机会自动断网，防止真实出口 IP 因隧道中断而直接暴露。</p><h3 id="6-5-配置-DoH（加密-DNS）"><a href="#6-5-配置-DoH（加密-DNS）" class="headerlink" title="6.5 配置 DoH（加密 DNS）"></a>6.5 配置 DoH（加密 DNS）</h3><p>设置 → 网络和互联网 → 私有 DNS → 选”私有 DNS 提供商主机名” → 填入：</p><ul><li>阿里：<code>dns.alidns.com</code></li><li>腾讯：<code>doh.pub</code></li></ul><p>加密你的域名查询请求，防止随身 WiFi 或本地网络窥探访问轨迹。</p><h3 id="6-6-验证"><a href="#6-6-验证" class="headerlink" title="6.6 验证"></a>6.6 验证</h3><p>连上隧道后，浏览器访问任意 IP 查询站，确认显示的是<strong>云服务器 IP</strong>，而非随身 WiFi 的出口 IP。若显示一致，说明隧道生效。</p><hr><h2 id="七、选型速查"><a href="#七、选型速查" class="headerlink" title="七、选型速查"></a>七、选型速查</h2><table><thead><tr><th>需求</th><th>推荐</th></tr></thead><tbody><tr><td>性价比首选</td><td>Pixel 8a（MTE + 7 年支持，二手约 ¥1300–1900）</td></tr><tr><td>均衡之选</td><td>Pixel 9 &#x2F; 9a</td></tr><tr><td>旗舰体验</td><td>Pixel 9 Pro &#x2F; 9 Pro XL</td></tr><tr><td>避坑</td><td>Pixel 5a 及更早（已不支持）、Pixel 6 系列（扩展支持期）、运营商锁版</td></tr></tbody></table><blockquote><p>价格随成色、版本、容量波动较大，上表为 2026 年 7 月综合估算区间，实际成交以当日闲鱼 &#x2F; 淘宝页面为准。</p></blockquote><hr><h2 id="八、合规与边界声明（必读）"><a href="#八、合规与边界声明（必读）" class="headerlink" title="八、合规与边界声明（必读）"></a>八、合规与边界声明（必读）</h2><ol><li><strong>全程纯国内</strong>：服务器在国内、目标在国内、不跨境转发。iptables MASQUERADE 只指向国内出网网卡。</li><li><strong>链路全程实名</strong>：随身 WiFi、云服务器、运营商均为实名认证。本方案对司法与国家级溯源<strong>零防护</strong>，请勿用于任何违法用途或规避调查。</li><li><strong>主要价值</strong>：防 App 画像 &#x2F; 大数据杀熟、防公共 WiFi 与本地网络窃听、防运营商 DNS 窥探。</li><li><strong>切勿在副机登录任何国内实名账号</strong>（微信 &#x2F; 支付宝 &#x2F; 淘宝），否则所有”隐身”防线瞬间瓦解——服务器端已经知道你是谁。</li></ol><hr>]]>
    </content>
    <id>http://example.com/2026/07/25/2026072501/</id>
    <link href="http://example.com/2026/07/25/2026072501/"/>
    <published>2026-07-25T00:48:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p><strong>场景定位</strong>：一台不插 SIM 卡、不登实名账号的 GrapheneOS Pixel 7 隐私副机，所有流量经国内云服务器加密转发，对外只暴露云服务器身份。</p>
<p><strong>合规边界</strong>：全]]>
    </summary>
    <title>在国内搭一台隐私副机：GrapheneOS + 随身 WiFi + 国内 WireGuard 加密通道</title>
    <updated>2026-08-09T08:21:20.998Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="网络安全" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="网络协议" scheme="http://example.com/tags/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/"/>
    <category term="vpn" scheme="http://example.com/tags/vpn/"/>
    <content>
      <![CDATA[<blockquote><p>摘要:本文从网络分层与 OSI&#x2F;TCP-IP 模型讲起,说明 Shadowsocks、VMess、VLESS、Trojan、Reality 各自工作在哪一层。在此基础上研究了代理协议从 Shadowsocks 到 VMess、Trojan 再到 Reality 的演进脉络,并基于 XTLS&#x2F;REALITY 和 XTLS&#x2F;Xray-core 的源码,梳理了 Reality 的核心机制。最后给出了一段升级后的 Go Demo,支持协议首包识别、完整 ClientHello 解析、SNI 动态路由,以及 JA3 指纹所需字段提取。代码仅用于技术研究。</p></blockquote><hr><h2 id="前置知识-网络分层与代理协议的位置"><a href="#前置知识-网络分层与代理协议的位置" class="headerlink" title="前置知识:网络分层与代理协议的位置"></a>前置知识:网络分层与代理协议的位置</h2><p>要理解 Shadowsocks、VMess、Trojan、Reality 这些协议的区别,需要先搞清楚它们在网络栈中的位置。网络分层不是抽象概念,它直接决定了协议能伪装到什么程度、能被识别在哪里。</p><h3 id="一、TCP-IP-四层模型与-OSI-七层模型"><a href="#一、TCP-IP-四层模型与-OSI-七层模型" class="headerlink" title="一、TCP&#x2F;IP 四层模型与 OSI 七层模型"></a>一、TCP&#x2F;IP 四层模型与 OSI 七层模型</h3><p>两种模型都是把网络通信按功能分层,只是切法不同。对代理协议来说,TCP&#x2F;IP 四层更实用。</p><table><thead><tr><th>TCP&#x2F;IP 四层</th><th>OSI 七层</th><th>代表协议&#x2F;数据</th><th>作用一句话</th><th>代理协议例子</th></tr></thead><tbody><tr><td>应用层</td><td>应用层、表示层、会话层</td><td>HTTP、HTTPS、FTP、DNS、SOCKS5</td><td>解决”某个应用程序想发送什么数据、怎么解析”</td><td>Trojan 内层认证、VMess 请求头、SOCKS5</td></tr><tr><td>传输层</td><td>传输层</td><td>TCP、UDP、端口</td><td>解决”端到端可靠传输,数据交给哪个进程”</td><td>Shadowsocks 直接跑在 TCP&#x2F;UDP 上,Reality 在这里接管 TLS</td></tr><tr><td>网络层</td><td>网络层</td><td>IP、ICMP、路由</td><td>解决”数据包从哪来、到哪去,走哪条路”</td><td>网络层本身不感知代理协议,但 IP 和 SNI 会被审查</td></tr><tr><td>链路层</td><td>数据链路层、物理层</td><td>以太网、Wi-Fi、光纤</td><td>解决”比特流在物理介质上怎么跑”</td><td>和代理协议关系不大</td></tr></tbody></table><blockquote><p>一句话记忆:<strong>链路层传比特,网络层找路径,传输层管端口,应用层定语义。</strong></p></blockquote><h3 id="二、代理协议分别工作在哪个层"><a href="#二、代理协议分别工作在哪个层" class="headerlink" title="二、代理协议分别工作在哪个层"></a>二、代理协议分别工作在哪个层</h3><p>这是理解 Shadowsocks、VMess、Trojan、Reality 差异的关键。</p><table><thead><tr><th>协议</th><th>主要工作层</th><th>说明</th></tr></thead><tbody><tr><td><strong>SOCKS5</strong></td><td>应用层</td><td>它只是一套”告诉代理服务器目标地址”的协议,本身不加密。很多代理工具在本地先开个 SOCKS5 服务,再由下层协议把流量传出去。</td></tr><tr><td><strong>Shadowsocks</strong></td><td>传输层之上&#x2F;应用层之下</td><td>它把 SOCKS5 要转发的数据拿过来,用对称密钥加密后直接塞进 TCP&#x2F;UDP 发走。它不伪装成 HTTP,也不做 TLS 握手,只是一条加密隧道。</td></tr><tr><td><strong>VMess</strong></td><td>应用层</td><td>VMess 自己定义了请求头和数据格式,再交给 TCP、WebSocket、gRPC 等传输层承载。如果外层是 TLS+WebSocket,那 TLS 是另一层负责伪装的。</td></tr><tr><td><strong>VLESS</strong></td><td>应用层</td><td>和 VMess 类似,是 V2Ray&#x2F;Xray 的代理协议,但头部更轻、职责更单一。通常配合 TLS&#x2F;XTLS 作为外层。</td></tr><tr><td><strong>Trojan</strong></td><td>传输层(TLS) + 应用层</td><td>客户端和服务端先完成标准 TLS 握手(传输层),然后在 TLS 加密通道里用 Trojan 自己的密码认证(应用层)。它伪装成 HTTPS,但服务器需要自己的证书。</td></tr><tr><td><strong>Reality</strong></td><td>传输层(TLS 握手层)</td><td>Reality 不定义新的代理语义,它只解决”怎么让 TLS 握手看起来像访问真实网站”。真正的代理协议(如 VLESS)跑在 Reality 接管后的隧道里。</td></tr></tbody></table><blockquote><p><strong>重点</strong>:Shadowsocks 和 VMess&#x2F;VLESS 是”应用层代理协议”,它们负责”目标地址是什么”;Trojan 和 Reality 是”传输层伪装方案”,它们负责”让这条连接看起来像普通 HTTPS”。Reality 的创新在于:它连 TLS 证书和握手都懒得自己演,而是直接让真网站演。</p></blockquote><h3 id="三、几个必须理解的概念"><a href="#三、几个必须理解的概念" class="headerlink" title="三、几个必须理解的概念"></a>三、几个必须理解的概念</h3><h4 id="1-SNI-Server-Name-Indication-服务器名称指示"><a href="#1-SNI-Server-Name-Indication-服务器名称指示" class="headerlink" title="1. SNI(Server Name Indication, 服务器名称指示)"></a>1. SNI(Server Name Indication, 服务器名称指示)</h4><p>一台服务器的一个 IP 上可能同时托管几百个网站。TLS 握手时,客户端需要在 <code>ClientHello</code> 里告诉服务器:”我要访问的是 <code>www.microsoft.com</code>“。这个字段就是 SNI。</p><ul><li>没有 SNI,服务器不知道该给哪个证书。</li><li>审查者常常用 SNI 黑名单做初步过滤。</li><li>Reality 的关键:让 SNI 指向真实网站(如 <code>www.microsoft.com</code>),并且真实证书也匹配这个 SNI。</li></ul><h4 id="2-TLS-握手与-ClientHello"><a href="#2-TLS-握手与-ClientHello" class="headerlink" title="2. TLS 握手与 ClientHello"></a>2. TLS 握手与 ClientHello</h4><p>TLS 握手是 HTTPS 建立安全连接的第一步。客户端先发 <code>ClientHello</code>,包含:</p><ul><li>TLS 版本</li><li>支持的密码套件(Cipher Suites)</li><li>支持的扩展(Extensions),包括 SNI、ALPN、SupportedVersions、KeyShare 等</li><li>一个 32 字节的随机数 <code>ClientRandom</code></li><li>在 TLS 1.3 中,<code>SessionId</code> 字段保留但不再用于会话恢复</li></ul><p>服务器回应 <code>ServerHello</code>,然后发证书、密钥交换等。TLS 1.3 握手完成后,双方派生出会话密钥,后续通信加密。</p><p>Reality 的 trick 就是:<strong>在这个握手过程里,把客户端的 ClientHello 原样转发给真实目标,再把真实目标的 ServerHello&#x2F;证书 透传回来。</strong> 从链路上看,就像客户端真的在和真实网站握手。</p><h4 id="3-证书与证书链"><a href="#3-证书与证书链" class="headerlink" title="3. 证书与证书链"></a>3. 证书与证书链</h4><p>证书由 CA(证书颁发机构)签发,证明”这个公钥确实属于 <code>www.microsoft.com</code>“。浏览器&#x2F;操作系统内置了可信 CA 列表,所以访问大网站时证书能验证通过。</p><p>传统代理服务器自己申请证书时,证书上的域名是代理服务器自己的域名,审查者可以把这个域名&#x2F;指纹拉黑。Reality 的方案是:<strong>不申请证书,直接借用真实网站的证书。</strong></p><h4 id="4-透明转发与接管"><a href="#4-透明转发与接管" class="headerlink" title="4. 透明转发与接管"></a>4. 透明转发与接管</h4><ul><li><strong>透明转发</strong>:收到客户端数据后,原样不改地发给另一个目标,再把目标返回的数据原样发回客户端。对双方来说,仿佛直连。</li><li><strong>接管</strong>:在 TLS 握手的某个节点,代理服务器不再把真实目标的响应透传给客户端,而是自己生成后续 TLS 记录,用派生出的密钥和客户端建立隧道。</li></ul><p>Reality 的认证分支就是:<strong>认证成功 → 接管;认证失败 → 透明转发给真实网站。</strong></p><h3 id="四、为什么分层视角很重要"><a href="#四、为什么分层视角很重要" class="headerlink" title="四、为什么分层视角很重要"></a>四、为什么分层视角很重要</h3><p>如果你从分层角度看,就能明白为什么 Shadowsocks 容易被识别、为什么 Trojan 需要域名、为什么 Reality 是突破。</p><ul><li><strong>Shadowsocks</strong> 在传输层直接发加密数据。审查者不需要解密,只看”这是一个非 HTTP&#x2F;HTTPS 的 TCP 流”,就可能直接阻断。</li><li><strong>VMess</strong> 在应用层自己定义了一套格式。虽然外层可以套 TLS,但 VMess 请求头本身有固定长度和特征,DPI 可以学出来。</li><li><strong>Trojan</strong> 在传输层做得像 HTTPS。但因为它要扮演 HTTPS 服务器,所以审查者可以针对”这个 HTTPS 服务器”做指纹、证书、SNI 黑名单。</li><li><strong>Reality</strong> 在传输层不扮演 HTTPS 服务器,而是把自己变成真实 HTTPS 连接的一部分。审查者能看到的证书和 SNI 都是真网站的,无法从这一层识别出代理。</li></ul><p>理解了这一层,再看后面的四协议演进,就会非常清晰。</p><hr><h2 id="代理协议的演进-为什么需要-Reality"><a href="#代理协议的演进-为什么需要-Reality" class="headerlink" title="代理协议的演进:为什么需要 Reality"></a>代理协议的演进:为什么需要 Reality</h2><h3 id="第一代-Shadowsocks"><a href="#第一代-Shadowsocks" class="headerlink" title="第一代:Shadowsocks"></a>第一代:Shadowsocks</h3><p>Shadowsocks 是一个轻量级 SOCKS5 代理,核心设计是”把数据包加密成随机字节流”。</p><ul><li><strong>握手</strong>:几乎没有显式握手。客户端直接用预共享密钥(PSK)发加密数据。</li><li><strong>加密</strong>:早期用流密码(AES-CTR、ChaCha20),后期升级到 AEAD(AES-GCM、ChaCha20-Poly1305)。</li><li><strong>被识别的原因</strong>:虽然载荷不可读,但流密码时代的流量有统计特征;而且端口固定、首包结构有迹可循。升级到 AEAD 后好很多,但仍依赖固定密钥,没有”抗主动探测”机制——审查者可以直接用密钥尝试解密,或者通过时间&#x2F;长度特征识别。</li></ul><p>一句话总结:<strong>Shadowsocks 隐藏内容,但没有隐藏”这是一个代理连接”这件事。</strong></p><h3 id="第二代-VMess"><a href="#第二代-VMess" class="headerlink" title="第二代:VMess"></a>第二代:VMess</h3><p>VMess 是 V2Ray 的核心协议,设计目标是”特征更少、更灵活”。</p><ul><li><strong>握手</strong>:客户端发送一个加密的请求头,包含 UUID 派生的认证信息、目标地址、加密方式、传输层类型等。</li><li><strong>认证</strong>:基于 UUID 和动态 ID(时间相关),服务端用正确 UUID 才能解密请求头。</li><li><strong>加密</strong>:请求头和后续数据都用 AEAD 加密。</li><li><strong>灵活</strong>:可以承载在 TCP、WebSocket、HTTP&#x2F;2、gRPC 等多种传输层之上。</li><li><strong>被识别的原因</strong>:VMess 的固定长度请求头、特定扩展使用方式,以及客户端和服务端实现中的一些指纹, eventually 可以被 DPI 学习。而且它的”伪装”依赖于上层传输(比如 TLS+WebSocket),TLS 层仍然需要自己的证书和域名。</li></ul><p>一句话总结:<strong>VMess 把协议做得更复杂、更灵活,但本质上还是”自己造一个协议,然后努力伪装”。</strong></p><h3 id="第三代-Trojan"><a href="#第三代-Trojan" class="headerlink" title="第三代:Trojan"></a>第三代:Trojan</h3><p>Trojan 是一个极简主义设计,它直接放弃”造协议”,而是说:** TLS 加密本来就是互联网常态,我们就在 TLS 里面跑代理。**</p><ul><li><strong>握手</strong>:客户端先和服务端完成标准 TLS 握手。服务端需要持有证书(通常是真实域名证书或自签证书)。</li><li><strong>认证</strong>:TLS 握手成功后,客户端在加密通道里发送 <code>hex(SHA224(password))</code> + SOCKS5 风格的目标地址请求。</li><li><strong>Fallback</strong>:如果认证失败,Trojan 服务端把流量转发给一个正常 HTTP 服务(比如本地 80 端口),让主动探测者看到一个正常网站。</li><li><strong>被识别的原因</strong>:Trojan 比 VMess 更像 HTTPS,但它仍然需要自己的证书和域名。审查者可以记录证书指纹、SNI、TLS 指纹(JA3),建立”这个证书&#x2F;指纹属于代理服务器”的黑名单。而且一旦证书是自签的或者域名不常见,就很容易被标记。</li></ul><p>一句话总结:<strong>Trojan 把代理藏在 HTTPS 里,但”HTTPS 服务器”本身仍然是一个攻击面。</strong></p><h3 id="第四代-Reality"><a href="#第四代-Reality" class="headerlink" title="第四代:Reality"></a>第四代:Reality</h3><p>Reality 是 XTLS&#x2F;Xray 项目 2023 年推出的方案。它直接否定了 Trojan 的 premise:<strong>为什么要自己当 HTTPS 服务器?为什么不能直接让真实的大网站当服务器?</strong></p><ul><li><strong>核心思想</strong>:代理服务器自己不持有证书。客户端发起 TLS 握手时,代理服务器把 <code>ClientHello</code> 原样转发给真实目标网站(如 <code>www.microsoft.com</code>),把真实网站返回的 <code>ServerHello</code> + 证书链透传回客户端。</li><li><strong>自己人认证</strong>:客户端和代理服务器通过 X25519 密钥交换、HKDF、AES-GCM,在 <code>ClientHello</code> 的 <code>SessionId</code> 字段里藏一个加密的短 ID。服务端认证成功后”接管”握手;认证失败则把流量透明转发给真实网站。</li><li><strong>突破点</strong>:链路上看到的证书、SNI、TLS 指纹全部来自真实网站,不需要自己的域名和证书,抗主动探测(失败走 fallback 到真实网站)。</li></ul><p>一句话总结:<strong>Reality 不再伪装成 HTTPS,而是真的成为一次 HTTPS 连接的一部分,然后在关键时刻接管它。</strong></p><h3 id="四代对比"><a href="#四代对比" class="headerlink" title="四代对比"></a>四代对比</h3><table><thead><tr><th>协议</th><th>伪装思路</th><th>认证方式</th><th>最大弱点</th></tr></thead><tbody><tr><td>Shadowsocks</td><td>加密成随机流</td><td>预共享密钥</td><td>流量统计特征,无抗探测能力</td></tr><tr><td>VMess</td><td>加密请求头 + 多种传输层</td><td>UUID + 动态 ID</td><td>固定头部指纹,依赖 TLS 伪装</td></tr><tr><td>Trojan</td><td>标准 TLS + 内层认证</td><td>SHA224(password)</td><td>需要自己的证书&#x2F;域名,可被拉黑</td></tr><tr><td>Reality</td><td>直接用真实网站证书</td><td>X25519 + HKDF + AES-GCM</td><td>dest 网站被封则失效,长连接统计特征仍可能被 AI 识别</td></tr></tbody></table><hr><h2 id="Reality-源码核心机制"><a href="#Reality-源码核心机制" class="headerlink" title="Reality 源码核心机制"></a>Reality 源码核心机制</h2><p>我下载并阅读了 XTLS&#x2F;Xray-core 的 <code>transport/internet/reality/reality.go</code> 和 <code>github.com/xtls/reality</code> 的核心文件(特别是 <code>tls.go</code> 中的 <code>Server</code> 函数)。下面是关键机制。</p><h3 id="1-客户端-在-SessionId-里藏暗号"><a href="#1-客户端-在-SessionId-里藏暗号" class="headerlink" title="1. 客户端:在 SessionId 里藏暗号"></a>1. 客户端:在 SessionId 里藏暗号</h3><p>Xray-core 的 <code>transport/internet/reality/reality.go</code> 中,UClient 负责构造客户端握手。关键代码逻辑如下:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">hello.SessionId = <span class="built_in">make</span>([]<span class="type">byte</span>, <span class="number">32</span>)</span><br><span class="line"><span class="built_in">copy</span>(hello.Raw[<span class="number">39</span>:], hello.SessionId)</span><br><span class="line">hello.SessionId[<span class="number">0</span>] = core.Version_x</span><br><span class="line">hello.SessionId[<span class="number">1</span>] = core.Version_y</span><br><span class="line">hello.SessionId[<span class="number">2</span>] = core.Version_z</span><br><span class="line">hello.SessionId[<span class="number">3</span>] = <span class="number">0</span> <span class="comment">// reserved</span></span><br><span class="line">binary.BigEndian.PutUint32(hello.SessionId[<span class="number">4</span>:], <span class="type">uint32</span>(time.Now().Unix()))</span><br><span class="line"><span class="built_in">copy</span>(hello.SessionId[<span class="number">8</span>:], config.ShortId)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用 X25519 ECDH 派生 AuthKey</span></span><br><span class="line">publicKey, err := ecdh.X25519().NewPublicKey(config.PublicKey)</span><br><span class="line">ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe</span><br><span class="line">uConn.AuthKey, _ = ecdhe.ECDH(publicKey)</span><br><span class="line"></span><br><span class="line"><span class="comment">// HKDF</span></span><br><span class="line">hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:<span class="number">20</span>], []<span class="type">byte</span>(<span class="string">&quot;REALITY&quot;</span>)).Read(uConn.AuthKey)</span><br><span class="line"></span><br><span class="line"><span class="comment">// AES-GCM 加密 SessionId[:16]</span></span><br><span class="line">aead := crypto.NewAesGcm(uConn.AuthKey)</span><br><span class="line">aead.Seal(hello.SessionId[:<span class="number">0</span>], hello.Random[<span class="number">20</span>:], hello.SessionId[:<span class="number">16</span>], hello.Raw)</span><br><span class="line"><span class="built_in">copy</span>(hello.Raw[<span class="number">39</span>:], hello.SessionId)</span><br></pre></td></tr></table></figure><p><code>SessionId</code> 的 32 字节结构:</p><table><thead><tr><th>偏移</th><th>长度</th><th>内容</th></tr></thead><tbody><tr><td>0-2</td><td>3</td><td>Xray 版本号 X.Y.Z</td></tr><tr><td>3</td><td>1</td><td>保留 0x00</td></tr><tr><td>4-7</td><td>4</td><td>Unix 时间戳(大端)</td></tr><tr><td>8-23</td><td>16</td><td>shortId(客户端身份)</td></tr><tr><td>24-31</td><td>8</td><td>其余空间(被加密后覆盖)</td></tr></tbody></table><p>前 16 字节被 AES-GCM 加密,整个 <code>SessionId</code> 看起来仍是随机字节,审查者看不出来。</p><h3 id="2-服务端-认证-分支"><a href="#2-服务端-认证-分支" class="headerlink" title="2. 服务端:认证 + 分支"></a>2. 服务端:认证 + 分支</h3><p><code>github.com/xtls/reality/tls.go</code> 中的 <code>Server</code> 函数是核心。它一上来就做了三件事:</p><ol><li><strong>连接真实目标 dest</strong> 并发送 PROXY protocol 头(可选)。</li><li><strong>用 <code>MirrorConn</code> 包裹客户端连接</strong>,让从客户端读到的数据同时被转发给 dest。</li><li><strong>读取 ClientHello</strong> 并尝试认证。</li></ol><p>认证逻辑的关键代码:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> _, keyShare := <span class="keyword">range</span> hs.clientHello.keyShares &#123;</span><br><span class="line">    <span class="keyword">if</span> keyShare.group == X25519 &amp;&amp; <span class="built_in">len</span>(keyShare.data) == <span class="number">32</span> &#123;</span><br><span class="line">        peerPub = keyShare.data</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> hs.c.AuthKey, err = curve25519.X25519(config.PrivateKey, peerPub); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line">hkdf.New(sha256.New, hs.c.AuthKey, hs.clientHello.random[:<span class="number">20</span>], []<span class="type">byte</span>(<span class="string">&quot;REALITY&quot;</span>)).Read(hs.c.AuthKey)</span><br><span class="line"></span><br><span class="line">block, _ := aes.NewCipher(hs.c.AuthKey)</span><br><span class="line">aead, _ := cipher.NewGCM(block)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用 AES-GCM 解密 SessionId</span></span><br><span class="line">aead.Open(plainText[:<span class="number">0</span>], hs.clientHello.random[<span class="number">20</span>:], ciphertext, hs.clientHello.original)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 校验版本、时间差、shortId</span></span><br><span class="line"><span class="keyword">if</span> (version OK) &amp;&amp; (time diff OK) &amp;&amp; (config.ShortIds[hs.c.ClientShortId]) &#123;</span><br><span class="line">    hs.c.conn = conn  <span class="comment">// 认证成功: 接管连接</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-分支行为"><a href="#3-分支行为" class="headerlink" title="3. 分支行为"></a>3. 分支行为</h3><ul><li><strong>认证失败</strong>: <code>hs.c.conn != conn</code>, 服务端直接 <code>io.Copy(target, underlying)</code> 和 <code>io.Copy(underlying, target)</code>, 把客户端和真实网站双向打通。审查者看到的是正常网站响应。</li><li><strong>认证成功</strong>: <code>hs.c.conn = conn</code>, 服务端继续调用 <code>hs.handshake()</code> 和 <code>hs.readClientFinished()</code>, 自己伪造 TLS 1.3 握手记录,接管密钥,后续走代理隧道。</li></ul><p>这就是 Reality 抗主动探测的核心:<strong>对陌生人,你就是一个正常网站的反向代理。</strong></p><h3 id="4-服务端伪造握手记录"><a href="#4-服务端伪造握手记录" class="headerlink" title="4. 服务端伪造握手记录"></a>4. 服务端伪造握手记录</h3><p>认证成功后,服务端不能直接把 dest 的响应原样发回客户端(那样客户端和 dest 会用自己的密钥完成握手,代理服务器就被架空了)。服务端会:</p><ul><li>从 dest 读取 <code>ServerHello</code>、<code>ChangeCipherSpec</code>、<code>EncryptedExtensions</code>、<code>Certificate</code>、<code>CertificateVerify</code>、<code>Finished</code> 等记录。</li><li>根据这些记录的结构,自己生成对应的 TLS 1.3 记录,但用自己的密钥。</li><li>让客户端以为它就是 dest 的响应,从而用代理服务器的密钥继续通信。</li></ul><p>这部分代码在 <code>handshake_server_tls13.go</code> 里,与标准 Go <code>crypto/tls</code> 的服务器握手高度相似,但关键点被 Reality 接管。</p><hr><h2 id="升级-Demo-的目标"><a href="#升级-Demo-的目标" class="headerlink" title="升级 Demo 的目标"></a>升级 Demo 的目标</h2><p>基于上面的源码理解,我把 Demo 升级到 v2,新增以下能力:</p><ol><li><strong>协议首包识别</strong>:判断 incoming 连接是 TLS、HTTP 还是未知协议。</li><li><strong>完整 ClientHello 解析</strong>:版本、随机数、SessionID、密码套件、压缩方法、扩展列表、SNI、ALPN、SupportedVersions、KeyShare groups、SignatureSchemes。</li><li><strong>SNI 动态路由</strong>:根据解析出的 SNI 选择不同的真实目标网站。</li><li><strong>JA3 指纹字段提取</strong>:收集 JA3 计算所需的字段,但暂不引入外部依赖计算完整 hash。</li><li><strong>保留 Reality 认证插槽</strong>:在代码中明确标注 X25519 + HKDF + AES-GCM 认证逻辑应插入的位置,但本 Demo 不实现(避免变成可部署翻墙工具)。</li><li><strong>更详细的日志</strong>:方便观察不同客户端(OpenSSL、浏览器、curl)的握手特征差异。</li></ol><hr><h2 id="完整代码"><a href="#完整代码" class="headerlink" title="完整代码"></a>完整代码</h2><p>项目目录:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">reality-study/</span><br><span class="line">├── go.mod</span><br><span class="line">├── main.go</span><br><span class="line">└── reality-study.exe</span><br></pre></td></tr></table></figure><p><code>main.go</code>:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br><span class="line">265</span><br><span class="line">266</span><br><span class="line">267</span><br><span class="line">268</span><br><span class="line">269</span><br><span class="line">270</span><br><span class="line">271</span><br><span class="line">272</span><br><span class="line">273</span><br><span class="line">274</span><br><span class="line">275</span><br><span class="line">276</span><br><span class="line">277</span><br><span class="line">278</span><br><span class="line">279</span><br><span class="line">280</span><br><span class="line">281</span><br><span class="line">282</span><br><span class="line">283</span><br><span class="line">284</span><br><span class="line">285</span><br><span class="line">286</span><br><span class="line">287</span><br><span class="line">288</span><br><span class="line">289</span><br><span class="line">290</span><br><span class="line">291</span><br><span class="line">292</span><br><span class="line">293</span><br><span class="line">294</span><br><span class="line">295</span><br><span class="line">296</span><br><span class="line">297</span><br><span class="line">298</span><br><span class="line">299</span><br><span class="line">300</span><br><span class="line">301</span><br><span class="line">302</span><br><span class="line">303</span><br><span class="line">304</span><br><span class="line">305</span><br><span class="line">306</span><br><span class="line">307</span><br><span class="line">308</span><br><span class="line">309</span><br><span class="line">310</span><br><span class="line">311</span><br><span class="line">312</span><br><span class="line">313</span><br><span class="line">314</span><br><span class="line">315</span><br><span class="line">316</span><br><span class="line">317</span><br><span class="line">318</span><br><span class="line">319</span><br><span class="line">320</span><br><span class="line">321</span><br><span class="line">322</span><br><span class="line">323</span><br><span class="line">324</span><br><span class="line">325</span><br><span class="line">326</span><br><span class="line">327</span><br><span class="line">328</span><br><span class="line">329</span><br><span class="line">330</span><br><span class="line">331</span><br><span class="line">332</span><br><span class="line">333</span><br><span class="line">334</span><br><span class="line">335</span><br><span class="line">336</span><br><span class="line">337</span><br><span class="line">338</span><br><span class="line">339</span><br><span class="line">340</span><br><span class="line">341</span><br><span class="line">342</span><br><span class="line">343</span><br><span class="line">344</span><br><span class="line">345</span><br><span class="line">346</span><br><span class="line">347</span><br><span class="line">348</span><br><span class="line">349</span><br><span class="line">350</span><br><span class="line">351</span><br><span class="line">352</span><br><span class="line">353</span><br><span class="line">354</span><br><span class="line">355</span><br><span class="line">356</span><br><span class="line">357</span><br><span class="line">358</span><br><span class="line">359</span><br><span class="line">360</span><br><span class="line">361</span><br><span class="line">362</span><br><span class="line">363</span><br><span class="line">364</span><br><span class="line">365</span><br><span class="line">366</span><br><span class="line">367</span><br><span class="line">368</span><br><span class="line">369</span><br><span class="line">370</span><br><span class="line">371</span><br><span class="line">372</span><br><span class="line">373</span><br><span class="line">374</span><br><span class="line">375</span><br><span class="line">376</span><br><span class="line">377</span><br><span class="line">378</span><br><span class="line">379</span><br><span class="line">380</span><br><span class="line">381</span><br><span class="line">382</span><br><span class="line">383</span><br><span class="line">384</span><br><span class="line">385</span><br><span class="line">386</span><br><span class="line">387</span><br><span class="line">388</span><br><span class="line">389</span><br><span class="line">390</span><br><span class="line">391</span><br><span class="line">392</span><br><span class="line">393</span><br><span class="line">394</span><br><span class="line">395</span><br><span class="line">396</span><br><span class="line">397</span><br><span class="line">398</span><br><span class="line">399</span><br><span class="line">400</span><br><span class="line">401</span><br><span class="line">402</span><br><span class="line">403</span><br><span class="line">404</span><br><span class="line">405</span><br><span class="line">406</span><br><span class="line">407</span><br><span class="line">408</span><br><span class="line">409</span><br><span class="line">410</span><br><span class="line">411</span><br><span class="line">412</span><br><span class="line">413</span><br><span class="line">414</span><br><span class="line">415</span><br><span class="line">416</span><br><span class="line">417</span><br><span class="line">418</span><br><span class="line">419</span><br><span class="line">420</span><br><span class="line">421</span><br><span class="line">422</span><br><span class="line">423</span><br><span class="line">424</span><br><span class="line">425</span><br><span class="line">426</span><br><span class="line">427</span><br><span class="line">428</span><br><span class="line">429</span><br><span class="line">430</span><br><span class="line">431</span><br><span class="line">432</span><br><span class="line">433</span><br><span class="line">434</span><br><span class="line">435</span><br><span class="line">436</span><br><span class="line">437</span><br><span class="line">438</span><br><span class="line">439</span><br><span class="line">440</span><br><span class="line">441</span><br><span class="line">442</span><br><span class="line">443</span><br><span class="line">444</span><br><span class="line">445</span><br><span class="line">446</span><br><span class="line">447</span><br><span class="line">448</span><br><span class="line">449</span><br><span class="line">450</span><br><span class="line">451</span><br><span class="line">452</span><br><span class="line">453</span><br><span class="line">454</span><br><span class="line">455</span><br><span class="line">456</span><br><span class="line">457</span><br><span class="line">458</span><br><span class="line">459</span><br><span class="line">460</span><br><span class="line">461</span><br><span class="line">462</span><br><span class="line">463</span><br><span class="line">464</span><br><span class="line">465</span><br><span class="line">466</span><br><span class="line">467</span><br><span class="line">468</span><br><span class="line">469</span><br><span class="line">470</span><br><span class="line">471</span><br><span class="line">472</span><br><span class="line">473</span><br><span class="line">474</span><br><span class="line">475</span><br><span class="line">476</span><br><span class="line">477</span><br><span class="line">478</span><br><span class="line">479</span><br><span class="line">480</span><br><span class="line">481</span><br><span class="line">482</span><br><span class="line">483</span><br><span class="line">484</span><br><span class="line">485</span><br><span class="line">486</span><br><span class="line">487</span><br><span class="line">488</span><br><span class="line">489</span><br><span class="line">490</span><br><span class="line">491</span><br><span class="line">492</span><br><span class="line">493</span><br><span class="line">494</span><br><span class="line">495</span><br><span class="line">496</span><br><span class="line">497</span><br><span class="line">498</span><br><span class="line">499</span><br><span class="line">500</span><br><span class="line">501</span><br><span class="line">502</span><br><span class="line">503</span><br><span class="line">504</span><br><span class="line">505</span><br><span class="line">506</span><br><span class="line">507</span><br><span class="line">508</span><br><span class="line">509</span><br><span class="line">510</span><br><span class="line">511</span><br><span class="line">512</span><br><span class="line">513</span><br><span class="line">514</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;encoding/binary&quot;</span></span><br><span class="line"><span class="string">&quot;encoding/hex&quot;</span></span><br><span class="line"><span class="string">&quot;flag&quot;</span></span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;io&quot;</span></span><br><span class="line"><span class="string">&quot;log&quot;</span></span><br><span class="line"><span class="string">&quot;net&quot;</span></span><br><span class="line"><span class="string">&quot;strings&quot;</span></span><br><span class="line"><span class="string">&quot;time&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 升级后的 Reality 研究 Demo(v2)。</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// 相比 v1 版本,本版本增加了:</span></span><br><span class="line"><span class="comment">//   1. 协议首包识别(TLS / HTTP / 未知)</span></span><br><span class="line"><span class="comment">//   2. 更完整的 TLS ClientHello 解析(版本、密码套件、扩展、ALPN、Supported Versions、KeyShare groups)</span></span><br><span class="line"><span class="comment">//   3. SNI 到真实目标网站的动态路由映射</span></span><br><span class="line"><span class="comment">//   4. 提取 JA3 指纹所需字段(暂不计算完整 JA3 hash,避免引入外部依赖)</span></span><br><span class="line"><span class="comment">//   5. 在日志中直观展示不同协议、不同客户端的握手特征</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// 本 Demo 仍然只演示 TLS 握手字节流的操控与透传,不实现 Reality 的认证、</span></span><br><span class="line"><span class="comment">// 密钥派生与隧道接管。代码中保留了明确的&quot;认证插槽&quot;注释,标示 Reality 源码</span></span><br><span class="line"><span class="comment">// 中 X25519+HKDF+AES-GCM 的接入点。</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">defaultListen = <span class="string">&quot;127.0.0.1:8443&quot;</span></span><br><span class="line">defaultDest   = <span class="string">&quot;www.microsoft.com:443&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// ForwardRule 描述一条 SNI -&gt; dest 的映射规则。</span></span><br><span class="line"><span class="keyword">type</span> ForwardRule <span class="keyword">struct</span> &#123;</span><br><span class="line">SNI  <span class="type">string</span></span><br><span class="line">Dest <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ClientHelloInfo 保存解析后的 ClientHello 关键信息。</span></span><br><span class="line"><span class="keyword">type</span> ClientHelloInfo <span class="keyword">struct</span> &#123;</span><br><span class="line">RecordVersion      <span class="type">uint16</span></span><br><span class="line">HandshakeVersion   <span class="type">uint16</span></span><br><span class="line">Random             [<span class="number">32</span>]<span class="type">byte</span></span><br><span class="line">SessionID          []<span class="type">byte</span></span><br><span class="line">CipherSuites       []<span class="type">uint16</span></span><br><span class="line">CompressionMethods []<span class="type">uint8</span></span><br><span class="line">Extensions         []Extension</span><br><span class="line">SNI                <span class="type">string</span></span><br><span class="line">ALPNs              []<span class="type">string</span></span><br><span class="line">SupportedVersions  []<span class="type">uint16</span></span><br><span class="line">KeyShareGroups     []<span class="type">uint16</span></span><br><span class="line">SignatureSchemes   []<span class="type">uint16</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Extension 保存单个 TLS 扩展的原始信息。</span></span><br><span class="line"><span class="keyword">type</span> Extension <span class="keyword">struct</span> &#123;</span><br><span class="line">Type <span class="type">uint16</span></span><br><span class="line">Len  <span class="type">uint16</span></span><br><span class="line">Data []<span class="type">byte</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ProtocolType 表示首包识别结果。</span></span><br><span class="line"><span class="keyword">type</span> ProtocolType <span class="type">int</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">ProtoUnknown ProtocolType = <span class="literal">iota</span></span><br><span class="line">ProtoTLS</span><br><span class="line">ProtoHTTP</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p ProtocolType)</span></span> String() <span class="type">string</span> &#123;</span><br><span class="line"><span class="keyword">switch</span> p &#123;</span><br><span class="line"><span class="keyword">case</span> ProtoTLS:</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;TLS&quot;</span></span><br><span class="line"><span class="keyword">case</span> ProtoHTTP:</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;HTTP&quot;</span></span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;UNKNOWN&quot;</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">listen := flag.String(<span class="string">&quot;listen&quot;</span>, defaultListen, <span class="string">&quot;本地监听地址&quot;</span>)</span><br><span class="line">dest := flag.String(<span class="string">&quot;dest&quot;</span>, defaultDest, <span class="string">&quot;默认真实目标网站地址&quot;</span>)</span><br><span class="line">sniMap := flag.String(<span class="string">&quot;sni-map&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="string">&quot;SNI 路由映射,逗号分隔,例如: &#x27;www.microsoft.com:microsoft.com:443,www.apple.com:apple.com:443&#x27;&quot;</span>)</span><br><span class="line">show := flag.Bool(<span class="string">&quot;show&quot;</span>, <span class="literal">false</span>, <span class="string">&quot;打印详细握手信息&quot;</span>)</span><br><span class="line">flag.Parse()</span><br><span class="line"></span><br><span class="line">rules := parseSniMap(*sniMap)</span><br><span class="line"></span><br><span class="line">ln, err := net.Listen(<span class="string">&quot;tcp&quot;</span>, *listen)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatalf(<span class="string">&quot;监听失败: %v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">defer</span> ln.Close()</span><br><span class="line"></span><br><span class="line">log.Printf(<span class="string">&quot;[+] 监听 %s, 默认目标 %s&quot;</span>, *listen, *dest)</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(rules) &gt; <span class="number">0</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[+] 已加载 %d 条 SNI 路由规则&quot;</span>, <span class="built_in">len</span>(rules))</span><br><span class="line"><span class="keyword">for</span> _, r := <span class="keyword">range</span> rules &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;    %s -&gt; %s&quot;</span>, r.SNI, r.Dest)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">log.Println(<span class="string">&quot;[+] 测试命令: openssl s_client -connect 127.0.0.1:8443 -servername www.microsoft.com&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">conn, err := ln.Accept()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;accept 失败: %v&quot;</span>, err)</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">go</span> handleConn(conn, *dest, rules, *show)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">parseSniMap</span><span class="params">(s <span class="type">string</span>)</span></span> []ForwardRule &#123;</span><br><span class="line"><span class="keyword">if</span> strings.TrimSpace(s) == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> rules []ForwardRule</span><br><span class="line"><span class="keyword">for</span> _, part := <span class="keyword">range</span> strings.Split(s, <span class="string">&quot;,&quot;</span>) &#123;</span><br><span class="line">part = strings.TrimSpace(part)</span><br><span class="line"><span class="keyword">if</span> part == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line">kv := strings.SplitN(part, <span class="string">&quot;:&quot;</span>, <span class="number">2</span>)</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(kv) != <span class="number">2</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[-] 忽略非法 SNI 映射: %s&quot;</span>, part)</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line">rules = <span class="built_in">append</span>(rules, ForwardRule&#123;SNI: kv[<span class="number">0</span>], Dest: kv[<span class="number">1</span>]&#125;)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> rules</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">routeBySNI</span><span class="params">(sni, defaultDest <span class="type">string</span>, rules []ForwardRule)</span></span> <span class="type">string</span> &#123;</span><br><span class="line"><span class="keyword">for</span> _, r := <span class="keyword">range</span> rules &#123;</span><br><span class="line"><span class="keyword">if</span> r.SNI == sni &#123;</span><br><span class="line"><span class="keyword">return</span> r.Dest</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> defaultDest</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleConn</span><span class="params">(clientConn net.Conn, defaultDest <span class="type">string</span>, rules []ForwardRule, show <span class="type">bool</span>)</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> clientConn.Close()</span><br><span class="line">remoteAddr := clientConn.RemoteAddr().String()</span><br><span class="line"></span><br><span class="line">header := <span class="built_in">make</span>([]<span class="type">byte</span>, <span class="number">5</span>)</span><br><span class="line"><span class="keyword">if</span> _, err := io.ReadFull(clientConn, header); err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[%s] 读取首包失败: %v&quot;</span>, remoteAddr, err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">proto := detectProtocol(header)</span><br><span class="line">log.Printf(<span class="string">&quot;[%s] 协议识别: %s&quot;</span>, remoteAddr, proto)</span><br><span class="line"></span><br><span class="line"><span class="keyword">switch</span> proto &#123;</span><br><span class="line"><span class="keyword">case</span> ProtoTLS:</span><br><span class="line">handleTLS(clientConn, header, defaultDest, rules, show, remoteAddr)</span><br><span class="line"><span class="keyword">case</span> ProtoHTTP:</span><br><span class="line">handlePlainHTTP(clientConn, header, defaultDest, remoteAddr)</span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">log.Printf(<span class="string">&quot;[%s] 未知协议,按默认目标 %s 透明转发&quot;</span>, remoteAddr, defaultDest)</span><br><span class="line">forwardRaw(clientConn, header, defaultDest)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">detectProtocol</span><span class="params">(header []<span class="type">byte</span>)</span></span> ProtocolType &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(header) &lt; <span class="number">5</span> &#123;</span><br><span class="line"><span class="keyword">return</span> ProtoUnknown</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> header[<span class="number">0</span>] == <span class="number">0x16</span> &#123;</span><br><span class="line">ver := binary.BigEndian.Uint16(header[<span class="number">1</span>:<span class="number">3</span>])</span><br><span class="line"><span class="keyword">if</span> ver == <span class="number">0x0301</span> || ver == <span class="number">0x0302</span> || ver == <span class="number">0x0303</span> || ver == <span class="number">0x0304</span> || ver == <span class="number">0x0300</span> &#123;</span><br><span class="line"><span class="keyword">return</span> ProtoTLS</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">methods := []<span class="type">string</span>&#123;<span class="string">&quot;GET &quot;</span>, <span class="string">&quot;POST&quot;</span>, <span class="string">&quot;HEAD&quot;</span>, <span class="string">&quot;PUT &quot;</span>, <span class="string">&quot;DELE&quot;</span>, <span class="string">&quot;OPTI&quot;</span>, <span class="string">&quot;PATC&quot;</span>, <span class="string">&quot;TRAC&quot;</span>, <span class="string">&quot;CONN&quot;</span>&#125;</span><br><span class="line"><span class="keyword">for</span> _, m := <span class="keyword">range</span> methods &#123;</span><br><span class="line"><span class="keyword">if</span> bytes.HasPrefix(header, []<span class="type">byte</span>(m)) &#123;</span><br><span class="line"><span class="keyword">return</span> ProtoHTTP</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> ProtoUnknown</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleTLS</span><span class="params">(clientConn net.Conn, header []<span class="type">byte</span>, defaultDest <span class="type">string</span>, rules []ForwardRule, show <span class="type">bool</span>, remoteAddr <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">recLen := <span class="type">int</span>(binary.BigEndian.Uint16(header[<span class="number">3</span>:<span class="number">5</span>]))</span><br><span class="line">record := <span class="built_in">make</span>([]<span class="type">byte</span>, <span class="number">5</span>+recLen)</span><br><span class="line"><span class="built_in">copy</span>(record, header)</span><br><span class="line"><span class="keyword">if</span> _, err := io.ReadFull(clientConn, record[<span class="number">5</span>:]); err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[%s] 读取 TLS record 体失败: %v&quot;</span>, remoteAddr, err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">info, err := parseClientHello(record)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[%s] 解析 ClientHello 失败: %v&quot;</span>, remoteAddr, err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">dest := routeBySNI(info.SNI, defaultDest, rules)</span><br><span class="line"><span class="keyword">if</span> info.SNI == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">info.SNI = <span class="string">&quot;(none)&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">log.Printf(<span class="string">&quot;[%s] TLS ClientHello: SNI=%s, Version=0x%04x, Ciphers=%d, Exts=%d, KeyShareGroups=%v, ALPN=%v, Dest=%s&quot;</span>,</span><br><span class="line">remoteAddr, info.SNI, info.HandshakeVersion, <span class="built_in">len</span>(info.CipherSuites), <span class="built_in">len</span>(info.Extensions),</span><br><span class="line">info.KeyShareGroups, info.ALPNs, dest)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> show &#123;</span><br><span class="line">printClientHelloDetails(remoteAddr, info)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// =====================================================</span></span><br><span class="line"><span class="comment">// Reality 认证插槽(本 Demo 未实现):</span></span><br><span class="line"><span class="comment">// 在真实 Reality 中,此时会:</span></span><br><span class="line"><span class="comment">//   1. 从 info.KeyShares 中提取 X25519 客户端公钥</span></span><br><span class="line"><span class="comment">//   2. 用服务端 privateKey 做 X25519 ECDH,得到 sharedSecret</span></span><br><span class="line"><span class="comment">//   3. hkdf.New(sha256.New, sharedSecret, info.Random[:20], []byte(&quot;REALITY&quot;)).Read(authKey)</span></span><br><span class="line"><span class="comment">//   4. 用 AES-GCM 解密 info.SessionID[:16] -&gt; 版本/时间戳/shortId</span></span><br><span class="line"><span class="comment">//   5. 校验版本范围、时间差、shortId 白名单</span></span><br><span class="line"><span class="comment">// 认证失败 -&gt; 透明转发给 dest(抗主动探测)</span></span><br><span class="line"><span class="comment">// 认证成功 -&gt; 接管握手,自行生成 TLS 1.3 记录,后续走代理隧道</span></span><br><span class="line"><span class="comment">// 本 Demo 为了边界清晰,只执行透明转发。</span></span><br><span class="line"><span class="comment">// =====================================================</span></span><br><span class="line"></span><br><span class="line">forwardTLS(clientConn, record, dest, remoteAddr)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">printClientHelloDetails</span><span class="params">(remoteAddr <span class="type">string</span>, info *ClientHelloInfo)</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> cipherNames []<span class="type">string</span></span><br><span class="line"><span class="keyword">for</span> _, c := <span class="keyword">range</span> info.CipherSuites &#123;</span><br><span class="line">cipherNames = <span class="built_in">append</span>(cipherNames, fmt.Sprintf(<span class="string">&quot;0x%04x&quot;</span>, c))</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> extTypes []<span class="type">string</span></span><br><span class="line"><span class="keyword">for</span> _, e := <span class="keyword">range</span> info.Extensions &#123;</span><br><span class="line">extTypes = <span class="built_in">append</span>(extTypes, fmt.Sprintf(<span class="string">&quot;0x%04x&quot;</span>, e.Type))</span><br><span class="line">&#125;</span><br><span class="line">log.Printf(<span class="string">&quot;[%s]   CipherSuites: %s&quot;</span>, remoteAddr, strings.Join(cipherNames, <span class="string">&quot;, &quot;</span>))</span><br><span class="line">log.Printf(<span class="string">&quot;[%s]   Extension Types: %s&quot;</span>, remoteAddr, strings.Join(extTypes, <span class="string">&quot;, &quot;</span>))</span><br><span class="line">log.Printf(<span class="string">&quot;[%s]   SupportedVersions: %v&quot;</span>, remoteAddr, versionsToStrings(info.SupportedVersions))</span><br><span class="line">log.Printf(<span class="string">&quot;[%s]   SignatureSchemes: %v&quot;</span>, remoteAddr, schemesToStrings(info.SignatureSchemes))</span><br><span class="line">log.Printf(<span class="string">&quot;[%s]   SessionID: %s&quot;</span>, remoteAddr, hex.EncodeToString(info.SessionID))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">versionsToStrings</span><span class="params">(vers []<span class="type">uint16</span>)</span></span> []<span class="type">string</span> &#123;</span><br><span class="line"><span class="keyword">var</span> out []<span class="type">string</span></span><br><span class="line"><span class="keyword">for</span> _, v := <span class="keyword">range</span> vers &#123;</span><br><span class="line"><span class="keyword">switch</span> v &#123;</span><br><span class="line"><span class="keyword">case</span> <span class="number">0x0304</span>:</span><br><span class="line">out = <span class="built_in">append</span>(out, <span class="string">&quot;TLS 1.3&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> <span class="number">0x0303</span>:</span><br><span class="line">out = <span class="built_in">append</span>(out, <span class="string">&quot;TLS 1.2&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> <span class="number">0x0302</span>:</span><br><span class="line">out = <span class="built_in">append</span>(out, <span class="string">&quot;TLS 1.1&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> <span class="number">0x0301</span>:</span><br><span class="line">out = <span class="built_in">append</span>(out, <span class="string">&quot;TLS 1.0&quot;</span>)</span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">out = <span class="built_in">append</span>(out, fmt.Sprintf(<span class="string">&quot;0x%04x&quot;</span>, v))</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">schemesToStrings</span><span class="params">(schemes []<span class="type">uint16</span>)</span></span> []<span class="type">string</span> &#123;</span><br><span class="line"><span class="keyword">var</span> out []<span class="type">string</span></span><br><span class="line"><span class="keyword">for</span> _, s := <span class="keyword">range</span> schemes &#123;</span><br><span class="line">out = <span class="built_in">append</span>(out, fmt.Sprintf(<span class="string">&quot;0x%04x&quot;</span>, s))</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">forwardTLS</span><span class="params">(clientConn net.Conn, record []<span class="type">byte</span>, dest, remoteAddr <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">destConn, err := net.Dial(<span class="string">&quot;tcp&quot;</span>, dest)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[%s] 连接目标 %s 失败: %v&quot;</span>, remoteAddr, dest, err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">defer</span> destConn.Close()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> _, err := destConn.Write(record); err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[%s] 转发 ClientHello 失败: %v&quot;</span>, remoteAddr, err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">log.Printf(<span class="string">&quot;[%s] 开始双向透传 &lt;-&gt; %s&quot;</span>, remoteAddr, dest)</span><br><span class="line">relay(clientConn, destConn)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">forwardRaw</span><span class="params">(clientConn net.Conn, header []<span class="type">byte</span>, dest <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">remoteAddr := clientConn.RemoteAddr().String()</span><br><span class="line">destConn, err := net.Dial(<span class="string">&quot;tcp&quot;</span>, dest)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[%s] 连接目标 %s 失败: %v&quot;</span>, remoteAddr, dest, err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">defer</span> destConn.Close()</span><br><span class="line"><span class="keyword">if</span> _, err := destConn.Write(header); err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[%s] 转发首包失败: %v&quot;</span>, remoteAddr, err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line">relay(clientConn, destConn)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handlePlainHTTP</span><span class="params">(clientConn net.Conn, header []<span class="type">byte</span>, defaultDest, remoteAddr <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[%s] 明文 HTTP, 按默认目标 %s 转发&quot;</span>, remoteAddr, defaultDest)</span><br><span class="line">forwardRaw(clientConn, header, defaultDest)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">relay</span><span class="params">(a, b net.Conn)</span></span> &#123;</span><br><span class="line">done := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;, <span class="number">2</span>)</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">_, _ = io.Copy(b, a)</span><br><span class="line">done &lt;- <span class="keyword">struct</span>&#123;&#125;&#123;&#125;</span><br><span class="line">&#125;()</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">_, _ = io.Copy(a, b)</span><br><span class="line">done &lt;- <span class="keyword">struct</span>&#123;&#125;&#123;&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&lt;-done</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">parseClientHello</span><span class="params">(record []<span class="type">byte</span>)</span></span> (*ClientHelloInfo, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(record) &lt; <span class="number">5</span> || record[<span class="number">0</span>] != <span class="number">0x16</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;不是 TLS Handshake record&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">recLen := <span class="type">int</span>(binary.BigEndian.Uint16(record[<span class="number">3</span>:<span class="number">5</span>]))</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(record) != <span class="number">5</span>+recLen &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;TLS record 长度不匹配&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">body := record[<span class="number">5</span>:]</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(body) &lt; <span class="number">5</span> || body[<span class="number">0</span>] != <span class="number">1</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;不是 ClientHello&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">info := &amp;ClientHelloInfo&#123;&#125;</span><br><span class="line">info.RecordVersion = binary.BigEndian.Uint16(record[<span class="number">1</span>:<span class="number">3</span>])</span><br><span class="line"></span><br><span class="line">pos := <span class="number">4</span></span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(body) &lt; pos+<span class="number">34</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;ClientHello 主体过短&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">info.HandshakeVersion = binary.BigEndian.Uint16(body[pos : pos+<span class="number">2</span>])</span><br><span class="line">pos += <span class="number">2</span></span><br><span class="line"><span class="built_in">copy</span>(info.Random[:], body[pos:pos+<span class="number">32</span>])</span><br><span class="line">pos += <span class="number">32</span></span><br><span class="line"></span><br><span class="line">sidLen := <span class="type">int</span>(body[pos])</span><br><span class="line">pos++</span><br><span class="line"><span class="keyword">if</span> pos+sidLen &gt; <span class="built_in">len</span>(body) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;Session ID 数据越界&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">info.SessionID = <span class="built_in">make</span>([]<span class="type">byte</span>, sidLen)</span><br><span class="line"><span class="built_in">copy</span>(info.SessionID, body[pos:pos+sidLen])</span><br><span class="line">pos += sidLen</span><br><span class="line"></span><br><span class="line">csLen := <span class="type">int</span>(binary.BigEndian.Uint16(body[pos : pos+<span class="number">2</span>]))</span><br><span class="line">pos += <span class="number">2</span></span><br><span class="line"><span class="keyword">if</span> pos+csLen &gt; <span class="built_in">len</span>(body) || csLen%<span class="number">2</span> != <span class="number">0</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;Cipher Suites 数据越界&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; csLen; i += <span class="number">2</span> &#123;</span><br><span class="line">info.CipherSuites = <span class="built_in">append</span>(info.CipherSuites, binary.BigEndian.Uint16(body[pos+i:pos+i+<span class="number">2</span>]))</span><br><span class="line">&#125;</span><br><span class="line">pos += csLen</span><br><span class="line"></span><br><span class="line">compLen := <span class="type">int</span>(body[pos])</span><br><span class="line">pos++</span><br><span class="line"><span class="keyword">if</span> pos+compLen &gt; <span class="built_in">len</span>(body) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;Compression Methods 数据越界&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; compLen; i++ &#123;</span><br><span class="line">info.CompressionMethods = <span class="built_in">append</span>(info.CompressionMethods, body[pos+i])</span><br><span class="line">&#125;</span><br><span class="line">pos += compLen</span><br><span class="line"></span><br><span class="line">extLen := <span class="type">int</span>(binary.BigEndian.Uint16(body[pos : pos+<span class="number">2</span>]))</span><br><span class="line">pos += <span class="number">2</span></span><br><span class="line"><span class="keyword">if</span> pos+extLen &gt; <span class="built_in">len</span>(body) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;Extensions 数据越界&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">parseExtensions(body[pos:pos+extLen], info)</span><br><span class="line"><span class="keyword">return</span> info, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">parseExtensions</span><span class="params">(exts []<span class="type">byte</span>, info *ClientHelloInfo)</span></span> &#123;</span><br><span class="line">pos := <span class="number">0</span></span><br><span class="line"><span class="keyword">for</span> pos+<span class="number">4</span> &lt;= <span class="built_in">len</span>(exts) &#123;</span><br><span class="line">extType := binary.BigEndian.Uint16(exts[pos : pos+<span class="number">2</span>])</span><br><span class="line">extLen := <span class="type">int</span>(binary.BigEndian.Uint16(exts[pos+<span class="number">2</span> : pos+<span class="number">4</span>]))</span><br><span class="line">pos += <span class="number">4</span></span><br><span class="line"><span class="keyword">if</span> pos+extLen &gt; <span class="built_in">len</span>(exts) &#123;</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line">data := exts[pos : pos+extLen]</span><br><span class="line">info.Extensions = <span class="built_in">append</span>(info.Extensions, Extension&#123;</span><br><span class="line">Type: extType,</span><br><span class="line">Len:  <span class="type">uint16</span>(extLen),</span><br><span class="line">Data: data,</span><br><span class="line">&#125;)</span><br><span class="line"><span class="keyword">switch</span> extType &#123;</span><br><span class="line"><span class="keyword">case</span> <span class="number">0x0000</span>:</span><br><span class="line">info.SNI = parseSNIExt(data)</span><br><span class="line"><span class="keyword">case</span> <span class="number">0x0010</span>:</span><br><span class="line">info.ALPNs = parseALPN(data)</span><br><span class="line"><span class="keyword">case</span> <span class="number">0x002b</span>:</span><br><span class="line">info.SupportedVersions = parseSupportedVersions(data)</span><br><span class="line"><span class="keyword">case</span> <span class="number">0x0033</span>:</span><br><span class="line">info.KeyShareGroups = parseKeyShareGroups(data)</span><br><span class="line"><span class="keyword">case</span> <span class="number">0x000d</span>:</span><br><span class="line">info.SignatureSchemes = parseSignatureSchemes(data)</span><br><span class="line">&#125;</span><br><span class="line">pos += extLen</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">parseSNIExt</span><span class="params">(data []<span class="type">byte</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) &lt; <span class="number">2</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line">listLen := <span class="type">int</span>(binary.BigEndian.Uint16(data[<span class="number">0</span>:<span class="number">2</span>]))</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) != <span class="number">2</span>+listLen || listLen &lt; <span class="number">3</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line">data = data[<span class="number">2</span>:]</span><br><span class="line"><span class="keyword">if</span> data[<span class="number">0</span>] != <span class="number">0</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line">nameLen := <span class="type">int</span>(binary.BigEndian.Uint16(data[<span class="number">1</span>:<span class="number">3</span>]))</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) &lt; <span class="number">3</span>+nameLen &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="type">string</span>(data[<span class="number">3</span> : <span class="number">3</span>+nameLen])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">parseALPN</span><span class="params">(data []<span class="type">byte</span>)</span></span> []<span class="type">string</span> &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) &lt; <span class="number">2</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line">listLen := <span class="type">int</span>(binary.BigEndian.Uint16(data[<span class="number">0</span>:<span class="number">2</span>]))</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) != <span class="number">2</span>+listLen &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line">data = data[<span class="number">2</span>:]</span><br><span class="line"><span class="keyword">var</span> alpn []<span class="type">string</span></span><br><span class="line"><span class="keyword">for</span> <span class="built_in">len</span>(data) &gt;= <span class="number">1</span> &#123;</span><br><span class="line">l := <span class="type">int</span>(data[<span class="number">0</span>])</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) &lt; <span class="number">1</span>+l &#123;</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line">alpn = <span class="built_in">append</span>(alpn, <span class="type">string</span>(data[<span class="number">1</span>:<span class="number">1</span>+l]))</span><br><span class="line">data = data[<span class="number">1</span>+l:]</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> alpn</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">parseSupportedVersions</span><span class="params">(data []<span class="type">byte</span>)</span></span> []<span class="type">uint16</span> &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) &lt; <span class="number">3</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line">listLen := <span class="type">int</span>(data[<span class="number">0</span>])</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) != <span class="number">1</span>+listLen || listLen%<span class="number">2</span> != <span class="number">0</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> vers []<span class="type">uint16</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">1</span>; i &lt; <span class="number">1</span>+listLen; i += <span class="number">2</span> &#123;</span><br><span class="line">vers = <span class="built_in">append</span>(vers, binary.BigEndian.Uint16(data[i:i+<span class="number">2</span>]))</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> vers</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">parseKeyShareGroups</span><span class="params">(data []<span class="type">byte</span>)</span></span> []<span class="type">uint16</span> &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) &lt; <span class="number">2</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line">listLen := <span class="type">int</span>(binary.BigEndian.Uint16(data[<span class="number">0</span>:<span class="number">2</span>]))</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) != <span class="number">2</span>+listLen &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line">data = data[<span class="number">2</span>:]</span><br><span class="line"><span class="keyword">var</span> groups []<span class="type">uint16</span></span><br><span class="line"><span class="keyword">for</span> <span class="built_in">len</span>(data) &gt;= <span class="number">4</span> &#123;</span><br><span class="line">group := binary.BigEndian.Uint16(data[<span class="number">0</span>:<span class="number">2</span>])</span><br><span class="line">keyLen := <span class="type">int</span>(binary.BigEndian.Uint16(data[<span class="number">2</span>:<span class="number">4</span>]))</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) &lt; <span class="number">4</span>+keyLen &#123;</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line">groups = <span class="built_in">append</span>(groups, group)</span><br><span class="line">data = data[<span class="number">4</span>+keyLen:]</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> groups</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">parseSignatureSchemes</span><span class="params">(data []<span class="type">byte</span>)</span></span> []<span class="type">uint16</span> &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) &lt; <span class="number">2</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line">listLen := <span class="type">int</span>(binary.BigEndian.Uint16(data[<span class="number">0</span>:<span class="number">2</span>]))</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(data) != <span class="number">2</span>+listLen || listLen%<span class="number">2</span> != <span class="number">0</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> schemes []<span class="type">uint16</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">2</span>; i &lt; <span class="number">2</span>+listLen; i += <span class="number">2</span> &#123;</span><br><span class="line">schemes = <span class="built_in">append</span>(schemes, binary.BigEndian.Uint16(data[i:i+<span class="number">2</span>]))</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> schemes</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span> &#123;</span><br><span class="line">_ = time.Now</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="运行与验证"><a href="#运行与验证" class="headerlink" title="运行与验证"></a>运行与验证</h2><h3 id="1-编译"><a href="#1-编译" class="headerlink" title="1. 编译"></a>1. 编译</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> reality-study</span><br><span class="line">go build -o reality-study.exe .</span><br></pre></td></tr></table></figure><h3 id="2-运行-带-SNI-路由映射"><a href="#2-运行-带-SNI-路由映射" class="headerlink" title="2. 运行(带 SNI 路由映射)"></a>2. 运行(带 SNI 路由映射)</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./reality-study.exe -listen 127.0.0.1:8444 -dest www.microsoft.com:443 -show -sni-map <span class="string">&quot;www.microsoft.com:microsoft.com:443,www.apple.com:apple.com:443&quot;</span></span><br></pre></td></tr></table></figure><p>启动日志:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">2026/07/19 07:49:15 [+] 监听 127.0.0.1:8444, 默认目标 www.microsoft.com:443</span><br><span class="line">2026/07/19 07:49:15 [+] 已加载 2 条 SNI 路由规则</span><br><span class="line">2026/07/19 07:49:15     www.microsoft.com -&gt; microsoft.com:443</span><br><span class="line">2026/07/19 07:49:15     www.apple.com -&gt; apple.com:443</span><br></pre></td></tr></table></figure><h3 id="3-用-openssl-测试"><a href="#3-用-openssl-测试" class="headerlink" title="3. 用 openssl 测试"></a>3. 用 openssl 测试</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openssl s_client -connect 127.0.0.1:8444 -servername www.microsoft.com -showcerts &lt;/dev/null</span><br></pre></td></tr></table></figure><p>服务端日志:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">2026/07/19 07:49:15 [127.0.0.1:9243] 协议识别: TLS</span><br><span class="line">2026/07/19 07:49:15 [127.0.0.1:9243] TLS ClientHello: SNI=www.microsoft.com, Version=0x0303, Ciphers=30, Exts=12, KeyShareGroups=[4588 29], ALPN=[], Dest=microsoft.com:443</span><br><span class="line">2026/07/19 07:49:15 [127.0.0.1:9243]   CipherSuites: 0x1302, 0x1303, 0x1301, 0xc02c, 0xc030, ...</span><br><span class="line">2026/07/19 07:49:15 [127.0.0.1:9243]   Extension Types: 0xff01, 0x0000, 0x000b, 0x000a, 0x0023, 0x0016, 0x0017, 0x000d, 0x002b, 0x002d, 0x0033, 0x001b</span><br><span class="line">2026/07/19 07:49:15 [127.0.0.1:9243]   SupportedVersions: [TLS 1.3 TLS 1.2]</span><br><span class="line">2026/07/19 07:49:15 [127.0.0.1:9243]   SignatureSchemes: [0x0905 0x0906 ...]</span><br><span class="line">2026/07/19 07:49:15 [127.0.0.1:9243]   SessionID: d2e39b06eab89ac5b1afdd95cadad5d51100c59a489c18536d0f2bb8ef62e365</span><br><span class="line">2026/07/19 07:49:16 [127.0.0.1:9243] 开始双向透传 &lt;-&gt; microsoft.com:443</span><br></pre></td></tr></table></figure><p>openssl 输出关键行:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Connecting to 127.0.0.1</span><br><span class="line">depth=3 C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Global Root G3</span><br><span class="line">verify return:1</span><br><span class="line">depth=2 C=US, O=Microsoft Corporation, CN=Microsoft TLS ECC Root G2</span><br><span class="line">verify return:1</span><br><span class="line">depth=1 C=US, O=Microsoft Corporation, CN=Microsoft TLS G2 ECC CA OCSP 06</span><br><span class="line">verify return:1</span><br><span class="line">depth=0 C=US, ST=WA, L=Redmond, O=Microsoft Corporation, CN=*.azureedge.net</span><br><span class="line">verify return:1</span><br><span class="line">CONNECTED(00000238)</span><br></pre></td></tr></table></figure><p>注意:由于 SNI 映射把 <code>www.microsoft.com</code> 转发到了 <code>microsoft.com:443</code>,而 <code>microsoft.com</code> 实际指向 Azure CDN,所以证书 CN 是 <code>*.azureedge.net</code>,但证书链仍是微软 CA 签发。这正好说明 Demo 的 SNI 路由生效了。</p><h3 id="4-Windows-PowerShell-用户注意事项"><a href="#4-Windows-PowerShell-用户注意事项" class="headerlink" title="4. Windows PowerShell 用户注意事项"></a>4. Windows PowerShell 用户注意事项</h3><p>博客前面的命令都是 bash 语法。在 Windows PowerShell 里运行,要注意三个问题。</p><h4 id="4-1-端口被占用"><a href="#4-1-端口被占用" class="headerlink" title="4.1 端口被占用"></a>4.1 端口被占用</h4><p>如果看到:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">listen tcp 127.0.0.1:8444: bind: Only one usage of each socket address ...</span><br></pre></td></tr></table></figure><p>说明之前启动的 <code>reality-study.exe</code> 还在后台。先杀掉再启动:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">taskkill /F /IM reality<span class="literal">-study</span>.exe</span><br></pre></td></tr></table></figure><p>或者查找具体占用 8444 的进程:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Get-Process</span> <span class="literal">-Id</span> (<span class="built_in">Get-NetTCPConnection</span> <span class="literal">-LocalPort</span> <span class="number">8444</span>).OwningProcess | <span class="built_in">Stop-Process</span> <span class="literal">-Force</span></span><br></pre></td></tr></table></figure><h4 id="4-2-Windows-默认没有-openssl"><a href="#4-2-Windows-默认没有-openssl" class="headerlink" title="4.2 Windows 默认没有 openssl"></a>4.2 Windows 默认没有 openssl</h4><p>PowerShell 里直接敲 <code>openssl</code> 会报 <code>The term &#39;openssl&#39; is not recognized</code>。最常见的解决办法:</p><p><strong>方案 A：用 Git Bash 自带的 openssl</strong></p><p>Git for Windows 安装后,<code>openssl.exe</code> 通常在:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">C:\Program Files\Git\usr\bin\openssl.exe</span><br></pre></td></tr></table></figure><p>在 PowerShell 里可以用完整路径调用,并把它加到 PATH:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$openssl</span> = <span class="string">&quot;C:\Program Files\Git\usr\bin\openssl.exe&quot;</span></span><br><span class="line"><span class="variable">$env:Path</span> += <span class="string">&quot;;C:\Program Files\Git\usr\bin&quot;</span></span><br><span class="line"><span class="string">&quot;Q&quot;</span> | &amp; <span class="variable">$openssl</span> s_client <span class="literal">-connect</span> <span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">8444</span> <span class="literal">-servername</span> www.microsoft.com <span class="literal">-showcerts</span></span><br></pre></td></tr></table></figure><blockquote><p><code>&quot;Q&quot; |</code> 是为了让 openssl 在打印完证书后退出。PowerShell 不支持 bash 的 <code>&lt;/dev/null</code> 输入重定向。</p></blockquote><p><strong>方案 B：单独安装 OpenSSL</strong></p><p>去 <a href="https://slproweb.com/products/Win32OpenSSL.html">slproweb.com&#x2F;products&#x2F;Win32OpenSSL.html</a> 下载 Win64 OpenSSL Light,安装时勾选加入 PATH,重启 PowerShell 后就能直接用 <code>openssl</code>。</p><p><strong>方案 C：用 Git Bash 终端</strong></p><p>直接打开 Git Bash,所有博客里的命令都能原样运行,包括 <code>&lt;/dev/null</code>。</p><h4 id="4-3-PowerShell-不支持-dev-null"><a href="#4-3-PowerShell-不支持-dev-null" class="headerlink" title="4.3 PowerShell 不支持 &lt; /dev/null"></a>4.3 PowerShell 不支持 <code>&lt; /dev/null</code></h4><p>bash 写法:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openssl s_client -connect 127.0.0.1:8444 -servername www.microsoft.com -showcerts &lt;/dev/null</span><br></pre></td></tr></table></figure><p>在 PowerShell 里会报错:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">The &#x27;&lt;&#x27; operator is reserved for future use.</span><br></pre></td></tr></table></figure><p>改成管道:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;Q&quot;</span> | openssl s_client <span class="literal">-connect</span> <span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">8444</span> <span class="literal">-servername</span> www.microsoft.com <span class="literal">-showcerts</span></span><br></pre></td></tr></table></figure><h3 id="5-用浏览器验证-最直观"><a href="#5-用浏览器验证-最直观" class="headerlink" title="5. 用浏览器验证(最直观)"></a>5. 用浏览器验证(最直观)</h3><p>启动 Demo 后,直接用浏览器打开:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://127.0.0.1:8444</span><br></pre></td></tr></table></figure><p>浏览器会提示证书不安全(因为访问的是 IP 而不是域名,和证书 CN 不匹配),点击”高级”查看证书,应该能看到:</p><ul><li>颁发对象: <code>www.microsoft.com</code></li><li>组织: <code>Microsoft Corporation</code></li><li>颁发者: <code>Microsoft TLS G2 RSA CA OCSP 04</code> 之类的微软 CA</li></ul><p>这就证明 Demo 已经把真实网站的证书透传到了你的浏览器。页面可能显示 <code>Invalid URL</code> 或 edgesuite 的错误,这是正常的——这个 Demo 只研究 TLS 层,不处理 HTTP 代理逻辑。</p><hr><h2 id="关键实现点解析"><a href="#关键实现点解析" class="headerlink" title="关键实现点解析"></a>关键实现点解析</h2><h3 id="1-协议首包识别"><a href="#1-协议首包识别" class="headerlink" title="1. 协议首包识别"></a>1. 协议首包识别</h3><p>TLS 的 record 头固定为 5 字节:</p><table><thead><tr><th>字节</th><th>含义</th></tr></thead><tbody><tr><td>0</td><td>ContentType, Handshake &#x3D; 0x16</td></tr><tr><td>1-2</td><td>记录版本</td></tr><tr><td>3-4</td><td>载荷长度</td></tr></tbody></table><p>HTTP 请求则以方法名开头。Demo 据此把连接分为 TLS &#x2F; HTTP &#x2F; 未知三类。</p><h3 id="2-ClientHello-的完整解析"><a href="#2-ClientHello-的完整解析" class="headerlink" title="2. ClientHello 的完整解析"></a>2. ClientHello 的完整解析</h3><p>在 <code>parseClientHello</code> 中,我们按 RFC 5246&#x2F;8446 的结构顺序解析:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Version(2) + Random(32)</span><br><span class="line">+ SessionIDLen(1) + SessionID</span><br><span class="line">+ CipherSuitesLen(2) + CipherSuites</span><br><span class="line">+ CompressionMethodsLen(1) + CompressionMethods</span><br><span class="line">+ ExtensionsLen(2) + Extensions</span><br></pre></td></tr></table></figure><p>扩展部分再逐个解析:</p><ul><li><code>0x0000</code> server_name: 提取 SNI</li><li><code>0x0010</code> ALPN: 提取应用层协议</li><li><code>0x002b</code> supported_versions: 提取 TLS 版本偏好</li><li><code>0x0033</code> key_share: 提取密钥交换 group ID</li><li><code>0x000d</code> signature_algorithms: 提取签名算法</li></ul><h3 id="3-JA3-指纹字段"><a href="#3-JA3-指纹字段" class="headerlink" title="3. JA3 指纹字段"></a>3. JA3 指纹字段</h3><p>JA3 是一种 TLS 客户端指纹算法,它把 ClientHello 中的以下字段拼成一个字符串,然后 MD5:</p><ul><li>TLS Version</li><li>Cipher Suites</li><li>Extensions</li><li>Supported Groups(椭圆曲线)</li><li>EC Point Formats</li></ul><p>我们的 Demo 已经收集了这些字段,如果需要,可以进一步用 <code>crypto/md5</code> 计算完整 JA3 字符串。但这里为了代码简洁和避免外部依赖,只打印原始字段,便于手动观察指纹差异。</p><h3 id="4-SNI-动态路由"><a href="#4-SNI-动态路由" class="headerlink" title="4. SNI 动态路由"></a>4. SNI 动态路由</h3><p>通过 <code>-sni-map</code> 参数,可以指定:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">www.microsoft.com:microsoft.com:443,www.apple.com:apple.com:443</span><br></pre></td></tr></table></figure><p>代码根据解析出的 SNI 选择目标地址。这对应 Reality 配置中的 <code>serverNames</code> 和 <code>dest</code> 映射。</p><h3 id="5-Reality-认证插槽"><a href="#5-Reality-认证插槽" class="headerlink" title="5. Reality 认证插槽"></a>5. Reality 认证插槽</h3><p>代码中保留了一段注释,明确标示了 Reality 认证应发生的位置。这个插槽如果要实现,需要:</p><ul><li>从 <code>info.KeyShareGroups</code> 和原始 record 中取出 X25519 公钥</li><li>用服务端 <code>privateKey</code> 做 <code>curve25519.X25519</code></li><li>用 HKDF 派生 <code>authKey</code></li><li>用 AES-GCM 解密 <code>SessionID[:16]</code></li><li>校验版本、时间、shortId</li></ul><p>本 Demo 没有实现,保持技术研究边界。</p><hr><h2 id="局限与下一步"><a href="#局限与下一步" class="headerlink" title="局限与下一步"></a>局限与下一步</h2><h3 id="当前局限"><a href="#当前局限" class="headerlink" title="当前局限"></a>当前局限</h3><ol><li><strong>未实现 Reality 认证和隧道接管</strong>:Demo 只能透明转发 TLS 握手,不能真正成为代理隧道。</li><li><strong>未处理 TLS 1.3 的 0-RTT &#x2F; 重协商</strong>:只处理最简单的情况。</li><li><strong>JA3 未计算完整 hash</strong>:只打印字段,可进一步用 MD5 计算。</li><li><strong>非 TLS 协议只做透明转发</strong>:没有针对 Shadowsocks&#x2F;VMess&#x2F;Trojan 做解析或识别。</li></ol><h3 id="下一步可深入"><a href="#下一步可深入" class="headerlink" title="下一步可深入"></a>下一步可深入</h3><ol><li><strong>实现 Reality 认证插槽</strong>:把 X25519 + HKDF + AES-GCM 逻辑填进去,做成本地验证 demo。</li><li><strong>接管 TLS 1.3 握手</strong>:参考 <code>github.com/xtls/reality/handshake_server_tls13.go</code>,自己生成 ServerHello、EncryptedExtensions、Certificate 等记录。</li><li><strong>引入 uTLS</strong>:让客户端模拟真实浏览器指纹,而不是用 OpenSSL 的默认指纹。</li><li><strong>做协议识别器</strong>:对 Shadowsocks&#x2F;VMess&#x2F;Trojan 的首包做启发式识别(注意:只能识别”疑似”,不能精确解密)。</li></ol><hr><h2 id="法律声明"><a href="#法律声明" class="headerlink" title="法律声明"></a>法律声明</h2><p>本文及代码仅供<strong>网络协议、TLS 握手和代理协议演进的技术研究</strong>。理解 Shadowsocks、VMess、Trojan、Reality 的设计原理,有助于学习网络安全、DPI 与反检测技术,本身是中性的知识。</p><p>但将这些技术用于搭建或运营可规避网络审查的代理服务,在中国大陆可能违反《网络安全法》《计算机信息网络国际联网管理暂行规定》等法规。技术研究归技术研究,实际部署的法律边界需要自己把握。</p><hr><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li>XTLS&#x2F;Xray-core: <code>https://github.com/XTLS/Xray-core</code></li><li>XTLS&#x2F;REALITY: <code>https://github.com/xtls/reality</code></li><li>Trojan 协议规范: <code>https://trojan-gfw.github.io/trojan/protocol</code></li><li>VMess 协议 DeepWiki: <code>https://deepwiki.com/v2fly/v2ray-core/3.1-vmess-protocol</code></li><li>Shadowsocks AEAD 协议: <code>https://shadowsocks.org/guide/aead</code></li></ul><hr>]]>
    </content>
    <id>http://example.com/2026/07/19/2026071901/</id>
    <link href="http://example.com/2026/07/19/2026071901/"/>
    <published>2026-07-19T01:18:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>摘要:本文从网络分层与 OSI&#x2F;TCP-IP 模型讲起,说明 Shadowsocks、VMess、VLESS、Trojan、Reality 各自工作在哪一层。在此基础上研究了代理协议从 Shadowsocks 到 VMess、Trojan]]>
    </summary>
    <title>Reality 协议研究：从 Shadowsocks 到 Reality 的演进与 Demo 复现</title>
    <updated>2026-07-19T01:51:17.946Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="网络安全" scheme="http://example.com/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="社会工程学" scheme="http://example.com/tags/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="GrapheneOS" scheme="http://example.com/tags/GrapheneOS/"/>
    <category term="手机取证" scheme="http://example.com/tags/%E6%89%8B%E6%9C%BA%E5%8F%96%E8%AF%81/"/>
    <content>
      <![CDATA[<h1 id="手机密码、加密通讯与取证对抗"><a href="#手机密码、加密通讯与取证对抗" class="headerlink" title="手机密码、加密通讯与取证对抗"></a>手机密码、加密通讯与取证对抗</h1><p>一次讯问笔录，一个被要求交出手机密码的嫌疑人，十分钟的”思想教育”后他妥协了。</p><p>这件事背后，涉及法律规制缺位、技术防御边界、加密通讯真相、取证工具能力——每个层面都有人说”这个能保护你”，但每个层面都有一条暗道可以绕过。</p><p>本文提供两种视角的完整拆解：</p><ul><li><strong>上篇：防御者视角</strong>——每个保护机制能做什么、不能做什么</li><li><strong>下篇：攻击者视角</strong>——站在攻击者一侧，审视如何绕过一切</li></ul><p>核心事实一致，分析框架不同。两种视角对照阅读，才能看到完整的图景。</p><hr><h1 id="上篇：防御者视角"><a href="#上篇：防御者视角" class="headerlink" title="上篇：防御者视角"></a>上篇：防御者视角</h1><p><strong>本地销毁 ≠ 真实销毁</strong>。<strong>技术保密 ≠ 信息保密</strong>。<strong>拒绝提供 ≠ 真正保护</strong>。</p><p>每一层技术防御，最终都要用四条暗道来检验——看似安全的操作，都有一条暗道绕过它。</p><hr><h2 id="讯问笔录还原：思想教育十分钟"><a href="#讯问笔录还原：思想教育十分钟" class="headerlink" title="讯问笔录还原：思想教育十分钟"></a>讯问笔录还原：思想教育十分钟</h2><p>截图记录了一次标准的执法讯问过程，嫌疑人对”交出手机密码”的诉求经历了完整的心理曲线：</p><table><thead><tr><th>阶段</th><th>嫌疑人回应</th><th>心理动因</th></tr></thead><tbody><tr><td>第一次拒绝</td><td>“里面有我的<strong>个人</strong>隐私，不想让你们知道”</td><td>援引隐私权</td></tr><tr><td>第二次拒绝</td><td>“我不信，而且我的支付宝和微信里面<strong>还有钱</strong>呢，万一被你们知道密码以后，把我的钱转走了怎么办”</td><td>援引财产权 + 信任缺失</td></tr><tr><td>第三次拒绝</td><td>“我信不过你们，不要问问我了”</td><td>程序性拒绝（不愿解释理由）</td></tr><tr><td>妥协</td><td>“（思想教育十分钟以后）可以”</td><td>在无法拒绝的处境下被迫同意</td></tr></tbody></table><p><strong>关键观察</strong>：</p><ol><li><strong>“个人隐私”被红色标注</strong>——原文以此开篇，重点批判讯问话术将”个人隐私”作为可被”思想教育”突破的软性权利。</li><li><strong>“信不过你们”是程序拒绝</strong>——最有力的回应，因为它不依赖任何具体权利，而是直接质疑侦查权的合法性。但实践中往往被解读为”态度问题”。</li><li><strong>“思想教育十分钟”是程序外的压力</strong>——这十分钟里发生了什么，没有任何记录，没有同步录音录像（仅笔录事后签字）。这正是程序漏洞的具体形态。</li><li><strong>“扣押笔录上有清晰时间，公安机关负责”</strong>——侦查人员用”我们负责”的承诺换取信任，但承诺本身没有法律约束力，事后追责几乎不可能。</li></ol><hr><h2 id="法律层面：手机搜查的程序性困境"><a href="#法律层面：手机搜查的程序性困境" class="headerlink" title="法律层面：手机搜查的程序性困境"></a>法律层面：手机搜查的程序性困境</h2><p>我国对手机搜查的规制存在三层结构性问题：</p><h3 id="立法层面的滞后"><a href="#立法层面的滞后" class="headerlink" title="立法层面的滞后"></a>立法层面的滞后</h3><ul><li><strong>《宪法》40 条</strong>设定了通信自由和通信秘密的基本权利边界，但<strong>没有配套细化规则</strong>。</li><li><strong>《刑事诉讼法》136-140 条</strong>的搜查对象限定在”身体、物品、住处和其他有关的地方”——立法时尚未有”数字生活空间”的概念。</li><li>2016 年”两高一部”《电子数据规定》与 2019 年公安部《电子数据取证规则》只规范了<strong>技术操作</strong>（拆封、提取、封存），<strong>对公民隐私权保护着墨甚少</strong>。</li></ul><p><strong>核心问题</strong>：法律管住了”手机壳”，却没管住”手机里的内容”。</p><h3 id="实践层面的”无证搜查”普遍化"><a href="#实践层面的”无证搜查”普遍化" class="headerlink" title="实践层面的”无证搜查”普遍化"></a>实践层面的”无证搜查”普遍化</h3><p>侦查人员搜查手机数据时，往往<strong>既不需要搜查证，也无需额外审批程序</strong>：</p><ul><li>《刑诉法》138 条的”紧急情况例外”被广泛适用</li><li>内部批准即可启动搜查，<strong>无需法院或检察机关的司法审查</strong></li><li>搜查令无需具体描述数据范围，意味着**”地毯式”检索合法化**</li></ul><h3 id="监督与救济的空白"><a href="#监督与救济的空白" class="headerlink" title="监督与救济的空白"></a>监督与救济的空白</h3><ul><li>现行规定只要求对电子数据的<strong>拆封、提取过程录像</strong>，对<strong>后续查看、分析过程</strong>缺乏监督</li><li>见证人制度形同虚设</li><li>程序违法的后果——非法证据排除规则——在手机数据领域几乎没有适用</li><li>隐私被侵害的<strong>救济渠道几乎空白</strong></li></ul><h3 id="嫌疑人有没有”拒绝权”？"><a href="#嫌疑人有没有”拒绝权”？" class="headerlink" title="嫌疑人有没有”拒绝权”？"></a>嫌疑人有没有”拒绝权”？</h3><ul><li>强迫嫌疑人交出密码 &#x3D; 强迫其配合侦查 &#x3D; <strong>可能违反”不得强迫自证其罪”原则</strong></li><li>但目前<strong>没有明确立法</strong>规定嫌疑人可以拒绝，也没有规定拒绝的法律后果</li><li>在司法实践中，”不配合”通常被作为”态度问题”处理——不会单独评价，但会反映在量刑情节上</li></ul><p><strong>这就是笔录中”思想教育十分钟”存在的制度空间</strong>：法律没有禁止”思想教育”，也没有要求对其录音录像，更没有规定可以拒绝。所以这十分钟是一个<strong>完全脱离监督的权力真空</strong>。</p><h3 id="域外对比"><a href="#域外对比" class="headerlink" title="域外对比"></a>域外对比</h3><table><thead><tr><th>国家&#x2F;地区</th><th>关键规则</th><th>核心机制</th></tr></thead><tbody><tr><td><strong>美国</strong></td><td>赖利诉加州案（2014）</td><td>原则上对手机必须<strong>持证搜查</strong>，无证搜查的证据应予排除</td></tr><tr><td><strong>德国</strong></td><td>《刑事诉讼法》分级审查</td><td>法官批准 + 搜查令需明确<strong>数据类型、时间范围、关键词</strong>；区分”内容信息”与”非内容信息”</td></tr><tr><td><strong>中国</strong></td><td>《刑诉法》138 条 + 内部审批</td><td>内部批准即可，”紧急情况”例外被普遍适用，无证搜查常态化</td></tr></tbody></table><hr><h2 id="底层思维框架：阅后即焚的四条暗道"><a href="#底层思维框架：阅后即焚的四条暗道" class="headerlink" title="底层思维框架：阅后即焚的四条暗道"></a>底层思维框架：阅后即焚的四条暗道</h2><p>4 条泄密暗道，看似与手机密码无关，实则提供了<strong>贯穿全文的底层思维框架</strong>——每一层技术防御，最终都要用这四条暗道来检验：</p><table><thead><tr><th>暗道</th><th>本质</th><th>对应到手机密码场景</th></tr></thead><tbody><tr><td><strong>暗道1：半路截胡</strong></td><td>软件只做了 HTTPS，没有端到端加密</td><td>你以为”我点的是这几个位置”，但对方的取证工具记录了你<strong>真实的点击坐标</strong>——你描述的位置和真实位置对不上，等于没给</td></tr><tr><td><strong>暗道2：服务器留底</strong></td><td>“阅后即焚”只是手机端删除，服务器仍有明文</td><td>你以为擦除手机就完事了，但云端备份（iCloud、Google Photos、微信聊天记录云端）仍然保留</td></tr><tr><td><strong>暗道3：屏幕翻拍</strong></td><td>物理拍摄或录屏，软件拦不住</td><td>你在解锁时对方<strong>直接看你的屏幕</strong>（或者用另一台手机拍）——你输的什么数字他全看到了</td></tr><tr><td><strong>暗道4：境外服务器脱管</strong></td><td>数据被境外势力掌握</td><td>你的手机是”国产”还是”境外品牌”？Pixel（Google）的取证权限归属是另一个问题</td></tr></tbody></table><p><strong>贯穿全文的三个不等式</strong>：</p><blockquote><p><strong>本地销毁 ≠ 真实销毁</strong>。<strong>技术保密 ≠ 信息保密</strong>。<strong>拒绝提供 ≠ 真正保护</strong>。</p></blockquote><p>这三条暗道和三个不等式，将在后续每一层技术分析中被反复验证。</p><hr><h2 id="GrapheneOS-技术方案详解"><a href="#GrapheneOS-技术方案详解" class="headerlink" title="GrapheneOS 技术方案详解"></a>GrapheneOS 技术方案详解</h2><p>GrapheneOS 是一个以隐私和安全为核心的安卓 ROM。以下是与执法场景最相关的全部技术方案。</p><h3 id="胁迫密码（Duress-PIN-Password）"><a href="#胁迫密码（Duress-PIN-Password）" class="headerlink" title="胁迫密码（Duress PIN&#x2F;Password）"></a>胁迫密码（Duress PIN&#x2F;Password）</h3><p><strong>机制</strong>：</p><ul><li>用户预设一个<strong>特殊的胁迫 PIN&#x2F;密码</strong></li><li>在<strong>任何要求设备凭据的输入点</strong>输入该密码，设备<strong>不可逆地擦除所有数据</strong>（包括 eSIM）</li><li>擦除在<strong>后台静默进行</strong>，不需要重启，无法中断</li><li><strong>对方看不到任何提示</strong></li></ul><p><strong>触发范围（覆盖所有 PIN&#x2F;密码输入点）</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">- 锁屏界面</span><br><span class="line">- 修改密码时的&quot;确认当前密码&quot;</span><br><span class="line">- 开启开发者选项时的 PIN 输入</span><br><span class="line">- 应用内 PIN 校验弹窗</span><br><span class="line">- 双因素指纹解锁的 PIN 输入</span><br></pre></td></tr></table></figure><p><strong>唯一例外</strong>：SIM 卡 PIN（基带侧，与系统凭据无关）。</p><p><strong>技术细节</strong>：PIN 模式下设纯数字胁迫 PIN，Password 模式下设字母数字胁迫密码，两者都需设置才能完整启用。如果胁迫密码与真实密码相同，真实密码始终优先，不会误擦。</p><p><strong>风险与边界</strong>：</p><table><thead><tr><th>维度</th><th>说明</th></tr></thead><tbody><tr><td><strong>不可逆</strong></td><td>一旦触发，数据无法恢复</td></tr><tr><td><strong>eSIM 一起擦</strong></td><td>需要重新去运营商激活</td></tr><tr><td><strong>擦除需要时间</strong></td><td>后台擦除非瞬间完成，对方可能在擦完前看到部分内容</td></tr><tr><td><strong>自身风险</strong></td><td>自己手滑输错也会擦——胁迫密码要与真密码差异足够大</td></tr><tr><td><strong>法律风险</strong></td><td>在合法执法场景下使用它销毁数据，在很多法域可能构成独立的违法行为（如妨碍公务、毁灭证据）</td></tr></tbody></table><p><strong>适用性</strong>：</p><ul><li>✅ <strong>适用</strong>：被抢劫、被绑架等<strong>非法胁迫</strong></li><li>⚠️ <strong>灰色</strong>：亲属有急事需要你解锁手机”看聊天记录找线索”，你不是自愿但又不能完全拒绝——胁迫密码可以避免被深度窥探</li><li>❌ <strong>不适用</strong>：合法执法讯问——主动擦除正在被合法搜查的设备数据，可能构成妨碍公务罪</li></ul><p><strong>关键问题</strong>：执法讯问中的”思想教育”与”非法胁迫”如何区分？答案在当前法律框架下<strong>没有清晰边界</strong>。</p><h3 id="机锁屏密码键盘"><a href="#机锁屏密码键盘" class="headerlink" title="机锁屏密码键盘"></a>机锁屏密码键盘</h3><p><strong>机制</strong>：每次解锁时，0-9 这十个数字在屏幕上的<strong>位置随机变化</strong>，但对应的数字本身不变。</p><p><strong>防御效果</strong>：</p><table><thead><tr><th>攻击类型</th><th>随机键盘的防御效果</th></tr></thead><tbody><tr><td><strong>肩膀偷窥（shoulder surfing）</strong></td><td>⭐⭐⭐⭐ 显著提升防御——即便对方看到了你点击的位置，也无法对应到具体数字</td></tr><tr><td><strong>屏幕油渍攻击（smudge attack）</strong></td><td>⭐⭐⭐⭐⭐ 完全消除——每次位置不同，油渍没有可解读的固定模式</td></tr><tr><td><strong>侧信道攻击（点击力度、时间、面积）</strong></td><td>⭐⭐⭐ 提升难度但未消除</td></tr><tr><td><strong>直接物理胁迫（让你当面解锁）</strong></td><td>❌ 几乎无效——你自己输入，你知道每个位置对应的数字</td></tr></tbody></table><p><strong>对讯问笔录场景</strong>：嫌疑人最终同意提供密码时需要<strong>主动告知数字本身</strong>（而不是”我点哪几个位置”），因为解锁时数字位置会重新随机化。随机键盘在这一步<strong>没有帮助</strong>——它对抗的是偷窥，不是口述。但日常使用中降低被偷窥到的概率（地铁、咖啡馆等场景）是其<strong>间接价值</strong>。</p><p><strong>真正的对抗手段</strong>不是”让对方不知道你点哪里”，而是”让对方不知道你输入了什么”——这需要胁迫密码 + 自动重启等其他机制的配合。</p><h3 id="其他相关设计"><a href="#其他相关设计" class="headerlink" title="其他相关设计"></a>其他相关设计</h3><table><thead><tr><th>设计</th><th>机制</th><th>对抗效果</th></tr></thead><tbody><tr><td><strong>自动重启 + BFU 状态</strong></td><td>锁定后 10 分钟-72 小时自动重启，重启后进入 BFU（Before First Unlock），加密密钥从内存清除</td><td>⭐⭐⭐⭐⭐ <strong>关键防御</strong>——重启后即使被物理拿走，面对全盘加密，暴力破解几乎不可能</td></tr><tr><td><strong>锁屏后禁用 USB-C 数据</strong></td><td>硬件层面切断数据线（含 DisplayPort 等替代模式）</td><td>⭐⭐⭐⭐⭐ 防止通过 USB 取证提取数据</td></tr><tr><td><strong>网络权限开关</strong></td><td>全局禁止任何应用直接和间接访问网络</td><td>⭐⭐⭐⭐ 防止手机被解锁后数据自动外泄</td></tr><tr><td><strong>用户配置文件系统</strong></td><td>上限 32 个配置文件，结束会话可清除加密密钥</td><td>⭐⭐⭐⭐ 敏感数据可隔离在 Owner profile，临时解锁可只解锁辅助 profile</td></tr><tr><td><strong>eSIM 自动擦除</strong></td><td>胁迫密码触发时连 eSIM 一起擦除</td><td>⭐⭐⭐ 防止 eSIM 信息被追溯</td></tr></tbody></table><h3 id="技术方案适用边界（综合对比）"><a href="#技术方案适用边界（综合对比）" class="headerlink" title="技术方案适用边界（综合对比）"></a>技术方案适用边界（综合对比）</h3><table><thead><tr><th>方案</th><th>防御对象</th><th>优势</th><th>边界&#x2F;风险</th><th>推荐场景</th></tr></thead><tbody><tr><td><strong>随机锁屏键盘</strong></td><td>偷窥、油渍、侧信道</td><td>防止日常被窥探</td><td>对当面解锁无效</td><td>公共场合日常使用</td></tr><tr><td><strong>胁迫密码</strong></td><td>物理胁迫（被抢劫、绑架）</td><td>不可逆擦除，无声执行</td><td>合法执法场景下使用可能违法；不可逆；eSIM 一起擦</td><td>真正的人身威胁</td></tr><tr><td><strong>自动重启 + BFU</strong></td><td>取证提取</td><td>加密密钥不留内存</td><td>重启需要时间窗口；解锁后（AFU）仍可取证</td><td>长期不在设备身边时</td></tr><tr><td><strong>锁屏 USB 切断</strong></td><td>物理取证</td><td>硬件级防御</td><td>充电也受限时自己也不方便</td><td>高度敏感环境</td></tr><tr><td><strong>多 profile 隔离</strong></td><td>解锁后的全盘查看</td><td>敏感数据不放在主 profile</td><td>需要日常切换，便利性下降</td><td>业务&#x2F;私人分离场景</td></tr><tr><td><strong>网络权限开关</strong></td><td>数据自动外泄</td><td>防”解锁后被远程拉取”</td><td>影响所有联网功能</td><td>临时解锁敏感设备时</td></tr></tbody></table><h3 id="四种情形推演"><a href="#四种情形推演" class="headerlink" title="四种情形推演"></a>四种情形推演</h3><p>如果讯问笔录中的嫌疑人<strong>使用的是一台刷了 GrapheneOS 的 Pixel</strong>：</p><p><strong>情形 A：嫌疑人选择输入真实密码</strong></p><ul><li>手机正常解锁</li><li>侦查人员可以查看所有数据（除非用了多 profile 隔离敏感数据）</li><li>后续：在 AFU 状态下，可被专业取证设备提取内存中的加密密钥，但 Titan M 安全芯片 + 全盘加密让离线暴力破解几乎不可能</li></ul><p><strong>情形 B：嫌疑人输入胁迫密码（如果设置了）</strong></p><ul><li>手机<strong>静默开始后台擦除所有数据</strong>（包括 eSIM）</li><li>表面上看手机”正常解锁”了，但内部数据正在消失</li><li>风险：擦除需要时间，对方可能在擦完前看到部分内容；一旦被发现”数据不见了 + 手机被解锁了”——可能直接推定”故意毁灭证据”；法律后果可能比”不交密码”更严重</li></ul><p><strong>情形 C：嫌疑人设置真实密码与胁迫密码两套</strong></p><ul><li>真实密码解锁后，正常查看</li><li>对方强制要求”再输一次”时，可以输入胁迫密码触发擦除</li><li>风险：擦除需要时间，且对方可能已经看到了部分内容</li></ul><p><strong>情形 D：嫌疑人开启自动重启（10 分钟）</strong></p><ul><li>锁定 10 分钟后自动重启进入 BFU</li><li>对方拿到手机时已经是 BFU 状态</li><li>加密密钥不在内存中，离线取证几乎不可能</li><li>但这不能阻止”思想教育十分钟后嫌疑人主动解锁”的情形</li></ul><p><strong>推演结论</strong>：单纯依赖技术防御<strong>不能完全对抗合法执法 + 解锁后的全面查看</strong>。真正有效的是”<strong>事前不留</strong>“——把敏感数据放在不在手机上的地方。多 profile 隔离 + 业务&#x2F;私人分离是<strong>最务实的工程化方案</strong>。</p><h3 id="原文关键陈述的防御能力对照"><a href="#原文关键陈述的防御能力对照" class="headerlink" title="原文关键陈述的防御能力对照"></a>原文关键陈述的防御能力对照</h3><blockquote><p>“这个手机的操作系统可以在手机解开 Bootloader 锁之后，还可以在刷完系统之后再将系统的 Bootloader 锁回去”<br>“可以设置接口，彻底关掉接口的所有功能，就算是充电都能关掉”<br>“也不知道这个系统能不能对抗帽子叔叔的信息采集系统”</p></blockquote><p>这三句原文陈述，恰好印证了 GrapheneOS 三层防御能力的不同侧面。逐一对照：</p><p><strong>第一句印证：Bootloader 锁回去 → Verified Boot 防篡改能力</strong></p><p>刷完系统后还能重新锁定 Bootloader，是 Verified Boot 生效的前提。这意味着：</p><ul><li>启动时每一层固件和软件都经过加密签名验证，<strong>确保系统未被篡改</strong></li><li>锁定后攻击者无法通过刷入恶意系统来植入持久化后门</li><li>配合 Auditor 应用，可远程验证设备是否保持原始状态</li></ul><p><strong>第二句印证：接口彻底关掉 → 物理取证阻断能力</strong></p><p>USB-C 在硬件层面和操作系统层面双重阻止数据连接，甚至充电都能关掉。这意味着：</p><ul><li>锁定后<strong>取证设备无法通过 USB 连接提取数据</strong></li><li>含 DisplayPort 等替代模式也被阻止，没有旁路可绕</li><li>BFU 状态下可选允许数据（兼顾安全与便利）</li></ul><p><strong>第三句印证：能不能对抗帽子叔叔 → 分层防护能力</strong></p><table><thead><tr><th>层面</th><th>GrapheneOS 的防护能力</th></tr></thead><tbody><tr><td><strong>BFU 状态下的暴力破解</strong></td><td>Titan M + 全盘加密提供工业级防护——离线暴力破解几乎不可能</td></tr><tr><td><strong>AFU 状态下的专业取证</strong></td><td>大幅提高取证难度——Titan M + 全盘加密仍构成坚实屏障，专业设备仅可能部分成功</td></tr><tr><td><strong>已解锁后的全盘查看</strong></td><td>多 profile 隔离提供工程化方案——敏感数据放在 Owner profile，临时解锁只开辅助 profile</td></tr><tr><td><strong>物理胁迫场景</strong></td><td>胁迫密码提供技术选项——静默擦除、不可逆、覆盖所有输入点；法律风险需自行评估</td></tr><tr><td><strong>USB 物理取证</strong></td><td>硬件级阻断——锁定后 USB-C 数据连接完全切断</td></tr><tr><td><strong>云端数据</strong></td><td>与设备无关——Pixel 本身也涉及 Google 数据归属问题</td></tr><tr><td><strong>跨设备追踪</strong></td><td>超出 GrapheneOS 控制范围</td></tr></tbody></table><p><strong>综合回答</strong>：GrapheneOS 能对抗的是<strong>大多数非针对性的、非专业的、非持续的取证尝试</strong>。对于国家级、有组织的、持续的资源投入，任何终端设备都会面临资源不对等的挑战——这不是 GrapheneOS 的局限，而是所有终端设备的客观边界。</p><p>技术防护的核心价值：1) <strong>提高攻击成本</strong>——让低成本的大规模监控变得不可行；2) <strong>保护非高价值目标</strong>——99% 的人不是高价值目标；3) <strong>在法律灰色地带提供选项</strong>——“我忘了密码”配合 BFU 状态至少让取证难度大幅上升；4) <strong>保留个体对自身数据的控制权</strong>——这是意识形态层面的，而非纯粹技术层面的。</p><hr><h2 id="侧信道对抗点（非技术对抗）"><a href="#侧信道对抗点（非技术对抗）" class="headerlink" title="侧信道对抗点（非技术对抗）"></a>侧信道对抗点（非技术对抗）</h2><p>从「社会工程学+程序员」的视角看，这个场景还有几个被忽略的<strong>非技术对抗点</strong>：</p><ol><li><p><strong>“我忘了”是最强的技术推诿</strong>——因为无法证明”遗忘”是真是假（密码学上验证”知道”很容易，验证”不知道”几乎不可能）。配合 BFU 状态和自动重启，”我忘了”就从”狡辩”变成了”技术上无法验证”。</p></li><li><p><strong>“我设置的是随机密码管理器生成的 64 位密码，我背不下来”</strong>——合法的、合理的、可验证的”遗忘”声明。配合 1Password &#x2F; Bitwarden &#x2F; KeePassXC 等工具，可以做到既合规又安全。</p></li><li><p><strong>“律师来之前我不会回答任何问题”</strong>——程序性权利，不需要解释理由。笔录中嫌疑人说”不要问问我了”虽然表达类似意思，但缺少”援引法律”的措辞，程序效力会打折。</p></li><li><p><strong>“请出示你的证件、搜查令、职务证明”</strong>——程序对程序的回应。对方是”侦查人员”还是”临时工”？有证还是无证？</p></li><li><p><strong>“请同步录音录像，包括这十分钟思想教育的内容”</strong>——直接挑战程序漏洞。如果对方拒绝，至少在后续可能的行政复议&#x2F;诉讼中留下了”对方拒绝透明化”的证据。</p></li></ol><p><strong>这些都不需要任何技术对抗，纯粹是程序性权利的合理援引</strong>——但实践中，绝大多数嫌疑人不知道自己有这些权利，或者不敢援引。</p><hr><h2 id="通讯架构的本质差异"><a href="#通讯架构的本质差异" class="headerlink" title="通讯架构的本质差异"></a>通讯架构的本质差异</h2><p><strong>这是理解两者加密方式差异的根基——通讯架构决定了加密能做什么、不能做什么。</strong></p><h3 id="Telegram：中心化消息路由器"><a href="#Telegram：中心化消息路由器" class="headerlink" title="Telegram：中心化消息路由器"></a>Telegram：中心化消息路由器</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">发送方 ──加密──→ Telegram 服务器 ──解密──→ ──重新加密──→ 接收方</span><br><span class="line">                  │                    │</span><br><span class="line">                  └─ 存储明文副本       ── 云端同步到所有设备</span><br><span class="line">                  └─ 路由消息           └─ 搜索索引</span><br><span class="line">                  └─ 推送通知           └─ 媒体转码</span><br><span class="line"></span><br><span class="line">Secret Chat 模式：</span><br><span class="line">发送方 ──E2E加密──→ Telegram 服务器 ──仅转发密文──→ 接收方（唯一绑定设备）</span><br><span class="line">                     │</span><br><span class="line">                     └─ 不存储明文</span><br><span class="line">                     └─ 不同步到其他设备</span><br><span class="line">                     └─ 7天后删除密文副本</span><br></pre></td></tr></table></figure><p>Telegram 选择”中心化消息路由器”架构，牺牲了默认 E2E，换取了：多设备无缝同步、云端搜索、媒体转码、离线消息队列。这些功能在严格的 E2E 加密下几乎不可能实现——因为服务器看不到明文。</p><h3 id="WhatsApp：去中心化加密信封分发器"><a href="#WhatsApp：去中心化加密信封分发器" class="headerlink" title="WhatsApp：去中心化加密信封分发器"></a>WhatsApp：去中心化加密信封分发器</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">发送方 ──E2E加密──→ WhatsApp 服务器 ──仅分发密文信封──→ 接收方设备1</span><br><span class="line">                                           │</span><br><span class="line">                                           └─ 独立密文信封──→ 接收方设备2</span><br><span class="line">                                           └─ 独立密文信封──→ 接收方设备3</span><br><span class="line">                                           └─ 密文暂存（接收方离线时）</span><br><span class="line"></span><br><span class="line">多设备同步：</span><br><span class="line">主设备 ──本地加密打包──→ 加密消息历史块 ──→ 配对设备</span><br><span class="line">         │                    │</span><br><span class="line">         └─ 用设备专属密钥加密  └─ 密钥通过 E2E 消息传递</span><br><span class="line">                              └─ 解密后密钥立即删除</span><br><span class="line">                              └─ 配对设备从本地数据库访问历史</span><br></pre></td></tr></table></figure><p>WhatsApp 选择”去中心化加密信封分发”架构，所有消息默认 E2E 加密，服务器只分发密文信封：消息不同步到服务器、每设备独立加密、历史同步通过本地加密传输、服务器无法搜索&#x2F;转码。</p><h3 id="架构-trade-off"><a href="#架构-trade-off" class="headerlink" title="架构 trade-off"></a>架构 trade-off</h3><p>Telegram 的中心化架构让以下功能<strong>变得可能</strong>：</p><ul><li>所有设备登录后自动获取完整历史（E2E 下不可能——服务器没有明文）</li><li>云端搜索消息内容（E2E 下不可能——服务器无法建立索引）</li><li>媒体自动转码适配（E2E 下不可能——服务器看不到文件内容）</li></ul><p>WhatsApp 的去中心化架构让以下功能<strong>变得不可能</strong>：</p><ul><li>云端搜索消息内容（只能在本地搜索）</li><li>媒体自动转码（只能原文件传输）</li><li>新设备自动获取完整历史（只能从主设备加密传输最近数月）</li><li>服务器端推送通知带内容（只能”你有一条新消息”)</li></ul><p><strong>这不是”谁更安全”的问题——这是”你愿意为了安全牺牲哪些便利”的问题。</strong></p><p>Telegram 的答案：大多数用户不需要 E2E，需要的是便利——所以默认 Cloud Chat，Secret Chat 作为可选。<br>WhatsApp 的答案：所有用户都应该有 E2E——所以默认 E2E，用多设备架构弥补便利性损失。<br>Signal 的答案：所有用户必须有 E2E，且元数据也应该最小化——所以默认 E2E + 最小元数据收集 + 非营利运营。</p><hr><h2 id="Telegram-加密与配合现状"><a href="#Telegram-加密与配合现状" class="headerlink" title="Telegram 加密与配合现状"></a>Telegram 加密与配合现状</h2><h3 id="加密架构"><a href="#加密架构" class="headerlink" title="加密架构"></a>加密架构</h3><p>Telegram <strong>默认是 client-server 加密</strong>，而非端到端加密。</p><ul><li>默认聊天（包括所有群聊）：消息在客户端加密后传到 Telegram 服务器，服务器<strong>解密后重新加密存储</strong>——Telegram 服务器能看到明文。</li><li>只有手动开启的「<strong>秘密聊天（Secret Chat）</strong>」才是真正的端到端加密，且仅限一对一，<strong>群聊永远不支持 E2E</strong>。</li><li>秘密聊天的消息<strong>不留在服务器</strong>，但普通聊天的聊天记录<strong>全部存在云端</strong>，Telegram 服务器能解密。</li><li>这也是为什么 Telegram 能支持”多设备同步历史记录”——因为服务器有明文副本。</li></ul><h3 id="政府配合现状"><a href="#政府配合现状" class="headerlink" title="政府配合现状"></a>政府配合现状</h3><p>2024 年 8 月之前，Telegram 基本不配合各国政府的执法请求。但 Durov 在法国被捕后，政策发生了重大转向：</p><table><thead><tr><th>时间</th><th>事件</th></tr></thead><tbody><tr><td>2024-08-24</td><td>Pavel Durov 在法国巴黎勒布尔歇机场被逮捕，被控 12 项罪名（包括共谋贩毒、洗钱、未配合当局）</td></tr><tr><td>2024-09</td><td>Telegram <strong>更新隐私政策</strong>，开始收集并保留元数据（IP 地址、设备信息、用户名变更记录）<strong>最长 1 年</strong></td></tr><tr><td>2024 下半年起</td><td>Durov 公开承诺”加强配合”，<strong>过去 6 个月内向全球执法机构提供了约 10,000 个账户的数据</strong></td></tr><tr><td>2025 起</td><td>Telegram 在收到有效法院命令时，<strong>会提供用户 IP 地址和电话号码</strong></td></tr></tbody></table><p>当前状态的分层解读：</p><ul><li>✅ <strong>秘密聊天的 E2E 加密仍然存在</strong>——服务器技术上仍然看不到内容</li><li>⚠️ <strong>普通聊天的内容服务器可以看到</strong>——配合政府时可以提供</li><li>⚠️ <strong>元数据（IP、设备、用户名变更）现在会保留 1 年</strong>——可被司法调取</li><li>⚠️ Durov 坚称”宁死也不给第三方访问私人消息的权限”——但这个承诺<strong>只对秘密聊天有效</strong></li></ul><h3 id="MTProto-2-0-加密详解（普通聊天——Client-Server-加密）"><a href="#MTProto-2-0-加密详解（普通聊天——Client-Server-加密）" class="headerlink" title="MTProto 2.0 加密详解（普通聊天——Client-Server 加密）"></a>MTProto 2.0 加密详解（普通聊天——Client-Server 加密）</h3><p><strong>密钥建立</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">客户端                                服务器</span><br><span class="line">  │                                    │</span><br><span class="line">  │── Request: nonce + DH params ────→│</span><br><span class="line">  │←── Response: server_nonce ────────│</span><br><span class="line">  │── 2048-bit RSA 加密的 nonce ─────→│</span><br><span class="line">  │←── Server 确认 ──────────────────│</span><br><span class="line">  │    双方计算 auth_key = DH(a, B)    │</span><br><span class="line">  │    auth_key_id = SHA1(auth_key)的低64位</span><br></pre></td></tr></table></figure><ul><li><strong>auth_key</strong>：2048-bit，注册时通过 Diffie-Hellman 交换生成，<strong>长期不变</strong></li><li>每个设备有独立的 auth_key → 支持多设备登录</li><li>auth_key <strong>从不通过网络传输</strong>——只在本地和服务器各持一份</li></ul><p><strong>消息加密流程</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">plaintext + padding (12-1024 random bytes)</span><br><span class="line">    ↓</span><br><span class="line">msg_key = SHA256(auth_key[88+x:88+x+32] + plaintext + padding) 的中间 128 位</span><br><span class="line">    ↓                          (x=0 发送方, x=8 接收方)</span><br><span class="line">aes_key/iv = SHA256(msg_key + auth_key片段) 的多步拼接</span><br><span class="line">    ↓</span><br><span class="line">ciphertext = AES-256-IGE(aes_key, aes_iv, plaintext + padding)</span><br><span class="line">    ↓</span><br><span class="line">发送: auth_key_id (64bit) + msg_key (128bit) + ciphertext</span><br></pre></td></tr></table></figure><p><strong>关键特性</strong>：</p><ul><li>AES-256-IGE（Infinite Garble Extension）——Telegram 自选的非标准 AES 模式</li><li>每条消息有不同的 msg_key → 不同的 aes_key&#x2F;iv——提供消息级别的随机性</li><li>但 <strong>auth_key 是长期不变的</strong>——窃取 auth_key 可解密所有历史消息</li><li>服务器持有 auth_key → 可以解密所有消息 → 这就是”服务器能看到明文”的技术原因</li></ul><p><strong>前向安全</strong>：普通聊天<strong>无真正的前向安全</strong>——auth_key 不变，服务器泄露 auth_key 可解密所有消息。MTProto 支持 PFS 机制（auth_key 定期重新协商），但实际部署中很少使用。</p><h3 id="Secret-Chat-加密详解（端到端加密）"><a href="#Secret-Chat-加密详解（端到端加密）" class="headerlink" title="Secret Chat 加密详解（端到端加密）"></a>Secret Chat 加密详解（端到端加密）</h3><p><strong>密钥建立</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">设备A (发起方)                         设备B (接受方)</span><br><span class="line">  │── messages.getDhConfig ───────────→│ 获取 DH 参数 (p, g)</span><br><span class="line">  │←── DH 参数 ──────────────────────│</span><br><span class="line">  │  生成随机数 a                       │  生成随机数 b</span><br><span class="line">  │  g_a = pow(g, a) mod p             │  g_b = pow(g, b) mod p</span><br><span class="line">  │── messages.requestEncryption ─────→│ g_a</span><br><span class="line">  │←── messages.acceptEncryption ─────│ g_b + key_fingerprint</span><br><span class="line">  │  key = pow(g_b, a) mod p           │  key = pow(g_a, b) mod p</span><br><span class="line">  │  (填充至 256 字节)                  │  (填充至 256 字节)</span><br><span class="line">  │  验证 key_fingerprint              │  计算 key_fingerprint</span><br><span class="line">  │  = SHA1(key) 的最后 64 位           │  = SHA1(key) 的最后 64 位</span><br></pre></td></tr></table></figure><p><strong>与普通聊天的加密差异</strong>：</p><ul><li>密钥只在两端之间通过 DH 交换生成，<strong>服务器不参与密钥协商</strong></li><li>key 是 256 字节的共享密钥，<strong>只存在于两个绑定设备上</strong></li><li>消息加密算法相同（AES-256-IGE），但密钥来源不同</li><li>设备绑定：只有执行 acceptEncryption 的设备能访问——B 的其他设备收到 encryptedChatDiscarded</li></ul><p><strong>前向安全实现</strong>：</p><ul><li>触发条件：key 用于加密&#x2F;解密超过 <strong>100 条消息</strong> 或使用超过 <strong>1 周</strong></li><li>轮换方式：在现有 Secret Chat 内重新执行 DH 交换 → 新的 key</li><li>旧 key <strong>安全删除</strong>，无法从新 key 重建</li><li>提供了<strong>中等程度的前向安全</strong>——但不如 Signal Protocol 的每消息级别前向安全</li></ul><p><strong>可视化验证码（Emoji 比较）</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">visualization_data = SHA1(initial_key)[0:128] + SHA256(layer46_upgrade_key)[0:160]</span><br><span class="line">    ↓</span><br><span class="line">映射为一组 emoji 图标，双方在设置页面对比验证</span><br><span class="line">    ↓</span><br><span class="line">用途：检测中间人攻击（如果 emoji 不一致 → 密钥交换被篡改）</span><br></pre></td></tr></table></figure><p><strong>文件加密</strong>：文件使用独立一次性密钥（与聊天 key 无关），2 个随机 256 位数 → AES key + IV → AES-256-IGE 加密，解密密钥在消息体内传输，文件地址在消息体外传输。</p><h3 id="MTProto-2-0-vs-1-0-与密码学评估"><a href="#MTProto-2-0-vs-1-0-与密码学评估" class="headerlink" title="MTProto 2.0 vs 1.0 与密码学评估"></a>MTProto 2.0 vs 1.0 与密码学评估</h3><p><strong>MTProto 2.0 vs 1.0</strong>：</p><table><thead><tr><th>特性</th><th>MTProto 1.0</th><th>MTProto 2.0</th></tr></thead><tbody><tr><td>哈希算法</td><td>SHA-1</td><td>SHA-256</td></tr><tr><td>msg_key 计算</td><td>消息体（不含 padding）</td><td>消息体 + padding + auth_key&#x2F;key 片段</td></tr><tr><td>填充字节</td><td>0-15</td><td>12-1024（防长度攻击）</td></tr><tr><td>密钥指纹</td><td>SHA-1</td><td>密钥指纹仍用 SHA-1（未升级）</td></tr><tr><td>MAC</td><td>自定义</td><td>HMAC-based</td></tr></tbody></table><p><strong>密码学风险</strong>：</p><ol><li>AES-IGE 不是标准模式——实现容易出错，缺乏广泛审计</li><li>密钥派生过于复杂——多步 SHA-256 拼接而非标准 HKDF</li><li>密钥指纹仍用 SHA-1——理论上存在碰撞风险</li><li>无标准安全证明——自定义协议缺乏数学证明</li><li>普通聊天 auth_key 长期不变——窃取可解密所有历史消息</li></ol><p><strong>Telegram 的回应</strong>：全规范公开 + $300K-$500K 漏洞赏金、无已知实际攻击、设计优先考虑移动网络性能。</p><hr><h2 id="WhatsApp-加密与安全边界"><a href="#WhatsApp-加密与安全边界" class="headerlink" title="WhatsApp 加密与安全边界"></a>WhatsApp 加密与安全边界</h2><h3 id="安全边界分层审视"><a href="#安全边界分层审视" class="headerlink" title="安全边界分层审视"></a>安全边界分层审视</h3><table><thead><tr><th>层面</th><th>状态</th></tr></thead><tbody><tr><td><strong>消息内容</strong></td><td>✅ E2E 加密，基于 Signal Protocol（Curve25519 + AES-256 + HMAC-SHA256），WhatsApp 服务器看不到</td></tr><tr><td><strong>元数据</strong></td><td>⚠️ <strong>大量收集</strong>：电话号码、时间戳、通信频率、IP 地址、设备信息、联系人列表</td></tr><tr><td><strong>与 Meta 共享</strong></td><td>⚠️ 元数据与 Facebook&#x2F;Instagram 共享，用于”运营、安全、商业功能”</td></tr><tr><td><strong>云端备份</strong></td><td>⚠️ <strong>默认不加密</strong>——iCloud &#x2F; Google Drive 上的备份是明文，执法机关可直接向 Apple&#x2F;Google 调取</td></tr><tr><td><strong>E2E 加密备份</strong></td><td>⚠️ 2025 年 10 月引入 passkey-based E2E 备份，但 <strong>opt-in，默认关闭</strong></td></tr><tr><td><strong>Meta AI 对话</strong></td><td>⚠️ <strong>不是 E2E 加密</strong>——会被 Meta 收集用于广告定向</td></tr><tr><td><strong>商业账号对话</strong></td><td>⚠️ 收到后转为”受商家隐私政策管辖”——E2E 保护失效</td></tr></tbody></table><p>关于政府配合：</p><ul><li>WhatsApp <strong>多次拒绝向政府提供消息内容</strong>（技术上自己也看不到，无法提供）——巴西曾因此多次封禁 WhatsApp</li><li>但 <strong>会配合提供元数据</strong>——这是关键区别</li><li>2025 年 6 月，WhatsApp 向英国法院申请作证，<strong>反对</strong>英国政府要求苹果在 iCloud 植入加密后门的命令——立场是坚决反对后门</li><li>反对后门 ≠ 不配合一切——元数据配合是常态</li></ul><h3 id="X3DH-密钥交换"><a href="#X3DH-密钥交换" class="headerlink" title="X3DH 密钥交换"></a>X3DH 密钥交换</h3><p><strong>三种密钥对</strong>：</p><table><thead><tr><th>密钥类型</th><th>算法</th><th>生命周期</th><th>上传到服务器</th></tr></thead><tbody><tr><td><strong>Identity Key (IK)</strong></td><td>Curve25519</td><td>长期（注册时生成）</td><td>仅公钥</td></tr><tr><td><strong>Signed Pre Key (SPK)</strong></td><td>Curve25519</td><td>中期（每周&#x2F;月轮换）</td><td>公钥 + IK 的签名</td></tr><tr><td><strong>One-Time Pre Key (OPK)</strong></td><td>Curve25519</td><td>一次性（用后即删）</td><td>公钥队列</td></tr></tbody></table><p><strong>X3DH 协议流程</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">阶段1: Bob 发布密钥到服务器</span><br><span class="line">  IK_B (公钥) + SPK_B (公钥 + Sig(IK_B, SPK_B)) + OPK_B1, OPK_B2, ... (公钥队列)</span><br><span class="line">    ↓ 服务器存储，等待 Alice 获取</span><br><span class="line"></span><br><span class="line">阶段2: Alice 发起通信</span><br><span class="line">  Alice ← 从服务器获取 Bob 的 Prekey Bundle</span><br><span class="line">  Alice → 验证 Sig(IK_B, SPK_B) → 若验证失败，中止</span><br><span class="line">  Alice → 生成临时密钥 EK_A (Curve25519)</span><br><span class="line">  Alice → 计算 DH 排列组合：</span><br><span class="line">    DH1 = DH(IK_A, SPK_B)    ← 互认证</span><br><span class="line">    DH2 = DH(EK_A, IK_B)     ← 互认证</span><br><span class="line">    DH3 = DH(EK_A, SPK_B)    ← 前向安全</span><br><span class="line">    [DH4 = DH(EK_A, OPK_B)]  ← 前向安全（强化）</span><br><span class="line">    ↓</span><br><span class="line">    SK = KDF(DH1 || DH2 || DH3 [|| DH4])</span><br><span class="line">         = HKDF(F || DH_concat, salt=零填充, info=应用标识)</span><br><span class="line">    ↓</span><br><span class="line">    AD = Encode(IK_A) || Encode(IK_B)</span><br><span class="line">    ↓</span><br><span class="line">    Alice → 发送初始消息: IK_A + EK_A + 预密钥标识 + AEAD(SK, AD) 密文</span><br><span class="line"></span><br><span class="line">阶段3: Bob 接收并处理</span><br><span class="line">  Bob → 提取 IK_A, EK_A</span><br><span class="line">  Bob → 加载私钥, 重复 DH 计算 → SK</span><br><span class="line">  Bob → 删除所有 DH 值</span><br><span class="line">  Bob → 验证 AEAD 解密 → 若失败，删除 SK 并中止</span><br><span class="line">  Bob → 若成功，删除 OPK_B私钥（前向安全）</span><br></pre></td></tr></table></figure><p><strong>X3DH 的安全属性</strong>：</p><table><thead><tr><th>DH 计算</th><th>功能</th><th>说明</th></tr></thead><tbody><tr><td>DH(IK_A, SPK_B)</td><td>互认证</td><td>证明 Alice 拥有 IK_A 且 Bob 拥有 SPK_B</td></tr><tr><td>DH(EK_A, IK_B)</td><td>互认证</td><td>证明 Alice 拥有 EK_A 且 Bob 拥有 IK_B</td></tr><tr><td>DH(EK_A, SPK_B)</td><td>前向安全</td><td>即使 IK 泄露，EK 提供独立安全</td></tr><tr><td>DH(EK_A, OPK_B)</td><td>强化前向安全</td><td>OPK 用后即删，额外保护</td></tr></tbody></table><p><strong>可否认性</strong>：X3DH <strong>不提供可发布的密码学证明</strong>——第三方无法证明 Alice&#x2F;Bob 确实参与了通信。</p><h3 id="Double-Ratchet-消息加密"><a href="#Double-Ratchet-消息加密" class="headerlink" title="Double Ratchet 消息加密"></a>Double Ratchet 消息加密</h3><p><strong>两种棘轮</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">DH Ratchet（不对称棘轮）：</span><br><span class="line">  每次收到对方的新 ratchet public key → 触发 DH ratchet step</span><br><span class="line">  → 生成新 ratchet key pair → 新 DH output → 更新 root key + chain keys</span><br><span class="line">  → &quot;ping-pong&quot;：双方轮流替换密钥</span><br><span class="line"></span><br><span class="line">Symmetric-key Ratchet（对称棘轮 / KDF Chain）：</span><br><span class="line">  每条消息 → KDF_CK(chain_key) → 新 chain_key + message_key</span><br><span class="line">  → message_key 用于加密/解密该条消息</span><br><span class="line">  → chain_key 被新值替代，旧值可删除</span><br><span class="line">  → 每条消息用唯一密钥 → 消息级别的前向安全</span><br></pre></td></tr></table></figure><p><strong>三条 KDF Chain</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Root Chain (RK)</span><br><span class="line">    │</span><br><span class="line">    │── DH output ──→ KDF_RK(RK, DH_output) ──→ 新 RK + CKr (receiving chain key)</span><br><span class="line">    │── DH output ──→ KDF_RK(RK, DH_output) ──→ 新 RK + CKs (sending chain key)</span><br><span class="line">    │</span><br><span class="line">Sending Chain (CKs)                    Receiving Chain (CKr)</span><br><span class="line">    │                                      │</span><br><span class="line">    │── KDF_CK(CKs) ──→ 新 CKs + mk        │── KDF_CK(CKr) ──→ 新 CKr + mk</span><br><span class="line">    mk = message_key                       mk = message_key</span><br><span class="line">    ↓                                      ↓</span><br><span class="line">    AES-256-CBC + HMAC-SHA-256             AES-256-CBC + HMAC-SHA-256</span><br></pre></td></tr></table></figure><p><strong>消息加密流程</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">plaintext</span><br><span class="line">    ↓</span><br><span class="line">RatchetEncrypt:</span><br><span class="line">    Ns, mk = RatchetSendKey(state)</span><br><span class="line">    │   state.CKs, mk = KDF_CK(state.CKs)</span><br><span class="line">    │   state.Ns += 1</span><br><span class="line">    ↓</span><br><span class="line">    header = HEADER(state.DHs, state.PN, Ns)</span><br><span class="line">    ↓</span><br><span class="line">    ENCRYPT(mk, plaintext, CONCAT(AD, header))</span><br><span class="line">    │   HKDF(SHA-256, mk) → 80字节</span><br><span class="line">    │   → 32字节 enc_key + 32字节 auth_key + 16字节 IV</span><br><span class="line">    │   AES-256-CBC + PKCS#7 加密 plaintext</span><br><span class="line">    │   HMAC-SHA-256(auth_key, AD || ciphertext)</span><br><span class="line">    ↓</span><br><span class="line">    输出: header + ciphertext + HMAC</span><br></pre></td></tr></table></figure><p><strong>前向安全保证</strong>：</p><ul><li><strong>消息级别</strong>——每条消息用唯一 message_key，使用后删除</li><li><strong>入侵恢复</strong>——DH ratchet 引入新熵，即使当前密钥泄露也能恢复</li><li><strong>chain key 替代</strong>——KDF_CK 后旧 chain key 可删除，无法回推历史</li></ul><p><strong>乱序消息处理</strong>：header 包含 PN（前 chain 长度）和 N（当前消息序号），接收方据此计算跳过的 message keys，暂存以便后续解密。MAX_SKIP 常量限制最大跳过数量。跳过的 message keys 使用后立即删除。</p><h3 id="群聊加密（Sender-Keys）"><a href="#群聊加密（Sender-Keys）" class="headerlink" title="群聊加密（Sender Keys）"></a>群聊加密（Sender Keys）</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">群成员A (发送方)                    服务器                  群成员B/C/D...</span><br><span class="line">    │── 生成 Sender Key ──────────→│                        │</span><br><span class="line">    │   (对称密钥 + chain key)      │                        │</span><br><span class="line">    │── 用 Signal 协议将 Sender Key │                        │</span><br><span class="line">    │   分发给每个群成员 ──────────→│── 转发密文信封 ───────→│── 用 Sender Key 解密</span><br><span class="line">    │── 用 Sender Key 加密消息 ───→│── 分发密文 ──────────→│</span><br><span class="line">    │── Sender Key 定期轮换 ──────→│── 分发新 Sender Key ──→│</span><br></pre></td></tr></table></figure><ul><li>每个发送者有自己的 Sender Key → 独立加密链</li><li>Sender Key 用 Signal 协议（X3DH + Double Ratchet）分发给每个群成员</li><li>服务器只转发密文，看不到明文</li><li>Sender Key 定期轮换 → 前向安全</li></ul><h3 id="多设备架构与密钥管理"><a href="#多设备架构与密钥管理" class="headerlink" title="多设备架构与密钥管理"></a>多设备架构与密钥管理</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">发送方设备1 ──E2E加密给──→ 接收方设备A ──独立密文信封──→</span><br><span class="line">发送方设备1 ──E2E加密给──→ 接收方设备B ──独立密文信封──→</span><br><span class="line">发送方设备1 ──E2E加密给──→ 接收方设备C ──独立密文信封──→</span><br><span class="line"></span><br><span class="line">每个接收方设备有独立的 Signal session:</span><br><span class="line">  - 独立的 identity key / signed pre key / one-time pre keys / Double Ratchet state</span><br><span class="line"></span><br><span class="line">JID 结构:</span><br><span class="line">  user@s.whatsapp.net     ← 主设备 (device 0)</span><br><span class="line">  user@s.whatsapp.net:1   ← 配对设备 1</span><br><span class="line">  user@s.whatsapp.net:2   ← 配对设备 2</span><br></pre></td></tr></table></figure><p><strong>配对设备同步</strong>：主设备打包最近聊天历史 → 用设备专属密钥加密为 blob → blob 密钥通过 E2E 消息传递到配对设备 → 配对设备解密后密钥立即删除 → 从此从本地数据库访问历史。</p><p><strong>元数据泄露问题</strong>（2024-2025 被揭露）：签名预密钥 ID 的分配方式泄露设备操作系统——Android 每月从 0 开始缓慢递增，iOS 模式截然不同。WhatsApp 2025 年已将 Android SPK ID 改为 24 位随机范围，但 OPK ID 仍能区分 iOS 和 Android。</p><h3 id="加密备份"><a href="#加密备份" class="headerlink" title="加密备份"></a>加密备份</h3><ul><li>2025 年 10 月引入 <strong>passkey-based E2E 备份</strong></li><li>算法：AES-256 + RSA-4096</li><li>用设备生物识别（指纹&#x2F;面部）保护 passkey</li><li><strong>opt-in，默认关闭</strong>——绝大多数用户仍未开启</li><li>不开启 → iCloud&#x2F;Google Drive 上的备份是明文</li></ul><hr><h2 id="核心对比"><a href="#核心对比" class="headerlink" title="核心对比"></a>核心对比</h2><h3 id="技术对比表"><a href="#技术对比表" class="headerlink" title="技术对比表"></a>技术对比表</h3><table><thead><tr><th>维度</th><th>Telegram Cloud Chat</th><th>Telegram Secret Chat</th><th>WhatsApp</th></tr></thead><tbody><tr><td><strong>加密类型</strong></td><td>Client-Server</td><td>端到端</td><td>端到端</td></tr><tr><td><strong>服务器能否看到内容</strong></td><td>✅ 能</td><td>❌ 不能</td><td>❌ 不能</td></tr><tr><td><strong>群聊 E2E</strong></td><td>❌ 不支持</td><td>❌ 不支持</td><td>✅ Sender Keys</td></tr><tr><td><strong>消息云端存储</strong></td><td>✅ 存储</td><td>❌ 不存储</td><td>❌ 不存储（但备份可能存）</td></tr><tr><td><strong>多设备历史同步</strong></td><td>✅ 支持</td><td>❌ 仅当前设备</td><td>✅ 加密同步</td></tr><tr><td><strong>元数据收集</strong></td><td>2024-09 起 1 年</td><td>仍收集连接元数据</td><td>大量收集 + 与 Meta 共享</td></tr><tr><td><strong>阅后即焚</strong></td><td>秘密聊天 1秒-1年</td><td>✅</td><td>24h&#x2F;7d&#x2F;90d</td></tr><tr><td><strong>截图防护</strong></td><td>Android 强制禁止</td><td>✅</td><td>❌ 仅通知</td></tr><tr><td><strong>政府配合内容</strong></td><td>普通聊天可提供</td><td>技术上无法</td><td>技术上无法</td></tr><tr><td><strong>政府配合元数据</strong></td><td>2024-09 起配合</td><td>仍配合连接日志</td><td>配合 + 与 Meta 共享</td></tr><tr><td><strong>密钥交换</strong></td><td>DH (2048-bit) + RSA</td><td>DH (2048-bit) 两端之间</td><td>X3DH (Curve25519，3-4次DH)</td></tr><tr><td><strong>消息加密</strong></td><td>AES-256-IGE</td><td>AES-256-IGE</td><td>AES-256-CBC + HMAC-SHA-256</td></tr><tr><td><strong>密钥派生</strong></td><td>SHA-256 拼接</td><td>SHA-256 拼接</td><td>HKDF(SHA-256)</td></tr><tr><td><strong>前向安全</strong></td><td>❌ 无</td><td>⚠️ 中等（100条&#x2F;1周）</td><td>✅ 强（每消息+DH ratchet）</td></tr><tr><td><strong>可否认性</strong></td><td>无</td><td>无</td><td>✅ X3DH密码学可否认性</td></tr><tr><td><strong>协议类型</strong></td><td>自定义 (MTProto)</td><td>自定义 (MTProto E2E层)</td><td>标准 (Signal Protocol)</td></tr><tr><td><strong>开源状态</strong></td><td>客户端+规范全公开</td><td>同上</td><td>客户端闭源，协议开源</td></tr><tr><td><strong>后量子准备</strong></td><td>无</td><td>无</td><td>Triple Ratchet (SPQR + ML-KEM)</td></tr></tbody></table><h3 id="通讯方式对比表"><a href="#通讯方式对比表" class="headerlink" title="通讯方式对比表"></a>通讯方式对比表</h3><table><thead><tr><th>维度</th><th>Telegram</th><th>WhatsApp</th></tr></thead><tbody><tr><td><strong>架构模型</strong></td><td>中心化消息路由器</td><td>去中心化密文信封分发器</td></tr><tr><td><strong>服务器角色</strong></td><td>解密→重加密→转发+存储+搜索+转码</td><td>仅转发密文信封+暂存离线消息</td></tr><tr><td><strong>消息存储</strong></td><td>云端永久存储（普通聊天）</td><td>本地存储+服务器暂存密文</td></tr><tr><td><strong>历史同步</strong></td><td>服务器推送→所有设备自动获取</td><td>主设备加密打包→配对设备本地解密</td></tr><tr><td><strong>离线消息</strong></td><td>服务器暂存明文→上线后推送</td><td>服务器暂存密文信封→上线后推送</td></tr><tr><td><strong>搜索</strong></td><td>服务器端搜索（明文索引）</td><td>仅本地搜索</td></tr><tr><td><strong>媒体处理</strong></td><td>服务器转码&#x2F;压缩→适配设备</td><td>原始文件 E2E 加密传输</td></tr><tr><td><strong>推送通知</strong></td><td>服务器生成通知内容</td><td>仅”你有一条新消息”</td></tr><tr><td><strong>删除机制</strong></td><td>双端删除→服务器也删除</td><td>本地删除→密文信封过期</td></tr><tr><td><strong>序列化</strong></td><td>TL 二进制（比 JSON 省 30-50% 流量）</td><td>Protobuf</td></tr><tr><td><strong>传输选择</strong></td><td>TCP &#x2F; HTTP &#x2F; WS &#x2F; UDP</td><td>WebSocket (Noise 协议框架)</td></tr></tbody></table><h3 id="阅后即焚实现对比"><a href="#阅后即焚实现对比" class="headerlink" title="阅后即焚实现对比"></a>阅后即焚实现对比</h3><p><strong>Telegram 秘密聊天的自毁计时器</strong>：</p><ul><li>可设范围：1 秒 ~ 1 年</li><li>计时器从<strong>对方阅读后</strong>开始计算</li><li>消息在<strong>双方设备</strong>上删除，服务器不留副本（因为 E2E）</li><li>Android 端<strong>禁止截图</strong>，iOS 端<strong>只能通知不能阻止</strong></li><li>⚠️ 不能阻止对方用另一部手机拍照&#x2F;录屏；阅读前的内容已经在对方设备上</li></ul><p><strong>WhatsApp 的 disappearing messages</strong>：</p><ul><li>可设：24 小时 &#x2F; 7 天 &#x2F; 90 天</li><li>计时器从<strong>发送时</strong>开始计算（不是阅读后）</li><li>⚠️ 不阻止截图（只通知”已截图”）；不阻止转发（转发不受自毁约束）；不阻止引用回复；云端备份中的副本不受影响</li><li>一句话：WhatsApp 的 disappearing messages 更像是”自动过期清理”</li></ul><table><thead><tr><th>维度</th><th>Telegram 秘密聊天</th><th>WhatsApp</th></tr></thead><tbody><tr><td>触发方式</td><td>阅读后计时</td><td>发送后计时</td></tr><tr><td>服务器副本</td><td>无（E2E）</td><td>无（但备份可能有）</td></tr><tr><td>截图防护</td><td>Android 强制禁止</td><td>仅通知</td></tr><tr><td>转发后是否受约束</td><td>不支持转发</td><td>不受约束</td></tr><tr><td>“焚”程度</td><td>⭐⭐⭐⭐</td><td>⭐⭐</td></tr></tbody></table><h3 id="加密软件安全声明解读"><a href="#加密软件安全声明解读" class="headerlink" title="加密软件安全声明解读"></a>加密软件安全声明解读</h3><table><thead><tr><th>声明</th><th>解读要点</th></tr></thead><tbody><tr><td>“我们用端到端加密”</td><td>要看是<strong>默认</strong>还是<strong>可选</strong>——Telegram 是可选，WhatsApp 是默认</td></tr><tr><td>“我们不留存任何数据”</td><td>要区分<strong>内容</strong>和<strong>元数据</strong>——WhatsApp 不留内容但留大量元数据；Telegram 秘密聊天不留内容，但 2024-09 起留元数据 1 年</td></tr><tr><td>“我们不与任何政府合作”</td><td>要看<strong>内容</strong>还是<strong>元数据</strong>——两者都可能配合元数据；内容配合取决于技术能力而非意愿</td></tr><tr><td>“阅后即焚”</td><td>要看是<strong>阅读后计时</strong>还是<strong>发送后计时</strong>——Telegram 秘密聊天是前者，WhatsApp 是后者</td></tr><tr><td>“无法被解密”</td><td>要看<strong>对谁</strong>——对服务器确实无法（E2E），对解锁后的端点完全可读</td></tr><tr><td>“零知识架构”</td><td>要看<strong>零知识覆盖什么</strong>——Signal 协议对消息内容是零知识，对元数据不是</td></tr></tbody></table><h3 id="Signal-定位对比"><a href="#Signal-定位对比" class="headerlink" title="Signal 定位对比"></a>Signal 定位对比</h3><p><strong>Signal 是当前在”不配合政府 + 最小元数据收集”维度上最接近理想描述的软件</strong>：</p><table><thead><tr><th>维度</th><th>Signal</th><th>Telegram</th><th>WhatsApp</th></tr></thead><tbody><tr><td><strong>默认 E2E</strong></td><td>✅</td><td>❌（仅秘密聊天）</td><td>✅</td></tr><tr><td><strong>群聊 E2E</strong></td><td>✅</td><td>❌</td><td>✅</td></tr><tr><td><strong>元数据收集</strong></td><td><strong>极小</strong>（仅注册时间、最后连接时间）</td><td>中等（2024-09 起保留 1 年）</td><td><strong>大量</strong> + 与 Meta 共享</td></tr><tr><td><strong>开源程度</strong></td><td>客户端 + 服务器全开源</td><td>客户端开源，服务器闭源</td><td>客户端闭源</td></tr><tr><td><strong>组织形态</strong></td><td>非营利（501c3）</td><td>商业公司</td><td>Meta 旗下商业公司</td></tr><tr><td><strong>政府配合</strong></td><td>仅配合注册时间+最后连接时间</td><td>2024-09 起配合元数据</td><td>配合元数据 + 与 Meta 共享</td></tr><tr><td><strong>所在司法管辖</strong></td><td>美国（但数据最小化）</td><td>迪拜</td><td>美国</td></tr></tbody></table><p>Signal 的”零知识”程度最高——即使收到美国政府的传票，能提供的也只有”这个号码在 X 时间注册过，最后连接时间是 Y”——没有任何社交图谱、消息内容、IP 历史。</p><p>但 Signal 也不是万能的：在中国使用同样需要翻墙——法律前置风险相同；解锁手机后消息同样是明文——端点风险相同；屏幕翻拍同样防不住——显示风险相同。</p><hr><h2 id="E2E-加密的定位与边界"><a href="#E2E-加密的定位与边界" class="headerlink" title="E2E 加密的定位与边界"></a>E2E 加密的定位与边界</h2><h3 id="四条暗道对-E2E-加密的影响"><a href="#四条暗道对-E2E-加密的影响" class="headerlink" title="四条暗道对 E2E 加密的影响"></a>四条暗道对 E2E 加密的影响</h3><p>回到第三章的四条泄密暗道，对 Telegram&#x2F;WhatsApp 这类 E2E 加密软件来说：</p><table><thead><tr><th>暗道</th><th>对 E2E 加密的效果</th><th>说明</th></tr></thead><tbody><tr><td><strong>暗道1：半路截胡</strong></td><td>✅ <strong>被防住</strong></td><td>E2E 加密让中间人即使截获密文也无法解密</td></tr><tr><td><strong>暗道2：服务器留底</strong></td><td>⚠️ <strong>部分防住</strong></td><td>Telegram 秘密聊天和 WhatsApp 消息内容服务器看不到；但元数据仍留底；WhatsApp 备份默认未加密是漏洞</td></tr><tr><td><strong>暗道3：屏幕翻拍</strong></td><td>❌ <strong>完全防不住</strong></td><td>E2E 加密保护的是传输，不是显示</td></tr><tr><td><strong>暗道4：境外服务器脱管</strong></td><td>❌ <strong>本身就是这个问题</strong></td><td>Telegram（迪拜&#x2F;俄罗斯背景）、WhatsApp（美国 Meta）本身就是境外服务器</td></tr></tbody></table><p><strong>核心洞见</strong>：E2E 加密只防住了 4 条暗道中的 1.5 条。最大的暗道（屏幕翻拍）和最敏感的暗道（境外服务器）反而不在 E2E 的防御范围内。</p><h3 id="“帽子叔叔”场景下的实际效果"><a href="#“帽子叔叔”场景下的实际效果" class="headerlink" title="“帽子叔叔”场景下的实际效果"></a>“帽子叔叔”场景下的实际效果</h3><p>假设嫌疑人手机里装了 Telegram（秘密聊天）和 WhatsApp：</p><p><strong>E2E 加密能挡住的</strong>：</p><ol><li>✅ 侦查机关<strong>向 Telegram&#x2F;WhatsApp 公司调取消息内容</strong>——技术上无法提供</li><li>✅ 侦查机关<strong>通过传输链路监听</strong>——E2E 加密挡住</li><li>✅ 侦查机关<strong>入侵服务器</strong>——消息内容没有明文副本</li></ol><p><strong>E2E 加密挡不住的</strong>：</p><ol><li>❌ <strong>元数据调取</strong>——Telegram 2024-09 起配合提供 IP&#x2F;设备&#x2F;用户名变更；WhatsApp 一直配合元数据调取——足以证明”你何时何地与谁聊过天”</li><li>❌ <strong>解锁手机后直接看</strong>——E2E 加密保护的是传输，不是端点，手机被解锁后所有消息都是明文</li><li>❌ <strong>WhatsApp 云端备份</strong>（如果未开启 E2E 备份）——可直接向 Apple&#x2F;Google 调取</li><li>❌ <strong>屏幕翻拍</strong>——侦查人员可以拍照取证</li><li>❌ <strong>使用行为本身</strong>——在中国使用 Telegram&#x2F;WhatsApp 需要翻墙，翻墙行为本身可能构成违法</li><li>❌ <strong>关联追溯</strong>——通过元数据可以追溯到联系人，联系人可能被单独讯问</li></ol><p><strong>技术对抗效果对比</strong>：</p><table><thead><tr><th>对抗手段</th><th>Telegram Secret Chat</th><th>WhatsApp</th><th>说明</th></tr></thead><tbody><tr><td><strong>远程调取服务器内容</strong></td><td>✅ 技术上无法</td><td>✅ 技术上无法</td><td>X3DH + Double Ratchet 更难破解</td></tr><tr><td><strong>服务器入侵后读取</strong></td><td>✅ 无法</td><td>✅ 无法</td><td>Signal 的标准组件更安全</td></tr><tr><td><strong>元数据调取</strong></td><td>❌ 2024-09 起配合</td><td>❌ 配合+与Meta共享</td><td>两者都无法抵抗</td></tr><tr><td><strong>解锁手机后查看</strong></td><td>❌ 完全暴露</td><td>❌ 完全暴露</td><td>E2E 保护传输，不保护显示</td></tr><tr><td><strong>云端备份</strong></td><td>✅ Secret Chat 无</td><td>❌ 默认备份未加密</td><td>WhatsApp 最大漏洞</td></tr><tr><td><strong>内存取证</strong></td><td>⚠️ key可提取</td><td>⚠️ Ratchet state可提取</td><td>两者都无法抵抗物理取证</td></tr><tr><td><strong>中间人攻击</strong></td><td>⚠️ 需忽略emoji验证</td><td>⚠️ 需忽略安全码验证</td><td>都有验证机制但多数人不验</td></tr></tbody></table><h3 id="E2E-加密在防御层次中的定位"><a href="#E2E-加密在防御层次中的定位" class="headerlink" title="E2E 加密在防御层次中的定位"></a>E2E 加密在防御层次中的定位</h3><blockquote><p><strong>E2E 加密解决的是”传输安全”和”服务器端安全”这两个特定问题，不解决”端点安全”、”元数据隐私”、”使用合法性”这三个更根本的问题。</strong></p></blockquote><table><thead><tr><th>层级</th><th>E2E 加密能贡献什么</th></tr></thead><tbody><tr><td>第一层 · 程序权利层</td><td>无关——E2E 是技术，不是权利</td></tr><tr><td>第二层 · 技术防御层</td><td><strong>贡献”服务器端防御”这一项</strong>——其他几项（BFU、USB 切断、多 profile）仍然需要</td></tr><tr><td>第三层 · 物理行为层</td><td><strong>间接相关</strong>——E2E 加密让你”敢把敏感数据放在云端”（理论上），但端点风险仍在</td></tr><tr><td>第四层 · 心理防线层</td><td>无关——但理解 E2E 的覆盖范围和边界本身就是心理防线的一部分</td></tr></tbody></table><p>对应到讯问笔录场景：</p><ul><li>用 Telegram 秘密聊天 &#x2F; WhatsApp → 调取服务器内容<strong>没用</strong>（E2E 挡住了）</li><li>但 → 调取元数据<strong>有用</strong>（能证明你何时何地与谁聊过）</li><li>但 → 解锁你手机直接看<strong>更有用</strong>（端点防御才是关键）</li><li>但 → 你<strong>使用这两款软件本身</strong>可能已经违法（翻墙前置问题）</li></ul><hr><h2 id="底层矛盾与最终定位"><a href="#底层矛盾与最终定位" class="headerlink" title="底层矛盾与最终定位"></a>底层矛盾与最终定位</h2><p>回到讯问笔录的核心矛盾——</p><p><strong>嫌疑人想保护的是</strong>：数字生活、财产、隐私、人格尊严<br><strong>侦查机关想要的是</strong>：证据、案件真相、社会秩序</p><p>这两者在法律框架内的平衡点，就是手机搜查的<strong>程序规制</strong>。但当前现实是：</p><ol><li><strong>法律规制缺位</strong> → 侦查权过大</li><li><strong>技术防御有效但有边界</strong> → 物理胁迫、合法搜查、云端数据都是漏洞</li><li><strong>“思想教育”作为程序外压力</strong> → 没有定义、没有录音、没有救济</li></ol><p><strong>单纯依靠技术对抗是”治标不治本”的</strong>：</p><ul><li>随机键盘 → 解决不了”思想教育”</li><li>胁迫密码 → 可能引入新的法律风险</li><li>自动重启 → 解决不了”已解锁状态下的内存取证”</li><li>E2E 加密 → 解决不了端点被攻破、元数据分析、使用合法性</li><li>网络权限开关 → 解决不了云端数据早已同步</li></ul><p><strong>真正能改变局面的，只有程序规制的完善</strong>：</p><blockquote><p>立法层面：增设电子数据搜查专门条款<br>程序层面：双重审查 + 搜查令具体化<br>监督层面：全程录像 + 非法证据排除 + 无关数据删除封存</p></blockquote><p>但在程序规制完善之前，<strong>作为个人能做的</strong>：</p><ol><li><strong>物理上</strong>：Pixel + GrapheneOS + 定期重启 + 锁屏 USB 切断 + 多 profile</li><li><strong>操作上</strong>：敏感数据<strong>不放在手机本地</strong>——用纸质笔记、加密 U 盘、专用离线设备</li><li><strong>通讯上</strong>：理解 E2E 加密只防住传输和服务器端，不防住端点、元数据、使用合法性</li><li><strong>法律上</strong>：清楚知道”我有权利要求出示搜查令””我有权要求全程录音录像””我有权聘请律师在场”</li><li><strong>心态上</strong>：理解”思想教育十分钟”在当前法律下没有明确救济渠道——最务实的做法是”<strong>事前不留</strong>“</li></ol><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><table><thead><tr><th>维度</th><th>关键点</th></tr></thead><tbody><tr><td><strong>场景</strong></td><td>讯问笔录展示了”思想教育十分钟”作为程序外压力的具体形态</td></tr><tr><td><strong>法律</strong></td><td>我国对手机搜查的规制存在立法滞后、监督空白、救济缺位</td></tr><tr><td><strong>设备防御</strong></td><td>胁迫密码保护”非法胁迫”；随机键盘防偷窥但对当面解锁无效；BFU + 自动重启是最强防御；多 profile 隔离是最务实的工程化方案</td></tr><tr><td><strong>通讯防御</strong></td><td>E2E 加密只防住传输层和服务器端；Telegram 默认不是 E2E；WhatsApp 消息 E2E 但备份默认未加密；两者元数据均配合政府；Signal 是元数据最小化的选择</td></tr><tr><td><strong>思维框架</strong></td><td>“本地销毁 ≠ 真实销毁”——四条暗道（截胡、留底、翻拍、脱管）贯穿所有技术层</td></tr><tr><td><strong>底层矛盾</strong></td><td>技术对抗有边界，程序规制完善才是根本解决之道</td></tr><tr><td><strong>务实建议</strong></td><td>事前不留 + 多 profile 隔离 + E2E 加密 + 程序性权利援引（”我忘了”+”我要律师”+”请出示证件”）</td></tr><tr><td><strong>诚实回答</strong></td><td>所有技术方案都能提高攻击成本，但不能对抗资源不对等的国家级对手；这是任何终端设备和任何 E2E 加密的必然边界，不是失败</td></tr></tbody></table><hr><h1 id="下篇：攻击者视角"><a href="#下篇：攻击者视角" class="headerlink" title="下篇：攻击者视角"></a>下篇：攻击者视角</h1><p><strong>本地删除 ≠ 数据消灭</strong>。<strong>加密传输 ≠ 信息保密</strong>。<strong>拒绝配合 ≠ 有效防御</strong>。</p><p>每个看起来安全的操作，都有一条 exfiltration 通道绕过它。</p><hr><h2 id="Kill-Chain-复盘——一次教科书级的社会工程学入侵"><a href="#Kill-Chain-复盘——一次教科书级的社会工程学入侵" class="headerlink" title="Kill Chain 复盘——一次教科书级的社会工程学入侵"></a>Kill Chain 复盘——一次教科书级的社会工程学入侵</h2><p>截图记录的不是”嫌疑人被讯问”——站在攻击者视角，这是一次<strong>社会工程学入侵的完整 kill chain</strong>：</p><table><thead><tr><th>Phase</th><th>攻击者行为</th><th>目标响应</th></tr></thead><tbody><tr><td><strong>Recon</strong></td><td>确认目标持有手机 &#x3D; 数字生活的完整副本</td><td>—</td></tr><tr><td><strong>Social Engineering</strong></td><td>三轮对话逐步测试心理防线类型</td><td>隐私权 → 财产权 → 程序性拒绝 → 全部被突破</td></tr><tr><td><strong>Exploit</strong></td><td>“思想教育十分钟”——无记录、无监督、无救济的权力真空</td><td>在无法拒绝的处境下被迫同意</td></tr><tr><td><strong>Access</strong></td><td>目标交出密码 → 设备解锁 → 不需要任何取证工具</td><td>“（思想教育十分钟以后）可以”</td></tr><tr><td><strong>Exfiltration</strong></td><td>解锁后的手机 &#x3D; 主动打开的门，全盘数据一览无余</td><td>所有聊天记录、照片、银行 App、位置历史暴露</td></tr></tbody></table><p><strong>攻击者的核心洞察</strong>：技术取证工具（Cellebrite、GrayKey）是<strong>备选方案</strong>——当社会工程学失效时才需要。最好的攻击永远不是技术攻击，而是<strong>让人自愿交出钥匙</strong>。</p><p><strong>攻击 playbook 中的关键细节</strong>：</p><table><thead><tr><th>细节</th><th>攻击者解读</th></tr></thead><tbody><tr><td>“个人隐私”被红色标注</td><td>攻击者把隐私权定性为<strong>可被突破的软性防线</strong>——不像财产权有法律硬壳</td></tr><tr><td>“信不过你们”</td><td>目标最强的防御——<strong>程序性拒绝不需要理由</strong>。但实践中被解读为”态度问题”</td></tr><tr><td>“思想教育十分钟”无记录</td><td>exploit 的 payload——可施加任何压力：威胁、暗示、许诺、情绪操控。没有 audit trail</td></tr><tr><td>“公安机关负责”</td><td>trust-building 话术——“我负责”没有法律约束力，事后追责几乎不可能</td></tr></tbody></table><p><strong>“思想教育十分钟”作为 exploit 的技术分析</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">传统 exploit: 发现软件漏洞 → 编写 payload → 获取执行权限 → 持久化 → exfiltration</span><br><span class="line">社会工程学 exploit: 发现人的漏洞 → 编写话术 → 获取配合 → 查看数据 → 拍照取证</span><br><span class="line"></span><br><span class="line">&quot;思想教育十分钟&quot; = payload delivery phase</span><br><span class="line">    - 无审计 trail（没有录音录像）</span><br><span class="line">    - 无防御检测（目标不知道自己有权利拒绝）</span><br><span class="line">    - 无 exploit 修补（法律没有禁止&quot;思想教育&quot;）</span><br><span class="line">    - 无 remediation（事后救济渠道空白）</span><br></pre></td></tr></table></figure><p>这是一个比任何 0-day exploit 都更稳定、更难修补的漏洞——载体是人，不是代码。人不会像软件一样被打补丁。</p><hr><h2 id="法律攻击向量与非技术-bypass"><a href="#法律攻击向量与非技术-bypass" class="headerlink" title="法律攻击向量与非技术 bypass"></a>法律攻击向量与非技术 bypass</h2><p>站在攻击者视角，我国手机搜查的法律规制不是”缺陷”——是<strong>可利用的攻击向量</strong>。而所有程序性权利的援引，在当前法律框架下都可以被 bypass。</p><h3 id="立法滞后-攻击面敞开"><a href="#立法滞后-攻击面敞开" class="headerlink" title="立法滞后 &#x3D; 攻击面敞开"></a>立法滞后 &#x3D; 攻击面敞开</h3><ul><li><strong>《宪法》40 条</strong>：设定通信权利边界但无细化规则 → 攻击者可在边界内自由操作</li><li><strong>《刑诉法》136-140 条</strong>：搜查对象限定”身体、物品、住处和其他有关的地方” → 无”数字生活空间”概念 → 手机数据搜查规制<strong>几乎空白</strong></li><li>2016&#x2F;2019 电子数据规定只规范技术操作，对隐私权保护<strong>着墨甚少</strong> → 攻击者只需遵守操作流程</li></ul><h3 id="无证搜查常态化-初始访问门槛极低"><a href="#无证搜查常态化-初始访问门槛极低" class="headerlink" title="无证搜查常态化 &#x3D; 初始访问门槛极低"></a>无证搜查常态化 &#x3D; 初始访问门槛极低</h3><ul><li>《刑诉法》138 条”紧急情况例外”被广泛适用 → 初始访问<strong>不需要搜查证</strong></li><li>内部批准即可启动 → <strong>无需司法审查</strong></li><li>搜查令无需描述数据范围 → <strong>“地毯式”检索合法化</strong></li></ul><h3 id="监督空白-持久化驻留"><a href="#监督空白-持久化驻留" class="headerlink" title="监督空白 &#x3D; 持久化驻留"></a>监督空白 &#x3D; 持久化驻留</h3><ul><li>只要求对拆封、提取过程录像 → 后续查看分析<strong>无监督</strong></li><li>非法证据排除在手机数据领域几乎没有适用 → 即使程序违法，<strong>数据仍然是证据</strong></li><li>隐私侵害的救济渠道几乎空白 → 攻击者不需要担心后果</li></ul><h3 id="拒绝权的模糊-社会工程学操作空间"><a href="#拒绝权的模糊-社会工程学操作空间" class="headerlink" title="拒绝权的模糊 &#x3D; 社会工程学操作空间"></a>拒绝权的模糊 &#x3D; 社会工程学操作空间</h3><ul><li>没有明确立法规定嫌疑人可以拒绝 → 目标不知道自己有权利</li><li>“不配合”被当作”态度问题” → 目标拒绝 &#x3D; 更重处罚 → 配合成了理性选择</li></ul><h3 id="程序性权利援引的-bypass"><a href="#程序性权利援引的-bypass" class="headerlink" title="程序性权利援引的 bypass"></a>程序性权利援引的 bypass</h3><p>攻击者面对的五种常见程序性防御，都有对应的 bypass：</p><table><thead><tr><th>目标的防御</th><th>攻击者的 bypass</th></tr></thead><tbody><tr><td><strong>“我忘了密码”</strong></td><td>不需要验证真假——只需持续压力直到目标”想起来”。思想教育就是这个过程</td></tr><tr><td><strong>“64位密码我背不下来”</strong></td><td>“没关系，可以从密码管理器里看。”密码管理器在手机上 → 解锁手机就能看到</td></tr><tr><td><strong>“律师来之前我不回答”</strong></td><td>讯问不需要律师在场。拒绝回答被记录为”态度问题”，继续施压</td></tr><tr><td><strong>“请出示搜查令”</strong></td><td>内部批准即可（2.2节）。”紧急情况”例外被普遍适用。搜查令不是必要条件</td></tr><tr><td><strong>“请同步录音录像”</strong></td><td>录音录像只覆盖正式讯问。”思想教育十分钟”不在录音录像范围内——这正是核心 exploit</td></tr></tbody></table><p><strong>结论</strong>：所有程序性权利在当前法律框架下都是<strong>软约束</strong>——法律没有强制要求侦查人员遵守，攻击者可以逐一 bypass。</p><h3 id="域外对比-1"><a href="#域外对比-1" class="headerlink" title="域外对比"></a>域外对比</h3><table><thead><tr><th>国家&#x2F;地区</th><th>攻击者条件</th><th>难度</th></tr></thead><tbody><tr><td><strong>美国</strong></td><td>必须持证搜查；赖利诉加州案确立手机需独立搜查令</td><td>⭐⭐⭐⭐ 高</td></tr><tr><td><strong>德国</strong></td><td>法官批准 + 搜查令需明确数据类型&#x2F;时间&#x2F;关键词</td><td>⭐⭐⭐⭐ 高</td></tr><tr><td><strong>中国</strong></td><td>内部批准即可；”紧急情况”例外普遍适用</td><td>⭐ 低</td></tr></tbody></table><h3 id="攻击者的综合评估"><a href="#攻击者的综合评估" class="headerlink" title="攻击者的综合评估"></a>攻击者的综合评估</h3><p>法律攻击向量远比技术攻击向量更容易利用。不需要购买 Cellebrite（~$100K+），不需要 0-day 研究员，只需<strong>利用法律漏洞 + 社会工程学</strong>。</p><hr><h2 id="Exfiltration-通道框架与-E2E-加密的边界"><a href="#Exfiltration-通道框架与-E2E-加密的边界" class="headerlink" title="Exfiltration 通道框架与 E2E 加密的边界"></a>Exfiltration 通道框架与 E2E 加密的边界</h2><p>4 条泄密暗道，站在攻击者视角是<strong>四条结构化的 exfiltration 通道</strong>。每条通道同时标注 E2E 加密是否阻断——这决定了攻击者需要选择哪条路径。</p><table><thead><tr><th>通道</th><th>攻击者视角</th><th>E2E 是否阻断</th><th>攻击者 bypass</th></tr></thead><tbody><tr><td><strong>通道1：截胡</strong></td><td>传输层中间人入口</td><td>✅ <strong>被阻断</strong></td><td>转向通道2&#x2F;3&#x2F;4</td></tr><tr><td><strong>通道2：留底</strong></td><td>服务器端数据持久驻留</td><td>⚠️ <strong>部分阻断</strong></td><td>内容被阻断但元数据仍留底；WhatsApp 备份默认未加密是结构性漏洞</td></tr><tr><td><strong>通道3：翻拍</strong></td><td>端点显示层——终极 bypass</td><td>❌ <strong>完全不阻断</strong></td><td>E2E 保护传输不保护显示——解锁后的手机 &#x3D; 明文</td></tr><tr><td><strong>通道4：脱管</strong></td><td>管辖权外数据调取</td><td>❌ <strong>本身就是通道</strong></td><td>境外服务器 + 翻墙违法 &#x3D; 法律攻击向量</td></tr></tbody></table><p><strong>核心结论</strong>：E2E 加密只阻断了 4 条通道中的 1.5 条。攻击者有<strong>多条高 ROI bypass 路径</strong>不需要破解 E2E。</p><hr><h2 id="GrapheneOS-攻击面——逐层拆解与攻击优先级"><a href="#GrapheneOS-攻击面——逐层拆解与攻击优先级" class="headerlink" title="GrapheneOS 攻击面——逐层拆解与攻击优先级"></a>GrapheneOS 攻击面——逐层拆解与攻击优先级</h2><p>GrapheneOS 是一个以隐私和安全为核心的安卓 ROM。站在攻击者视角，<strong>每一个”防御特性”都是一个需要绕过的障碍</strong>。</p><h3 id="胁迫密码——攻击者的双重利用"><a href="#胁迫密码——攻击者的双重利用" class="headerlink" title="胁迫密码——攻击者的双重利用"></a>胁迫密码——攻击者的双重利用</h3><p><strong>机制</strong>：用户预设特殊 PIN&#x2F;密码，输入后不可逆擦除所有数据（含 eSIM），后台静默进行，无法中断。覆盖所有 PIN&#x2F;密码输入点。唯一例外：SIM 卡 PIN。</p><p><strong>攻击者的两种利用方式</strong>：</p><table><thead><tr><th>利用方式</th><th>具体操作</th><th>效果</th></tr></thead><tbody><tr><td><strong>方式一：诱触发</strong></td><td>反复要求”再输一次密码确认” → 诱使目标输入胁迫密码</td><td>数据不可逆擦除——但攻击者通常<strong>不想让数据消失</strong></td></tr><tr><td><strong>方式二：法律武器化</strong></td><td>目标输入胁迫密码 → 数据擦除 → 推定”故意毁灭证据”</td><td><strong>妨碍公务罪 &#x2F; 毁灭证据罪</strong>——法律后果比”不交密码”更严重</td></tr></tbody></table><p><strong>攻击者的风险评估</strong>：</p><table><thead><tr><th>维度</th><th>攻击者视角</th></tr></thead><tbody><tr><td>不可逆</td><td>数据无法恢复 → 对取证是灾难（但法律推定可补偿）</td></tr><tr><td>擦除需要时间</td><td>后台擦除非瞬间 → 攻击者可能在擦完前看到部分内容 → <strong>部分取证仍然可能</strong></td></tr><tr><td>eSIM 一起擦</td><td>追踪难度增加（但可通过运营商记录回溯）</td></tr><tr><td>自身风险</td><td>目标手滑也会触发 → 误触概率取决于密码差异大小</td></tr></tbody></table><p><strong>结论</strong>：胁迫密码对攻击者<strong>不是障碍，是可利用的弱点</strong>。</p><h3 id="随机锁屏密码键盘——攻击者不需要绕过"><a href="#随机锁屏密码键盘——攻击者不需要绕过" class="headerlink" title="随机锁屏密码键盘——攻击者不需要绕过"></a>随机锁屏密码键盘——攻击者不需要绕过</h3><p><strong>机制</strong>：每次解锁时 0-9 数字位置随机变化。</p><p><strong>攻击者的核心洞察</strong>：随机键盘防御的是<strong>偷窥类攻击</strong>——目标不知情时获取密码。但执法场景中目标是<strong>主动配合</strong>的——攻击者只需要让目标口述密码数字本身。随机键盘在”思想教育后主动交出密码”场景下<strong>完全无用</strong>。</p><table><thead><tr><th>攻击方式</th><th>随机键盘有效性</th><th>攻击者实际操作</th></tr></thead><tbody><tr><td>偷窥&#x2F;油渍</td><td>⭐⭐⭐⭐⭐ 有效</td><td><strong>不需要偷窥——让目标口述</strong></td></tr><tr><td>物理胁迫当面解锁</td><td>❌ 无效</td><td><strong>本身就是标准操作</strong></td></tr></tbody></table><h3 id="BFU-USB-切断-多-Profile——逐层-bypass"><a href="#BFU-USB-切断-多-Profile——逐层-bypass" class="headerlink" title="BFU &#x2F; USB 切断 &#x2F; 多 Profile——逐层 bypass"></a>BFU &#x2F; USB 切断 &#x2F; 多 Profile——逐层 bypass</h3><table><thead><tr><th>防御</th><th>攻击者的 bypass</th></tr></thead><tbody><tr><td><strong>自动重启 + BFU</strong></td><td>思想教育后目标主动解锁 → 密钥回到内存 → AFU 状态 → 取证工具可工作。BFU 只在目标<strong>不配合</strong>时有效</td></tr><tr><td><strong>锁屏后禁用 USB-C</strong></td><td>目标解锁后 USB 端口恢复数据功能 → Cellebrite 直接连接。不需要绕过 USB 切断——只需让目标解锁</td></tr><tr><td><strong>多 Profile 隔离</strong></td><td>解锁 Owner profile 后所有辅助 profile 可访问。即使只解锁辅助 profile，Cellebrite AFU 状态仍可提取所有 profile CE 数据</td></tr><tr><td><strong>网络权限开关</strong></td><td>不需要通过网络获取数据——直接 USB 连接提取。只防远程 exfiltration，不防本地取证</td></tr><tr><td><strong>eSIM 自动擦除</strong></td><td>eSIM 擦除后仍可通过运营商获取 IMSI&#x2F;IMEI 历史记录</td></tr></tbody></table><h3 id="取证工具矩阵——攻击者的实际能力"><a href="#取证工具矩阵——攻击者的实际能力" class="headerlink" title="取证工具矩阵——攻击者的实际能力"></a>取证工具矩阵——攻击者的实际能力</h3><p>基于 Cellebrite&#x2F;GrayKey&#x2F;MSAB 2024-2025 泄露文件：</p><p><strong>Pixel + 原生 Android（攻击者首选目标）</strong>：</p><table><thead><tr><th>设备状态</th><th>Cellebrite Premium</th><th>GrayKey</th><th>MSAB XRY</th></tr></thead><tbody><tr><td>已解锁</td><td>✅ 全文件系统提取</td><td>✅ 全文件系统提取</td><td>✅ 全文件系统提取</td></tr><tr><td>AFU</td><td>✅ OS exploit 提取 CE</td><td>✅ fastboot exploit</td><td>✅ fastboot exploit</td></tr><tr><td>BFU</td><td>❌ 暴力破解不可能（Titan M2）</td><td>❌ 不可能</td><td>❌ 不可能</td></tr></tbody></table><p><strong>Pixel + GrapheneOS（攻击者的噩梦）</strong>：</p><table><thead><tr><th>设备状态</th><th>Cellebrite Premium（2025-01）</th><th>GrayKey</th><th>MSAB XRY</th></tr></thead><tbody><tr><td>已解锁</td><td>⚠️ 即使已解锁也无法复制数据（Cellebrite 2024 Teams 泄露截图）</td><td>⚠️ 待确认</td><td>⚠️ 待确认</td></tr><tr><td>AFU</td><td>❌ 无已知 exploit</td><td>❌ 无已知 exploit</td><td>❌ 无已知 exploit</td></tr><tr><td>BFU</td><td>❌ 完全无法提取</td><td>❌ 完全无法提取</td><td>❌ 完全无法提取</td></tr></tbody></table><p>截至 2025-01，三大取证工具对 GrapheneOS 均无已知 exploit。Cellebrite 泄露截图甚至显示<strong>已解锁的 GrapheneOS Pixel 也无法数据复制</strong>——即使攻击者通过社会工程学让目标解锁了手机，自动化提取工具仍然可能失败。</p><p><strong>但攻击者仍有路径</strong>：1) 手动查看（不需要 Cellebrite）；2) 拍照取证；3) 物理胁迫持续要求目标当场展示特定 App；4) 未来可能的新 exploit；5) chip-off &#x2F; JTAG（成本极高）。</p><h3 id="攻击路径优先级排序"><a href="#攻击路径优先级排序" class="headerlink" title="攻击路径优先级排序"></a>攻击路径优先级排序</h3><p>面对一个 Pixel + GrapheneOS 目标，攻击者的 ROI 排序：</p><table><thead><tr><th>优先级</th><th>攻击路径</th><th>ROI</th><th>条件</th><th>限制</th></tr></thead><tbody><tr><td><strong>1</strong></td><td>社会工程学 → 目标主动解锁</td><td>★★★★★</td><td>只需目标配合</td><td>无</td></tr><tr><td><strong>2</strong></td><td>AFU 状态取证 → Cellebrite&#x2F;GrayKey</td><td>★★★★</td><td>AFU 状态 + 原生 Android</td><td>对 GrapheneOS 当前无 exploit</td></tr><tr><td><strong>3</strong></td><td>BFU 暴力破解</td><td>★★</td><td>BFU 状态 + 短 PIN</td><td>8位+字母数字密码计算不可行</td></tr><tr><td><strong>4</strong></td><td>0-day 开发</td><td>★</td><td>高价值目标</td><td>$500K-$1M+；GrapheneOS 加固大幅增加难度</td></tr><tr><td><strong>5</strong></td><td>chip-off &#x2F; JTAG</td><td>☆</td><td>最后手段</td><td>FBE 加密即使物理提取也无法解密 CE 数据</td></tr></tbody></table><p><strong>终极结论</strong>：社会工程学的 ROI 远高于所有技术攻击。</p><h3 id="GrapheneOS-层面的技术取证解读"><a href="#GrapheneOS-层面的技术取证解读" class="headerlink" title="GrapheneOS 层面的技术取证解读"></a>GrapheneOS 层面的技术取证解读</h3><p>“GrapheneOS 系统可以在手机解开 Bootloader 锁之后，还可以在刷完系统之后再将系统的 Bootloader 锁回去 同时也可以设置接口，彻底关掉手机上type-c接口的所有功能包括充电功能都能关掉</p><blockquote><p>不知道这个系统能不能对抗帽子叔叔的信息采集系统</p></blockquote><table><thead><tr><th>—-</th><th>攻击含义</th><th>攻击者能否突破</th></tr></thead><tbody><tr><td><strong>Bootloader可以在解锁后重新锁回去</strong></td><td>Verified Boot → 无法刷入恶意 ROM、无法植入 bootloader 级持久化</td><td>✅ 无法刷入恶意系统；⚠️ 社会工程学绕过（让目标解锁后直接操作）</td></tr><tr><td><strong>type-c接口所有功能彻底关掉</strong></td><td>锁定后 USB-C 硬件级切断（含 DisplayPort 替代模式）</td><td>✅ 锁定后无法 USB 取证；</td></tr><tr><td><strong>能不能对抗帽子叔叔</strong></td><td>需要分层评估（见下表）</td><td>—</td></tr></tbody></table><table><thead><tr><th>层面</th><th>攻击者能否突破</th></tr></thead><tbody><tr><td>BFU 暴力破解</td><td>❌ Titan M2 + 全盘加密构成工业级障碍</td></tr><tr><td>AFU 专业取证</td><td>⚠️ 当前不能（无已知 exploit）；对原生 Android 可以</td></tr><tr><td>已解锁后全盘查看</td><td>✅ 手动查看 + 拍照取证</td></tr><tr><td>物理胁迫</td><td>✅ 社会工程学让目标主动解锁</td></tr><tr><td>云端数据</td><td>✅ 独立于设备，直接向 Google&#x2F;Apple 调取</td></tr><tr><td>跨设备追踪</td><td>✅ 运营商&#x2F;浏览器&#x2F;社交平台均可追踪</td></tr></tbody></table><p><strong>综合评估</strong>：GrapheneOS 对<strong>技术性取证</strong>提供极高阻力——Cellebrite&#x2F;GrayKey 当前无法突破。但对<strong>社会工程学攻击</strong>零防护——解锁后的手机仍然是明文。</p><hr><h2 id="通讯架构——攻击者的-interception-策略"><a href="#通讯架构——攻击者的-interception-策略" class="headerlink" title="通讯架构——攻击者的 interception 策略"></a>通讯架构——攻击者的 interception 策略</h2><h3 id="Telegram：中心化架构-攻击者的天堂"><a href="#Telegram：中心化架构-攻击者的天堂" class="headerlink" title="Telegram：中心化架构 &#x3D; 攻击者的天堂"></a>Telegram：中心化架构 &#x3D; 攻击者的天堂</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">普通聊天：</span><br><span class="line">发送方 ──加密──→ Telegram 服务器 ──解密──→ ──重新加密──→ 接收方</span><br><span class="line">                  │ 存储明文副本 │ 云端同步 │ 搜索索引 │ 推送通知 │ 媒体转码</span><br><span class="line"></span><br><span class="line">Secret Chat：</span><br><span class="line">发送方 ──E2E加密──→ Telegram 服务器 ──仅转发密文──→ 接收方（唯一绑定设备）</span><br><span class="line">                     │ 不存储明文 │ 不同步 │ 7天后删除密文副本</span><br></pre></td></tr></table></figure><p><strong>攻击者的 interception 路径</strong>：</p><ol><li><strong>服务器端直接获取</strong> → 普通聊天有明文副本 → <strong>司法调取</strong></li><li><strong>元数据分析</strong> → 通信频率&#x2F;时间&#x2F;联系人 → <strong>证明”何时何地与谁聊过”</strong></li><li><strong>配合政策转向</strong> → 2024-09 起保留 IP&#x2F;设备&#x2F;用户名变更 1 年 → 过去 6 个月提供约 10,000 账户数据</li><li><strong>Secret Chat bypass</strong> → 服务器看不到内容 → <strong>转向端点攻击</strong>：解锁手机 → 消息在设备上是明文</li></ol><h3 id="WhatsApp：去中心化架构-需要更多-bypass"><a href="#WhatsApp：去中心化架构-需要更多-bypass" class="headerlink" title="WhatsApp：去中心化架构 &#x3D; 需要更多 bypass"></a>WhatsApp：去中心化架构 &#x3D; 需要更多 bypass</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">发送方 ──E2E加密──→ WhatsApp 服务器 ──仅分发密文信封──→ 接收方设备1/2/3</span><br></pre></td></tr></table></figure><p><strong>攻击者的 interception 路径</strong>：</p><ol><li><strong>服务器端无法获取内容</strong> → 转向其他路径</li><li><strong>元数据调取</strong> → 大量收集 + 与 Meta 共享 → <strong>完整社交图谱</strong></li><li><strong>云端备份截获</strong> → iCloud&#x2F;Google Drive 默认未加密 → <strong>最高 ROI 路径</strong></li><li><strong>端点攻击</strong> → 解锁手机 → 所有消息明文</li></ol><p><strong>关键洞察</strong>：WhatsApp E2E 只保护传输层——攻击者从<strong>备份层</strong>或<strong>端点层</strong>获取。云端备份默认未加密是最大的<strong>结构性漏洞</strong>。</p><h3 id="攻击者的策略选择"><a href="#攻击者的策略选择" class="headerlink" title="攻击者的策略选择"></a>攻击者的策略选择</h3><table><thead><tr><th>目标使用的软件</th><th>攻击者优先路径</th></tr></thead><tbody><tr><td>Telegram 普通聊天</td><td><strong>服务器端路径</strong>（成本最低）</td></tr><tr><td>Telegram Secret Chat</td><td><strong>端点路径</strong>（解锁手机）</td></tr><tr><td>WhatsApp</td><td><strong>备份路径</strong>（向 Apple&#x2F;Google 调取）或端点路径</td></tr><tr><td>WhatsApp + E2E 备份开启</td><td><strong>只能走端点路径</strong></td></tr><tr><td>Signal</td><td><strong>只能走端点路径</strong> + 翻墙违法证据</td></tr></tbody></table><hr><h2 id="Telegram-加密——攻击者的-cryptanalysis"><a href="#Telegram-加密——攻击者的-cryptanalysis" class="headerlink" title="Telegram 加密——攻击者的 cryptanalysis"></a>Telegram 加密——攻击者的 cryptanalysis</h2><h3 id="普通聊天：攻击者不需要-cryptanalysis"><a href="#普通聊天：攻击者不需要-cryptanalysis" class="headerlink" title="普通聊天：攻击者不需要 cryptanalysis"></a>普通聊天：攻击者不需要 cryptanalysis</h3><p>默认是 client-server 加密 → 服务器有明文 → <strong>司法调取直接获取内容</strong>。</p><h3 id="MTProto-2-0-密码学弱点"><a href="#MTProto-2-0-密码学弱点" class="headerlink" title="MTProto 2.0 密码学弱点"></a>MTProto 2.0 密码学弱点</h3><table><thead><tr><th>弱点</th><th>利用可能性</th></tr></thead><tbody><tr><td>AES-IGE 非标准模式</td><td>实现可能出错 → 但 $500K 赏金无人领取 → 无已知攻击</td></tr><tr><td>密钥派生非 HKDF</td><td>可能隐藏 bug → 同样无已知利用</td></tr><tr><td>密钥指纹仍用 SHA-1</td><td>理论碰撞风险 → 但 msg_key 用 SHA-256</td></tr><tr><td>auth_key 长期不变</td><td>窃取可解密所有历史 → 需要端点攻击获取 auth_key</td></tr><tr><td>无标准安全证明</td><td>“没有证明”≠”有漏洞”</td></tr></tbody></table><p><strong>结论</strong>：密码学攻击 ROI 太低——转向端点攻击和社会工程学是更优策略。</p><h3 id="Secret-Chat-的攻击路径"><a href="#Secret-Chat-的攻击路径" class="headerlink" title="Secret Chat 的攻击路径"></a>Secret Chat 的攻击路径</h3><p><strong>密钥建立</strong>：两端 2048-bit DH → 256 字节共享密钥 → AES-256-IGE。前向安全：100 条&#x2F;1 周轮换。</p><table><thead><tr><th>攻击路径</th><th>可行性</th></tr></thead><tbody><tr><td>中间人攻击</td><td>⚠️ 需目标忽略 emoji 验证 → 实践中多数人不验证</td></tr><tr><td>端点攻击</td><td>✅ 解锁手机 → 消息明文</td></tr><tr><td>服务器端</td><td>❌ 只有密文，7 天后删除</td></tr><tr><td>元数据</td><td>✅ 证明”何时与谁建立了 Secret Chat”</td></tr></tbody></table><h3 id="政府配合——攻击者的法律杠杆"><a href="#政府配合——攻击者的法律杠杆" class="headerlink" title="政府配合——攻击者的法律杠杆"></a>政府配合——攻击者的法律杠杆</h3><table><thead><tr><th>时间</th><th>事件</th><th>攻击者新增能力</th></tr></thead><tbody><tr><td>2024-08</td><td>Durov 在法国被逮捕</td><td>Telegram 不再”不可触碰”</td></tr><tr><td>2024-09</td><td>更新隐私政策，保留元数据 1 年</td><td>IP&#x2F;设备&#x2F;用户名变更 → 可被司法调取</td></tr><tr><td>2024 下半年</td><td>向全球执法机构提供约 10,000 账户数据</td><td>常态化配合</td></tr><tr><td>2025 起</td><td>有效法院命令时提供 IP 和电话号码</td><td>直接获取身份信息</td></tr></tbody></table><hr><h2 id="WhatsApp-加密——攻击者的-interception-矩阵"><a href="#WhatsApp-加密——攻击者的-interception-矩阵" class="headerlink" title="WhatsApp 加密——攻击者的 interception 矩阵"></a>WhatsApp 加密——攻击者的 interception 矩阵</h2><h3 id="安全边界的攻击面"><a href="#安全边界的攻击面" class="headerlink" title="安全边界的攻击面"></a>安全边界的攻击面</h3><table><thead><tr><th>层面</th><th>攻击路径</th><th>可行性</th></tr></thead><tbody><tr><td>消息内容（E2E）</td><td>转向端点&#x2F;备份</td><td>服务器端 ❌</td></tr><tr><td>元数据</td><td>司法调取 + Meta 共享</td><td>✅ 直接获取</td></tr><tr><td>云端备份（默认未加密）</td><td>向 Apple&#x2F;Google 调取</td><td>✅ <strong>最高 ROI</strong></td></tr><tr><td>E2E 备份（opt-in 默认关闭）</td><td>绝大多数用户未开启</td><td>✅ 仍可行</td></tr><tr><td>Meta AI 对话（非 E2E）</td><td>Meta 直接提供</td><td>✅ 直接获取</td></tr><tr><td>商业账号对话</td><td>E2E 失效，从商家获取</td><td>✅ 可获取</td></tr></tbody></table><p><strong>最优路径</strong>：云端备份截获——成本最低、成功率最高、不需要端点访问。</p><h3 id="Signal-Protocol——cryptanalysis-评估"><a href="#Signal-Protocol——cryptanalysis-评估" class="headerlink" title="Signal Protocol——cryptanalysis 评估"></a>Signal Protocol——cryptanalysis 评估</h3><p><strong>X3DH</strong>：Curve25519，3-4 次 DH 排列组合 → HKDF → 共享密钥。有数学证明 + 广泛审计。</p><p><strong>Double Ratchet</strong>：DH ratchet + KDF chain → 每条消息新密钥 + 前向安全 + 入侵恢复。</p><p><strong>攻击者评估</strong>：直接 cryptanalysis 的 ROI <strong>接近零</strong>。</p><p><strong>攻击者的 bypass</strong>：1) 端点攻击（解锁手机）；2) 备份截获（默认未加密）；3) 元数据分析（通信关系本身是证据）；4) SS7&#x2F;IMSI catcher（定位+通信模式）；5) MITM（需目标忽略安全码验证）。</p><h3 id="群聊-多设备-网络层"><a href="#群聊-多设备-网络层" class="headerlink" title="群聊 + 多设备 + 网络层"></a>群聊 + 多设备 + 网络层</h3><table><thead><tr><th>机制</th><th>攻击者利用</th></tr></thead><tbody><tr><td><strong>群聊 Sender Keys</strong></td><td>解锁任何群成员手机 → 获取 Sender Key → 解密该发送者群消息</td></tr><tr><td><strong>多设备独立 session</strong></td><td>设备指纹泄露 OS；配对设备（旧 iPad）安全性更低 → 攻击更弱的端点</td></tr><tr><td><strong>SS7 &#x2F; IMSI catcher</strong></td><td>模拟基站 → IMSI&#x2F;IMEI&#x2F;位置；强制降级 2G → MITM；拦截 SMS → 截获验证码 → 重置密码</td></tr></tbody></table><hr><h2 id="综合对比——攻击者的-interception-全景"><a href="#综合对比——攻击者的-interception-全景" class="headerlink" title="综合对比——攻击者的 interception 全景"></a>综合对比——攻击者的 interception 全景</h2><h3 id="攻击路径-ROI-对比表"><a href="#攻击路径-ROI-对比表" class="headerlink" title="攻击路径 ROI 对比表"></a>攻击路径 ROI 对比表</h3><table><thead><tr><th>攻击路径</th><th>Telegram 普通聊天</th><th>Telegram Secret Chat</th><th>WhatsApp</th><th>Signal</th><th>ROI</th></tr></thead><tbody><tr><td>服务器端内容</td><td>✅ 直接获取</td><td>❌ 无明文</td><td>❌ 无明文</td><td>❌ 无明文</td><td>★★★★★（TG普通）</td></tr><tr><td>元数据调取</td><td>✅ 2024-09起</td><td>✅ 连接日志</td><td>✅ +Meta</td><td>⚠️ 仅注册+最后连接</td><td>★★★★</td></tr><tr><td>云端备份截获</td><td>N&#x2F;A</td><td>N&#x2F;A</td><td>✅ 默认未加密</td><td>❌ 无备份机制</td><td>★★★★★（WA）</td></tr><tr><td>端点攻击</td><td>✅ 明文</td><td>✅ 明文</td><td>✅ 明文</td><td>✅ 明文</td><td>★★★★★（通用）</td></tr><tr><td>屏幕翻拍</td><td>✅</td><td>✅</td><td>✅</td><td>✅</td><td>★★★★★（通用）</td></tr><tr><td>MITM</td><td>⚠️ 需忽略验证</td><td>⚠️ 需忽略验证</td><td>⚠️ 需忽略验证</td><td>⚠️ 需忽略验证</td><td>★★</td></tr><tr><td>cryptanalysis</td><td>❌ 无已知</td><td>❌ 无已知</td><td>❌ 无已知</td><td>❌ 无已知</td><td>★</td></tr><tr><td>SS7&#x2F;IMSI catcher</td><td>✅ 元数据+位置</td><td>✅ 元数据+位置</td><td>✅ 元数据+位置</td><td>✅ 元数据+位置</td><td>★★★</td></tr><tr><td>翻墙违法</td><td>✅ 法律向量</td><td>✅ 法律向量</td><td>✅ 法律向量</td><td>✅ 法律向量</td><td>★★★★</td></tr></tbody></table><h3 id="通讯架构对比"><a href="#通讯架构对比" class="headerlink" title="通讯架构对比"></a>通讯架构对比</h3><table><thead><tr><th>维度</th><th>Telegram</th><th>WhatsApp</th><th>Signal</th><th>攻击者偏好</th></tr></thead><tbody><tr><td>架构</td><td>中心化路由器</td><td>去中心化信封分发</td><td>去中心化+最小元数据</td><td>TG最易攻击</td></tr><tr><td>服务器角色</td><td>解密→重加密→转发+存储</td><td>仅转发密文</td><td>仅转发密文+最小日志</td><td>TG服务器是一级目标</td></tr><tr><td>消息存储</td><td>云端永久（普通聊天）</td><td>本地+密文暂存</td><td>本地+密文暂存</td><td>TG利于历史取证</td></tr><tr><td>推送通知</td><td>服务器生成内容</td><td>仅”一条新消息”</td><td>仅”一条新消息”</td><td>TG泄露消息预览</td></tr><tr><td>元数据</td><td>中等（2024-09起1年）</td><td>大量+Meta共享</td><td>极小</td><td>Signal最难</td></tr><tr><td>政府配合</td><td>2024-09起常态化</td><td>配合元数据+Meta</td><td>仅注册时间+最后连接</td><td>Signal最难</td></tr></tbody></table><h3 id="阅后即焚的-bypass"><a href="#阅后即焚的-bypass" class="headerlink" title="阅后即焚的 bypass"></a>阅后即焚的 bypass</h3><table><thead><tr><th>维度</th><th>Telegram Secret Chat</th><th>WhatsApp</th><th>攻击者 bypass</th></tr></thead><tbody><tr><td>触发方式</td><td>阅读后计时</td><td>发送后计时</td><td><strong>端点攻击</strong>——消息到达后立即提取</td></tr><tr><td>服务器副本</td><td>无（E2E）</td><td>无（但备份可能有）</td><td>WhatsApp 备份<strong>不受自毁约束</strong></td></tr><tr><td>截图防护</td><td>Android 强制禁止</td><td>仅通知</td><td><strong>另一部手机拍照</strong></td></tr><tr><td>转发约束</td><td>不支持转发</td><td>不受约束</td><td>转发副本<strong>不受自毁约束</strong></td></tr></tbody></table><h3 id="安全声明——攻击者的解读"><a href="#安全声明——攻击者的解读" class="headerlink" title="安全声明——攻击者的解读"></a>安全声明——攻击者的解读</h3><table><thead><tr><th>声明</th><th>攻击者利用的漏洞</th></tr></thead><tbody><tr><td>“我们用端到端加密”</td><td>Telegram 是<strong>可选</strong>而非默认 → 大多数用户用普通聊天 → 服务器有明文</td></tr><tr><td>“我们不留存任何数据”</td><td>区分<strong>内容</strong>和<strong>元数据</strong> → 内容不留但元数据大量留</td></tr><tr><td>“我们不与任何政府合作”</td><td>Telegram 2024-09 起配合 → WhatsApp 配合 + Meta 共享</td></tr><tr><td>“阅后即焚”</td><td>WA 发送后计时 → 到达后可提取；备份不受约束</td></tr><tr><td>“无法被解密”</td><td>对服务器确实无法 → 但对<strong>解锁后的端点</strong>完全可读</td></tr><tr><td>“零知识架构”</td><td>Signal 对内容零知识 → 但对元数据不是</td></tr></tbody></table><h3 id="Signal——攻击者的最难目标"><a href="#Signal——攻击者的最难目标" class="headerlink" title="Signal——攻击者的最难目标"></a>Signal——攻击者的最难目标</h3><p>攻击者对 Signal 的唯一 bypass：<strong>端点攻击</strong>。所有其他路径 ROI 极低：</p><ul><li>服务器内容 ❌ | 服务器元数据 ⚠️（仅注册+最后连接）| 云端备份 ❌ | cryptanalysis ❌</li><li>但：<strong>翻墙违法</strong> ✅（法律攻击向量）</li></ul><hr><h2 id="四种-Kill-Chain-场景"><a href="#四种-Kill-Chain-场景" class="headerlink" title="四种 Kill Chain 场景"></a>四种 Kill Chain 场景</h2><h3 id="Kill-Chain-A：低技术门槛（针对大多数目标）"><a href="#Kill-Chain-A：低技术门槛（针对大多数目标）" class="headerlink" title="Kill Chain A：低技术门槛（针对大多数目标）"></a>Kill Chain A：低技术门槛（针对大多数目标）</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Recon → 确认目标持有手机</span><br><span class="line">Initial Access → 内部批准搜查 → 无需搜查证（第二章）</span><br><span class="line">Social Engineering → 思想教育 → 目标交出密码（第一章）</span><br><span class="line">Execution → 目标解锁手机</span><br><span class="line">Exfiltration → 手动查看 + 拍照取证 / Cellebrite USB 提取</span><br><span class="line">Impact → 完整数字生活暴露</span><br></pre></td></tr></table></figure><p>ROI：★★★★★ 零技术成本，极高成功率</p><h3 id="Kill-Chain-B：中等技术门槛（目标用-WhatsApp）"><a href="#Kill-Chain-B：中等技术门槛（目标用-WhatsApp）" class="headerlink" title="Kill Chain B：中等技术门槛（目标用 WhatsApp）"></a>Kill Chain B：中等技术门槛（目标用 WhatsApp）</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Recon → 确认目标使用 WhatsApp</span><br><span class="line">Initial Access → 向 Apple/Google 发出司法调取</span><br><span class="line">Execution → 获取 iCloud/Google Drive 上 WhatsApp 备份（明文）</span><br><span class="line">Exfiltration → 直接分析备份内容</span><br><span class="line">Impact → 消息内容完整获取（E2E 在备份层被 bypass）</span><br></pre></td></tr></table></figure><p>ROI：★★★★ 低技术成本，高成功率（目标未开启 E2E 备份）</p><h3 id="Kill-Chain-C：高技术门槛（目标用-GrapheneOS-Signal）"><a href="#Kill-Chain-C：高技术门槛（目标用-GrapheneOS-Signal）" class="headerlink" title="Kill Chain C：高技术门槛（目标用 GrapheneOS + Signal）"></a>Kill Chain C：高技术门槛（目标用 GrapheneOS + Signal）</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Recon → 确认目标使用 GrapheneOS + Signal</span><br><span class="line">Initial Access → 只能走社会工程学</span><br><span class="line">Social Engineering → 思想教育 → 目标交出密码</span><br><span class="line">Execution → 目标解锁手机</span><br><span class="line">Exfiltration → 手动查看 Signal 消息 + 拍照取证</span><br><span class="line">Impact → 消息内容获取 + 翻墙违法证据</span><br></pre></td></tr></table></figure><p>ROI：★★★ 需要社会工程学成功，技术取证工具无法辅助</p><h3 id="Kill-Chain-D：国家级（高价值目标）"><a href="#Kill-Chain-D：国家级（高价值目标）" class="headerlink" title="Kill Chain D：国家级（高价值目标）"></a>Kill Chain D：国家级（高价值目标）</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Recon → SS7 定位 + 元数据分析 + 社交图谱构建</span><br><span class="line">Initial Access → Paragon Graphite 间谍软件 / 0-click exploit</span><br><span class="line">Execution → 植入持久化间谍软件</span><br><span class="line">Persistence → 系统级驻留（重启不消失）</span><br><span class="line">Exfiltration → 实时监控所有通讯 + 屏幕录制</span><br><span class="line">Impact → 完整数字生活实时监控</span><br></pre></td></tr></table></figure><p>ROI：★★ 成本极高（$2M+），但对高价值目标值得</p><hr><h2 id="系统级-ROI-分析"><a href="#系统级-ROI-分析" class="headerlink" title="系统级 ROI 分析"></a>系统级 ROI 分析</h2><table><thead><tr><th>层</th><th>攻击者看到的</th><th>可利用程度</th></tr></thead><tbody><tr><td><strong>法律层</strong></td><td>规制缺位 → 攻击面敞开</td><td>★★★★★</td></tr><tr><td><strong>社会工程学层</strong></td><td>“思想教育” → 稳定的 exploit</td><td>★★★★★</td></tr><tr><td><strong>备份层</strong></td><td>WhatsApp 默认未加密 → 低成本入口</td><td>★★★★</td></tr><tr><td><strong>元数据层</strong></td><td>大量收集 + 政府配合 → 直接获取</td><td>★★★★</td></tr><tr><td><strong>设备层</strong></td><td>GrapheneOS → 技术阻力极大</td><td>★★</td></tr><tr><td><strong>通讯层</strong></td><td>E2E 加密 → 传输&#x2F;服务器端阻力</td><td>★★</td></tr><tr><td><strong>密码学层</strong></td><td>无已知实际攻击路径</td><td>★</td></tr></tbody></table><p><strong>系统级结论</strong>：法律层和社会工程学层的 ROI 远高于设备层和通讯层。<strong>最好的防御不是技术防御——是法律规制和程序约束</strong>。但在当前法律框架下，这些恰恰是最大的攻击面。</p><hr><h2 id="最终评估"><a href="#最终评估" class="headerlink" title="最终评估"></a>最终评估</h2><table><thead><tr><th>维度</th><th>攻击者视角</th></tr></thead><tbody><tr><td><strong>核心 exploit</strong></td><td>“思想教育十分钟”是最稳定的 exploit——比任何 0-day 更难修补（载体是人，不是代码）</td></tr><tr><td><strong>法律攻击向量</strong></td><td>规制缺位是最大攻击面——无证搜查、无监督、无救济、程序性权利是软约束</td></tr><tr><td><strong>设备攻击面</strong></td><td>GrapheneOS 对技术取证极高阻力，但对社会工程学零防护；Cellebrite 当前无法突破</td></tr><tr><td><strong>通讯攻击面</strong></td><td>E2E 只阻断传输和服务器端；攻击者转向备份&#x2F;端点&#x2F;元数据&#x2F;法律向量</td></tr><tr><td><strong>Exfiltration</strong></td><td>四通道：截胡（被E2E阻断）、留底（部分阻断）、翻拍（完全不阻断）、脱管（本身就是通道）</td></tr><tr><td><strong>Kill Chain</strong></td><td>社会工程学 &gt; 云端备份截获 &gt; AFU取证 &gt; BFU暴力破解 &gt; 0-day &gt; chip-off</td></tr><tr><td><strong>三个等式</strong></td><td>本地删除 ≠ 数据消灭；加密传输 ≠ 信息保密；拒绝配合 ≠ 有效防御</td></tr><tr><td><strong>终极 bypass</strong></td><td>所有技术防御都可以被”让目标自愿交出密码”绕过——这是 ROI 最高的攻击路径</td></tr></tbody></table><hr>]]>
    </content>
    <id>http://example.com/2026/07/18/2026071801/</id>
    <link href="http://example.com/2026/07/18/2026071801/"/>
    <published>2026-07-18T06:18:22.000Z</published>
    <summary>
      <![CDATA[<h1 id="手机密码、加密通讯与取证对抗"><a href="#手机密码、加密通讯与取证对抗" class="headerlink" title="手机密码、加密通讯与取证对抗"></a>手机密码、加密通讯与取证对抗</h1><p>一次讯问笔录，一个被要求交出手机密码的嫌疑人]]>
    </summary>
    <title>手机密码、加密通讯与取证对抗</title>
    <updated>2026-07-18T08:38:33.628Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="社会工程学" scheme="http://example.com/categories/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="社会工程学" scheme="http://example.com/tags/%E7%A4%BE%E4%BC%9A%E5%B7%A5%E7%A8%8B%E5%AD%A6/"/>
    <category term="黑客组织" scheme="http://example.com/tags/%E9%BB%91%E5%AE%A2%E7%BB%84%E7%BB%87/"/>
    <category term="APT" scheme="http://example.com/tags/APT/"/>
    <category term="设备指纹" scheme="http://example.com/tags/%E8%AE%BE%E5%A4%87%E6%8C%87%E7%BA%B9/"/>
    <content>
      <![CDATA[<blockquote><h2 id="Scattered-Spider-（零落蜘蛛-散落蜘蛛）-黑客集团全解析-—-一群少年如何用一通电话撬动上亿美元，又如何败给一个-Windows-编号"><a href="#Scattered-Spider-（零落蜘蛛-散落蜘蛛）-黑客集团全解析-—-一群少年如何用一通电话撬动上亿美元，又如何败给一个-Windows-编号" class="headerlink" title="Scattered Spider （零落蜘蛛&#x2F;散落蜘蛛） 黑客集团全解析 — 一群少年如何用一通电话撬动上亿美元，又如何败给一个 Windows 编号"></a><strong>Scattered Spider （零落蜘蛛&#x2F;散落蜘蛛） 黑客集团全解析</strong> — 一群少年如何用一通电话撬动上亿美元，又如何败给一个 Windows 编号</h2></blockquote><hr><blockquote><h3 id="组织档案"><a href="#组织档案" class="headerlink" title="组织档案"></a>组织档案</h3><table><thead><tr><th>项目</th><th>内容</th></tr></thead><tbody><tr><td><strong>名称</strong></td><td>Scattered Spider（散落的蜘蛛）、ShinyHunters（闪光猎人）</td></tr><tr><td><strong>昵称</strong></td><td>参见 <a href="#2-scattered-spider-%E6%98%AF%E8%B0%81">Scattered Spider 是谁</a> 部分的别名列表</td></tr><tr><td><strong>成立时间</strong></td><td>约 2022 年 5 月</td></tr><tr><td><strong>类型</strong></td><td>黑客组织</td></tr><tr><td><strong>目的</strong></td><td>勒索软件、网络攻击、数据窃取勒索</td></tr><tr><td><strong>活跃区域</strong></td><td>美国和英国</td></tr><tr><td><strong>主要手段</strong></td><td>社会工程、勒索软件即服务（RaaS）、密码破解</td></tr><tr><td><strong>已知合作关系</strong></td><td>ALPHV、ShinyHunters</td></tr></tbody></table></blockquote><hr><h2 id="1-引子：赫尔辛基机场的拦截"><a href="#1-引子：赫尔辛基机场的拦截" class="headerlink" title="1. 引子：赫尔辛基机场的拦截"></a>1. 引子：赫尔辛基机场的拦截</h2><p>2026 年 4 月 10 日，芬兰赫尔辛基机场，安检口。</p><p>一个 19 岁的年轻人正准备登机飞往日本。芬兰警方按照 Interpol 红色通缉令拦下了他，当场扣押两块 2TB 容量的移动存储设备。</p><p>他叫 <strong>Peter Stokes</strong>，美国-爱沙尼亚双重国籍。据检方指控，他是黑客团伙 Scattered Spider（又称 Octo Tempest、0ktapus）的一员，涉嫌共谋、计算机入侵与欺诈。</p><p>过去一年多，他一直在用 VPN、代理和隧道工具在塔林、纽约、曼谷之间切换出口 IP。按照大多数人的直觉，这已经算是”消失”了。</p><p>真正把他钉在起诉书上的线索，藏在他自己那台 Windows 电脑里——一串他大概从没听说过的编号。这里头既没有惊天动地的抓包战，安插卧底那种老套桥段也用不上。</p><blockquote><p>💬 <strong>病毒式传播的推文</strong></p><p>X 上一条被转发和收藏了数千次的帖子，把这件事总结得最到位：</p><p><em>“19 year old kid used a VPN and change his IP across 3 countries… windows was tracking a number he didn’t even know existed”</em></p><p>「一个 19 岁孩子用 VPN 换了三个国家的 IP……而 Windows 一直在追踪一个他自己都不知道存在的编号。」</p><p>这条来自 <code>@cyber_razz</code> 的短帖浏览量冲到了 <strong>242 万</strong>。</p></blockquote><p>在讲述这个编号到底是什么之前，我们需要先把视角拉远，看看 Scattered Spider 这个组织究竟是什么样的存在。</p><hr><h2 id="2-Scattered-Spider-是谁"><a href="#2-Scattered-Spider-是谁" class="headerlink" title="2. Scattered Spider 是谁"></a>2. Scattered Spider 是谁</h2><p><strong>Scattered Spider</strong>，又称 <strong>UNC3944</strong>，最近又被确认为 <strong>ShinyHunters</strong>，是一个主要由青少年和年轻人组成的黑客组织，据信居住在美国和英国。该组织被认为与网络犯罪网络 <strong>“The Com”</strong> 有关联，更确切地说是与 The Com 的一个子集 <strong>“Hacker Com”</strong> 有关联。</p><p>该组织在新闻稿和记者报道中最常用的名称是”散落的蜘蛛”（Scattered Spider），但该组织也曾使用过许多其他名称：</p><table><thead><tr><th>别名</th><th>中文译名</th><th>说明</th></tr></thead><tbody><tr><td>Star Fraud</td><td>星骗</td><td>早期使用名称之一</td></tr><tr><td>Octo Tempest</td><td>八爪风暴</td><td>微软威胁追踪使用的名称</td></tr><tr><td>Scatter Swine</td><td>散落的猪</td><td>—</td></tr><tr><td>Muddled Libra</td><td>混乱的天秤座</td><td>Google Mandiant 使用的名称（UNC3944）</td></tr><tr><td>0ktapus</td><td>—</td><td>因 0ktapus 钓鱼攻击而得名</td></tr></tbody></table><p>Scattered Spider 是名为”The Community”或”The Com”的全球黑客社区的组成部分，该社区的成员曾入侵过美国大型科技公司。</p><h3 id="已知攻击目标"><a href="#已知攻击目标" class="headerlink" title="已知攻击目标"></a>已知攻击目标</h3><p>该组织因参与入侵和勒索凯撒娱乐集团和美高梅国际酒店集团（美国最大的两家赌场和博彩公司）而臭名昭著。Scattered Spider 还曾攻击过以下目标：</p><table><thead><tr><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody><tr><td>Visa</td><td>玛莎百货 (M&amp;S)</td><td>PNC 金融服务</td><td>泛美银行</td><td>纽约人寿保险</td><td>Synchrony Financial</td></tr><tr><td>Truist 银行</td><td>Twilio</td><td>捷豹路虎 (JLR)</td><td>AT&amp;T</td><td>Ticketmaster</td><td>Qantas 澳航</td></tr></tbody></table><h3 id="关键数据"><a href="#关键数据" class="headerlink" title="关键数据"></a>关键数据</h3><table><thead><tr><th>指标</th><th>数值</th></tr></thead><tbody><tr><td>网络入侵事件</td><td><strong>100+</strong></td></tr><tr><td>直接赎金支付</td><td><strong>$1 亿+</strong></td></tr><tr><td>成立年份</td><td><strong>2022</strong></td></tr><tr><td>已知成员被捕</td><td><strong>5+</strong></td></tr></tbody></table><hr><h2 id="3-组织简史与主要攻击手法"><a href="#3-组织简史与主要攻击手法" class="headerlink" title="3. 组织简史与主要攻击手法"></a>3. 组织简史与主要攻击手法</h2><p>据信，Scattered Spider 成立于 <strong>2022 年 5 月</strong>，当时该组织专注于攻击电信公司。其核心攻击手段包括：</p><table><thead><tr><th>攻击手法</th><th>说明</th></tr></thead><tbody><tr><td>SIM 卡交换诈骗</td><td>通过社工运营商客服，将受害者手机号移植到攻击者控制的 SIM 卡上，进而接管短信验证码</td></tr><tr><td>MFA 疲劳攻击</td><td>连续推送大量多因素认证请求，直到受害者不堪其扰误点”批准”</td></tr><tr><td>短信&#x2F;Telegram 钓鱼</td><td>发送伪装成企业 IT 部门的消息，诱导员工输入凭证</td></tr><tr><td>IT 帮助台社工</td><td>冒充员工致电帮助台，以”手机丢了收不到验证码”为由重置密码和 MFA</td></tr></tbody></table><p>该组织通常利用 Windows 反 DoS 软件中的网络安全漏洞 <code>CVE-2015-2291</code> 来终止安全软件，从而逃避检测。据信，该组织对 Microsoft Azure 有着深入的了解，能够对基于 Google Workspace 和 AWS 的云计算平台进行侦察，并且使用合法开发的远程访问工具。</p><p>该组织后来因攻击关键基础设施而闻名，之后又于 2023 年发动了赌场黑客攻击。2025 年，DataBreaches.net 报道称，Scattered Spider 已与 ShinyHunters 合并（或反之亦然）。</p><blockquote><p>⚠️ <strong>2025 年新动态</strong></p><p>一个由 Scattered Spider、Lapsus$ 和 ShinyHunters 等网络犯罪团伙组成的联盟于 2025 年成立，并且自 2025 年 8 月 8 日以来至少创建了 <strong>16 个 Telegram 频道</strong>。</p></blockquote><hr><h2 id="4-2023-赌场攻击事件"><a href="#4-2023-赌场攻击事件" class="headerlink" title="4. 2023 赌场攻击事件"></a>4. 2023 赌场攻击事件</h2><p>Scattered Spider 利用社会工程学手段入侵了凯撒娱乐和美高梅集团的内部系统。该组织通过获取登录凭证和一次性密码，绕过了多重身份验证技术。该组织声称，之所以选择米高梅作为攻击目标，是因为米高梅曾发现该组织试图操纵老虎机以谋取自身利益。</p><h3 id="凯撒娱乐（Caesars）"><a href="#凯撒娱乐（Caesars）" class="headerlink" title="凯撒娱乐（Caesars）"></a>凯撒娱乐（Caesars）</h3><p>2023 年，凯撒娱乐向 Scattered Spider 支付了 <strong>1500 万美元</strong>赎金，这是他们最初要价 3000 万美元的一半。Scattered Spider 使用与攻击 MGM 类似的策略，获取了凯撒娱乐”大量”客户的驾照号码，甚至可能还有社会安全号码。凯撒娱乐发表声明称，虽然公司无法保证删除 Scattered Spider 获取的信息，但将采取一切必要措施来实现这一目标。</p><blockquote><p>📌 <strong>争议</strong></p><p>关于 Scattered Spider 是否是袭击凯撒宫的组织，各方说法不一。一些人认为这是由一个英美组织所为，而另一些人则表示袭击者并非该组织，或身份不明。</p></blockquote><h3 id="美高梅度假村（MGM-Resorts）"><a href="#美高梅度假村（MGM-Resorts）" class="headerlink" title="美高梅度假村（MGM Resorts）"></a>美高梅度假村（MGM Resorts）</h3><p>Scattered Spider 与 <strong>ALPHV</strong>（一家提供勒索软件即服务的软件开发团队）合作。Scattered Spider 冒充其在 LinkedIn 上找到的 MGM 员工，致电 MGM 的客服热线以获取内部访问权限。该组织于 <strong>2023 年 9 月 11 日</strong>成功入侵。</p><p>美高梅度假村于 2023 年 9 月 12 日首次披露了此次网络攻击，并在次日向美国证券交易委员会提交的 8-K 表格报告中予以披露。该公司表示，尽管已”处理”了此次网络攻击，但其度假村的许多计算机系统仍然处于离线状态，包括但不限于餐饮积分和免费积分系统。此次攻击还导致度假村内的自动取款机和远程房卡无法使用，并使美高梅无法向顾客收取停车费。</p><h3 id="后续影响"><a href="#后续影响" class="headerlink" title="后续影响"></a>后续影响</h3><p>目前，美高梅集团与美国联邦贸易委员会（FTC）和联邦调查局（FBI）正在对此次网络攻击进行调查。穆迪公司表示，由于美高梅集团的大部分运营都高度依赖计算机，此次网络攻击可能会导致其信用评级下降。两家公司遭受攻击的消息公布后，凯撒娱乐和美高梅集团的股价均出现下跌。美高梅集团首席执行官威廉·霍恩巴克尔在一次行业会议上指出，此次黑客攻击导致公司对其旗下物业的状况”完全一无所知”。</p><p>2023 年 9 月，在黑客攻击事件发生后，美高梅和凯撒娱乐均遭到顾客集体诉讼，所有原告均声称这两家赌场运营商未能充分保护其数据，构成违约。2025 年 1 月，美高梅同意向数据泄露事件的受害者支付 <strong>4500 万美元</strong>的和解金。</p><hr><h2 id="5-Snowflake-云端数据泄露"><a href="#5-Snowflake-云端数据泄露" class="headerlink" title="5. Snowflake 云端数据泄露"></a>5. Snowflake 云端数据泄露</h2><p>该团伙的两名成员与针对 Snowflake 云计算客户的黑客攻击有关。黑客访问并窃取了客户数据，并索要数百万美元的赎金。近百家公司成为攻击目标，其中包括：</p><table><thead><tr><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody><tr><td>AT&amp;T</td><td>Ticketmaster</td><td>Advance Auto Parts</td><td>LendingTree</td><td>Neiman Marcus</td></tr></tbody></table><hr><h2 id="6-逮捕时间线"><a href="#6-逮捕时间线" class="headerlink" title="6. 逮捕时间线"></a>6. 逮捕时间线</h2><ul><li><p><strong>2024 年 1 月</strong> — <strong>Noah Michael Urban</strong>（化名 Sosa、King Bob、Elijah）在佛罗里达州被捕，涉嫌累计盗窃价值约 <strong>80 万美元</strong>的加密货币。Sosa 利用 SIM 卡交换技术入侵受害者的电子邮件和金融账户信息。</p></li><li><p><strong>2024 年 6 月</strong> — 该组织头目 <strong>Tyler Buchanan</strong>（又名 TylerB）在西班牙试图登上飞往意大利的航班时被捕。西班牙警方称，布坎南被捕时持有价值 <strong>2700 万美元</strong>的比特币。</p></li><li><p><strong>2024 年 7 月</strong> — 西米德兰兹郡警方在 FBI 协助下逮捕了一名 <strong>17 岁少年</strong>，该少年涉嫌参与米高梅网络攻击事件。这名嫌疑人居住在沃尔索尔，姓名未公开，在执法部门检查其电子设备期间获准保释。</p></li><li><p><strong>2024 年 11 月</strong> — 19 岁的 <strong>Remington Ogletree</strong> 被捕，罪名与他涉嫌参与该组织有关。</p></li><li><p><strong>2025 年 9 月 17 日</strong> — 一名涉嫌参与赌场黑客案的<strong>未成年嫌疑人</strong>向克拉克县少年拘留中心自首。</p></li><li><p><strong>2026 年 4 月 10 日</strong> — 网名为 <strong>“Bouquet”</strong> 的 19 岁爱沙尼亚裔美国双重国籍公民 <strong>Peter Stokes</strong> 在芬兰赫尔辛基机场试图登机前往日本时被捕。美国联邦检察官要求将其引渡至芝加哥，指控其犯有电信诈骗、共谋和计算机入侵等罪行，这些罪行至少涉及四起 Scattered Spider 网络犯罪活动。</p></li></ul><hr><h2 id="7-Peter-Stokes-案：GDID-如何锁定一台机器"><a href="#7-Peter-Stokes-案：GDID-如何锁定一台机器" class="headerlink" title="7. Peter Stokes 案：GDID 如何锁定一台机器"></a>7. Peter Stokes 案：GDID 如何锁定一台机器</h2><h3 id="官方定性：一个-19-岁少年与上亿美元的黑产网络"><a href="#官方定性：一个-19-岁少年与上亿美元的黑产网络" class="headerlink" title="官方定性：一个 19 岁少年与上亿美元的黑产网络"></a>官方定性：一个 19 岁少年与上亿美元的黑产网络</h3><p>2026 年 7 月 1 日，美国司法部（DOJ）发布新闻稿（编号 26-728），确认了这次跨国抓捕。</p><p>Stokes 被控是 Scattered Spider 的成员之一。这个组织在司法部的描述里，涉及超过 100 起网络入侵，直接造成超过 1 亿美元的赎金支付，加上间接损失，数字还要更大。</p><p>安全研究机构 Group-IB 把 Scattered Spider 形容成一个松散的”标签”：一群北美、欧洲的青少年和年轻成人黑客，共享同一套打法——电话社工 IT 帮助台、SIM 卡劫持、云账户接管，再衔接数据窃取和勒索。抓住一个人，这张网不会跟着散掉。</p><p>在检方的描述里，Stokes 住过爱沙尼亚塔林，也在阿联酋等地活动过。他的 Snapchat 上晒过现金、名表、钻石链和旅行照片。FBI 在相关材料里给出的评价是：”以他的年龄而论，财富显眼得不寻常。”</p><blockquote><p>⚖️ <strong>法律声明</strong></p><p>截至发稿，这些都还只是检方指控。在法院最终判决之前，Stokes 在法律上仍是无罪的。</p></blockquote><h3 id="倒回起点：一通”求助电话”撬开一家零售商的整个网络"><a href="#倒回起点：一通”求助电话”撬开一家零售商的整个网络" class="headerlink" title="倒回起点：一通”求助电话”撬开一家零售商的整个网络"></a>倒回起点：一通”求助电话”撬开一家零售商的整个网络</h3><p>投诉状里描述最详细的一案，发生在 2025 年 5 月。检方称之为对某美国奢侈珠宝零售商（投诉状化名 <strong>“Company F”</strong>）的入侵。</p><p>这次入侵没有用到任何高深的漏洞，起点是一通电话。</p><p>攻击者用 Google Voice 虚拟号码，冒充被锁账号的员工，打给公司 IT 帮助台：手机丢了，收不到验证码，工作等着用。如果帮助台缺乏严格的核身流程，接下来会发生什么几乎可以预料——密码被重置，多因素认证被重新绑定到攻击者控制的设备上。</p><blockquote><p>💥 <strong>几小时内发生的事</strong></p><p>3 个账户失陷，其中 2 个是 IT 管理员账户。目录服务、终端设备、云控制台的权限边界，就这样从内部塌陷了。</p></blockquote><p>拿到管理员权限后，攻击者安装了 <code>ngrok</code> 和 <code>Teleport</code> 这类远程隧道工具。ngrok 的作用是：哪怕公司防火墙只允许对外连接，agent 仍然可以主动连上 ngrok 云端，把内网服务映射成一个公网能访问的地址。</p><p>数据被打包外传到亚马逊云存储，规模至少 <strong>77GB</strong>，包含个人云盘文件、目录服务数据和运维管理相关内容。</p><p>安全团队成功拦下了勒索软件的部署，但攻击者仍然发出了主题为 <code>&quot;WE STOLE THE DATA…&quot;</code> 的勒索邮件，索要约 <strong>800 万美元</strong>赎金。公司最终没有支付赎金，但响应、调查和业务中断的成本加起来，也有约 <strong>200 万美元</strong>。</p><p>到这里，故事还是一个常规的企业入侵案。真正让案情转向的，是接下来这一步。</p><hr><h2 id="8-GDID-技术解析"><a href="#8-GDID-技术解析" class="headerlink" title="8. GDID 技术解析"></a>8. GDID 技术解析</h2><h3 id="VPN-换掉的只是门牌号"><a href="#VPN-换掉的只是门牌号" class="headerlink" title="VPN 换掉的只是门牌号"></a>VPN 换掉的只是门牌号</h3><p>投诉状里记录了这样一个精确到分钟的细节：</p><blockquote><p>2025 年 5 月 12 日 <strong>19:21 (UTC)</strong>，一个 ngrok 账户被创建，创建时使用的 IP 属于 VPN 代理段（以 <code>.168</code> 结尾）。</p><p>同一时刻，微软的记录显示：一个编号为 <code>g:6755467234350028</code> 的标识，访问了 ngrok 的注册页面 <code>dashboard.ngrok.com/signup</code>。</p><p>约三小时后的 <strong>22:47 (UTC)</strong>，同一个编号，经过同一个代理 IP，访问了受害者公司的官网。</p></blockquote><p>这个编号，就是 <strong>GDID（Global Device Identifier，全球设备标识符）</strong>。</p><p>投诉状里，微软方代表给出的官方定义是这样的：</p><blockquote><p><em>“a persistent, device-level identifier designed to uniquely identify an installation of a Windows operating system on a device… remains consistent across Windows operating system updates… reinstall of Windows [creates a] new unique GDID.”</em></p><p>「一个持久的、设备级别的标识符，用于唯一标识某台设备上的一次 Windows 操作系统安装……系统更新后保持不变……重装 Windows 会生成一个新的 GDID。」</p></blockquote><p>换句话说，<strong>GDID 认准的，是这一次 Windows 安装本身——你的网络出口换成什么样，它都不在乎。</strong></p><blockquote><p>💡 <strong>通俗类比</strong></p><p>GDID 有点像你家门锁芯片里刻的那个序列号。你每天出门可以换口罩、换外套，相当于 VPN 换 IP。但门锁序列号没变——物业的维护日志一旦记下”这个序列号在什么时间通过哪条外网链路连回家”，再和你叫的外卖、住的酒店对上，别人就能推断出，来来去去的其实是同一个人。</p><p>公网 IP 能靠 VPN 随手换掉，浏览器 Cookie 一清了之，账号名换一个也就几分钟的事——这几样，都跟 GDID 对不上号。<strong>GDID 对应的，是操作系统安装本身的身份。</strong></p></blockquote><h3 id="其他系统的同类标识"><a href="#其他系统的同类标识" class="headerlink" title="其他系统的同类标识"></a>其他系统的同类标识</h3><table><thead><tr><th>操作系统</th><th>标识名称</th><th>性质</th></tr></thead><tbody><tr><td>Windows</td><td><code>GDID</code></td><td>设备级安装标识，跨更新保持不变</td></tr><tr><td>Apple (iOS&#x2F;macOS)</td><td><code>UDID</code> &#x2F; Hardware UUID</td><td>设备唯一标识</td></tr><tr><td>Android</td><td><code>Android ID</code></td><td>设备安装标识（用户重置可变）</td></tr><tr><td>Linux</td><td><code>/etc/machine-id</code></td><td>机器安装标识</td></tr></tbody></table><p>任何需要自动更新、软件许可和崩溃分析的操作系统，都得给设备分配一个能被反复识别的身份。区别只在于，各家生态默认的遥测范围有多大，执法机构调取起来有多方便。</p><blockquote><p>📌 <strong>能关掉吗？</strong></p><p>科技媒体 Windows Latest 在 7 月 10 日的报道标题直接写道：<strong>你无法完全关闭它</strong>——即便关闭了诊断数据上报，许可验证、应用商店和账户设备图谱这些环节，仍然可能保留这台安装的身份记录。</p></blockquote><hr><h2 id="9-证据链如何拼成一张网"><a href="#9-证据链如何拼成一张网" class="headerlink" title="9. 证据链如何拼成一张网"></a>9. 证据链如何拼成一张网</h2><h3 id="塔林、纽约、曼谷、机场"><a href="#塔林、纽约、曼谷、机场" class="headerlink" title="塔林、纽约、曼谷、机场"></a>塔林、纽约、曼谷、机场</h3><p>安全社群里，<code>@vxunderground</code> 是最早把投诉状逐条摘出来的账号之一。7 月 4 日那条帖子，用条目形式列出了投诉状第 12、34 页的关键内容，直接给出 GDID 编号，并指向微软 Azure Monitor 文档里 UCDOStatus 表下的 <code>GlobalDeviceId</code> 字段——这几乎是公开互联网上，微软官方文档提到这个标识的唯一明文出处。这条帖子拿下了约 <strong>1.07 万赞、125 万浏览</strong>。</p><p>把这条 GDID 编号的历史 IP 拉出来，就是一份设备的”旅行日记”：</p><ul><li><p><strong>2024 年 6 月上旬</strong> — 这台设备使用<strong>塔林</strong>的 IP 地址，同一时间，同一 IP 登录了检方指控中 Stokes 名下的 Facebook 和 Snapchat 账号。</p></li><li><p><strong>2024 年 10 月</strong> — 微软的网络安全研究人员提交了一份刑事举报（criminal referral），把网络人格 <strong>“spencer”</strong> 与真实姓名 Peter Stokes、塔林居住地关联了起来。</p></li><li><p><strong>2024 年 11 月</strong> — 美国官方掌握的跨境旅行记录显示 Stokes 赴<strong>纽约</strong>，同一时间段，这台设备的 IP 与 Apple、Snapchat 的登录 IP 在纽约重叠。Snapchat 上出现了四季酒店、华尔道夫酒店、UFC 赛事的画面。</p></li><li><p><strong>2025 年 1-2 月</strong> — 设备 IP 切换到<strong>泰国</strong>，同期的 Snapchat 和两个 Apple 账号登录，同样落在泰国，配图是曼谷华尔道夫酒店。</p></li><li><p><strong>2025 年 5 月</strong> — <strong>Company F 入侵事件</strong>发生。GDID <code>g:6755467234350028</code> 在 ngrok 注册和受害者公司官网访问中均被记录。</p></li></ul><p>每一次地理位置切换，社交账号的登录时间和 IP 都几乎精确地对上了。检方能做的，是把这几条独立的线——微软的设备记录、ngrok 的账户日志、Apple&#x2F;Snapchat&#x2F;Facebook 各自的登录 IP、美国官方的出入境记录——用同一套时间戳和 IP 拼在一起。</p><blockquote><p>🔑 <strong>关键结论</strong></p><p>真正让他现出原形的，是攻击操作、生活炫耀、旅行打卡，统统压在了<strong>同一台 Windows 安装上</strong>——单独换个 IP，做不到这一步。他换掉的是网络出口，换不掉的是承载了他全部数字生活的那台机器。</p><p>护照能拦下的是身体，GDID 拦下的，是那台跟着他一起旅行的电脑。</p></blockquote><hr><h2 id="10-舆论分裂：细思极恐-vs-活该"><a href="#10-舆论分裂：细思极恐-vs-活该" class="headerlink" title="10. 舆论分裂：细思极恐 vs 活该"></a>10. 舆论分裂：细思极恐 vs 活该</h2><p>案件曝光后，X 上的讨论重心很快偏离了案情本身，转而聚焦在一件事上：<strong>Windows 原来一直带着一个大家从没听说过的编号。</strong></p><p>评论区的声音大致分成三种：</p><h3 id="1-痛快型"><a href="#1-痛快型" class="headerlink" title="1. 痛快型"></a>1. 痛快型</h3><p>有人建议干脆现金买二手电脑、永远不在家或公司联网、只用公共 Wi-Fi、出门不带手机、不注册个人邮箱账号——这是一份相当极端的 OpSec（操作安全）清单，对红队人员或高风险群体或许有参考价值，对普通用户来说，成本高到没有实操意义。</p><h3 id="2-技术纠偏型"><a href="#2-技术纠偏型" class="headerlink" title="2. 技术纠偏型"></a>2. 技术纠偏型</h3><p>有人提醒，Apple 的 UDID、Android ID、Linux 的 <code>/etc/machine-id</code>，本质上都属于同一类”设备&#x2F;安装标识”，Windows 不过是其中一个例子。还有人指出，调查往往得先有 IP 或账户这类切入点，才能依法要求服务商深挖 GDID 历史，微软不会在没有法律程序的情况下，把全国用户的设备记录随便交出去。</p><h3 id="3-治理批判型"><a href="#3-治理批判型" class="headerlink" title="3. 治理批判型"></a>3. 治理批判型</h3><p><code>@vxunderground</code> 后续帖子讽刺微软的遥测机制，说这些数据收集打着 <code>&quot;improve your Windows Experience&quot;</code>（改善你的 Windows 体验）的旗号。批评者的核心不满在于：微软官方文档里，能找到 GDID 这个字段的地方，几乎只有 Azure Monitor 那份 UCDOStatus 表里的一行简短说明——</p><blockquote><p><em>“Microsoft global device identifier. This is a identifier used by Microsoft internally.”</em></p><p>「微软全球设备标识符，微软内部使用的一个标识符。」</p></blockquote><p>一个几乎从未被公开解释过的字段，却足以在联邦法庭的起诉书里成为关键证据。这种落差，才是真正让人不安的地方。</p><hr><h2 id="11-OpSec-深度复盘：Stokes-到底错在哪"><a href="#11-OpSec-深度复盘：Stokes-到底错在哪" class="headerlink" title="11. OpSec 深度复盘：Stokes 到底错在哪"></a>11. OpSec 深度复盘：Stokes 到底错在哪</h2><p>案件曝光后，安全圈围绕这起案件的攻防细节做了大量复盘。抛开情绪化评价，真正值得拆解的是其中暴露的几个关键技术问题。</p><h3 id="11-1-虚拟机隔离的陷阱：共享宿主网络栈的关联泄露"><a href="#11-1-虚拟机隔离的陷阱：共享宿主网络栈的关联泄露" class="headerlink" title="11.1 虚拟机隔离的陷阱：共享宿主网络栈的关联泄露"></a>11.1 虚拟机隔离的陷阱：共享宿主网络栈的关联泄露</h3><p>一个常见的直觉是：用虚拟机就能避免设备标识追踪。但这个直觉是错的。</p><p>在 Windows 上运行虚拟机（VM）或 WSL2 时，如果<strong>没有设置独立网口直通</strong>，虚拟机的流量仍然走 Windows 宿主的网口，GDID 会随着宿主网络栈一同带出。换句话说，虚拟机内的操作仍然会被关联到宿主操作系统的设备标识上。</p><p>这意味着”用虚拟机就安全了”是一个危险的假设。虚拟机的网络出口如果挂在宿主操作系统的网络栈上，设备标识的关联泄露就无法避免。正确做法是让虚拟机通过独立的物理网卡出网，而非共享宿主网络——或者干脆从一台 Linux 宿主 SSH 出去，避免 Windows 网络栈介入整个链路。</p><h3 id="11-2-Linux-标识可修改-vs-Windows-GDID-不可修改"><a href="#11-2-Linux-标识可修改-vs-Windows-GDID-不可修改" class="headerlink" title="11.2 Linux 标识可修改 vs Windows GDID 不可修改"></a>11.2 Linux 标识可修改 vs Windows GDID 不可修改</h3><p>GDID 并非唯一的设备标识符——Linux 有 <code>/etc/machine-id</code>，Apple 有 UDID，Android 有 Android ID。但各系统之间存在一个实质性的技术差异：<strong>可修改性</strong>。</p><p>Windows 的 GDID 对用户不可见、不可修改、不可关闭；而 Linux 的 <code>/etc/machine-id</code> 虽然同样会被服务器记录，但用户可以手动指定、修改或清空，也可以通过网络命名空间隔离出网行为。这并不意味着 Linux “更安全”——所有联网设备都会在服务器端留下可追溯的记录——但在用户对自身设备标识的控制权上，两者确实存在结构性差异。</p><h3 id="11-3-洋葱路由也不是万能的"><a href="#11-3-洋葱路由也不是万能的" class="headerlink" title="11.3 洋葱路由也不是万能的"></a>11.3 洋葱路由也不是万能的</h3><p>即便使用 Tor（洋葱路由），也并非完全不可追踪。历史上，FBI 曾多次通过控制 Tor 出口节点、利用浏览器漏洞等手段实现去匿名化。丝绸之路（Silk Road）案件的侦破就是典型案例之一。</p><p>洋葱路由解决的是网络层的流量混淆，但无法解决设备标识层面的关联问题。如果设备本身通过宿主操作系统的遥测机制向外泄露了标识信息，网络层的匿名化并不能提供额外保护。</p><h3 id="11-4-ngrok-vs-frp：隧道工具的选择与检测风险"><a href="#11-4-ngrok-vs-frp：隧道工具的选择与检测风险" class="headerlink" title="11.4 ngrok vs frp：隧道工具的选择与检测风险"></a>11.4 ngrok vs frp：隧道工具的选择与检测风险</h3><p>Stokes 使用 ngrok 建立远程隧道，这个选择本身也值得检讨。事实上，任何远程隧道工具都会在网络层留下可观测的流量特征——企业部署了 EDR&#x2F;NDR 方案时，异常的隧道连接（无论 ngrok 还是 frp）都有可能触发告警。关键不在于选哪个工具”更隐蔽”，而在于防御方的检测能力是否到位。</p><h3 id="11-5-斯诺登的先例"><a href="#11-5-斯诺登的先例" class="headerlink" title="11.5 斯诺登的先例"></a>11.5 斯诺登的先例</h3><p>这并非 Windows 遥测机制第一次引发争议。2013 年，斯诺登泄露的文件曾披露微软与 NSA 等情报机构之间存在合作通道。虽然微软后续否认了相关指控，但这一历史背景使得 GDID 的存在在隐私倡导者眼中更加敏感——一个用户无法查看、无法修改、无法关闭的设备级标识符，在法律程序下可以被调取用于刑事证据，这本身就是一种值得审视的权力不对称。</p><h3 id="11-6-最致命的失误：工作与生活未隔离"><a href="#11-6-最致命的失误：工作与生活未隔离" class="headerlink" title="11.6 最致命的失误：工作与生活未隔离"></a>11.6 最致命的失误：工作与生活未隔离</h3><p>Stokes 案中最核心的 OpSec 教训，不在于他用了什么工具或什么系统，而在于他<strong>把攻击操作和日常生活放在了同一台物理设备上的同一个操作系统安装上</strong>。</p><p>这是最基础也是最致命的失误。即使是技术水平一般的攻击者，也应当做到工作环境与生活环境的物理或虚拟隔离：攻击使用的环境应当是干净、独立的，每完成一次任务即销毁并重建；攻击工具应通过本地介质传递，而非从个人账户下载；网络层应使用多级跳板。这些原则并非高深技巧，但 Stokes 一条都没有做到。</p><p>他把犯罪操作、生活炫耀和旅行打卡，统统压在了同一台 Windows 安装上——VPN 换掉的是网络出口，换不掉的是承载了他全部数字生活的那台机器。</p><h3 id="11-7-企业防御视角：帮助台是第一道防线"><a href="#11-7-企业防御视角：帮助台是第一道防线" class="headerlink" title="11.7 企业防御视角：帮助台是第一道防线"></a>11.7 企业防御视角：帮助台是第一道防线</h3><p>从防御方的角度看，Company F 被攻破的起点暴露了一个更根本的问题：<strong>IT 帮助台工作人员接了一通电话，就重置了 IT 管理员账户的密码和 MFA。</strong></p><p>这暴露的不是一个技术漏洞，而是身份验证流程的管理缺陷。高权限账户的密码重置不应该允许通过一通电话完成，而应要求回拨验证、多人员审批等额外控制。技术防线再严密，如果帮助台的流程管不住，第一张骨牌就会倒下。</p><h3 id="11-8-复盘总结"><a href="#11-8-复盘总结" class="headerlink" title="11.8 复盘总结"></a>11.8 复盘总结</h3><table><thead><tr><th>议题</th><th>核心要点</th><th>价值评级</th></tr></thead><tbody><tr><td>虚拟机网络隔离</td><td>VM 未直通网口时，GDID 随宿主网络栈泄露</td><td>⭐⭐⭐ 常见误区</td></tr><tr><td>Linux vs Windows 标识可修改性</td><td>Linux <code>/etc/machine-id</code> 可改可伪造，Windows GDID 不可改</td><td>⭐⭐⭐ 实质性差异</td></tr><tr><td>洋葱路由局限性</td><td>Tor 出口节点可被控制，无法解决设备标识关联</td><td>⭐⭐ 战略提醒</td></tr><tr><td>隧道工具检测风险</td><td>ngrok 和 frp 均有可检测的流量特征</td><td>⭐⭐ 工具选型参考</td></tr><tr><td>工作&#x2F;生活设备隔离</td><td>攻击操作与日常使用同一台设备是最致命的 OpSec 失误</td><td>⭐⭐⭐ 核心教训</td></tr><tr><td>帮助台流程缺陷</td><td>管理员账户可被电话重置，是管理问题而非技术问题</td><td>⭐⭐⭐ 企业防御重点</td></tr></tbody></table><hr><h2 id="12-对普通人和企业意味着什么"><a href="#12-对普通人和企业意味着什么" class="headerlink" title="12. 对普通人和企业意味着什么"></a>12. 对普通人和企业意味着什么</h2><p>这起案子里，真正的破绽出在<strong>人</strong>身上——帮助台一通电话就能被唬住，密码学再严密也挡不住。</p><p>Company F 丢掉整个网络的控制权，起点是帮助台工作人员接了一通”听起来很紧急”的电话。行业里对这类攻击早有对策：重要账户的密码重置需要回拨到预留号码、需要经理二次审批，高权限账户不应该允许用可重置的短信验证码找回。这些流程不修补，再强的技术防线，也挡不住第一张倒下的骨牌。</p><p>Stokes 这边的破绽，是他把犯罪操作和生活方式，放在了<strong>同一台设备</strong>上。VPN 能解决的是”网络服务商知不知道你是谁”，解决不了”你自己把行程发到了 Snapchat”。当 GDID 把设备的 IP 轨迹摊开时，社交平台上的时间戳和地理位置，就变成了现成的对照表。</p><blockquote><p>🎯 <strong>三层启示</strong></p><p><strong>对普通用户</strong>：这起案子给不了”如何反侦查”的教程，能带来的收获，是理解隐私本身是分层的。换 IP 只解决了网络层的可见性，账户、设备和你自己留下的生活痕迹，是完全独立的另外几层。</p><p><strong>对企业安全负责人</strong>：帮助台是攻防的第一线，日志则是能反过来帮上忙的武器——前提是，你得先把流程管好。</p><p><strong>对政策制定者</strong>：一个能力既然已经存在，并且真实地出现在了联邦法庭的证据链里，相应的透明度和监督，就不该继续是一份写在 Azure 文档角落里的、只有一行字的说明。</p></blockquote><hr><p>Stokes 被扣下的两块 2TB 存储设备，如今躺在美国联邦调查程序里。他本人正在伊利诺伊北区法院，等待着与检方指控相关的下一步进展。</p><p>而那个曾经只在微软内部系统里流转的编号，已经在公众面前，第一次露出了它真正的分量。</p><hr><p><em>本文基于公开报道、美国司法部新闻稿及法庭投诉状整理编写。所有指控在法院最终判决前，被告在法律上均被假定为无罪。</em></p><p><strong>主要参考来源：</strong> Wikipedia “Scattered Spider” 词条；美国司法部新闻稿（编号 26-728）；美国伊利诺伊北区联邦法院刑事投诉状；@vxunderground &#x2F; @cyber_razz &#x2F; @T3chFalcon X 平台帖文；DataBreaches.net；Windows Latest 报道；Group-IB 威胁情报报告。</p>]]>
    </content>
    <id>http://example.com/2026/07/16/2026071601/</id>
    <link href="http://example.com/2026/07/16/2026071601/"/>
    <published>2026-07-16T01:08:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<h2 id="Scattered-Spider-（零落蜘蛛-散落蜘蛛）-黑客集团全解析-—-一群少年如何用一通电话撬动上亿美元，又如何败给一个-Windows-编号"><a href="#Scattered-Spider-（零落蜘蛛-散落蜘蛛）-黑客集]]>
    </summary>
    <title>黑客组织 - 散落蜘蛛：从赌场勒索到赫尔辛基机场逮捕</title>
    <updated>2026-08-09T08:43:48.093Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="Java" scheme="http://example.com/categories/Java/"/>
    <category term="随手记" scheme="http://example.com/tags/%E9%9A%8F%E6%89%8B%E8%AE%B0/"/>
    <content>
      <![CDATA[<blockquote><p>就业平台的三角色系统、音乐网站的协同过滤推荐、钢铁工厂的中文搜索——三个不同行业的全栈项目，每个都有独特的业务挑战。再加上两组同业务双版本（单体vs微服务）的架构对比，本文把行业实战和架构选型的完整图景讲透。</p></blockquote><hr><h2 id="第一章：高校毕业生就业服务平台"><a href="#第一章：高校毕业生就业服务平台" class="headerlink" title="第一章：高校毕业生就业服务平台"></a>第一章：高校毕业生就业服务平台</h2><h3 id="1-1-三角色系统"><a href="#1-1-三角色系统" class="headerlink" title="1.1 三角色系统"></a>1.1 三角色系统</h3><table><thead><tr><th>角色</th><th>type值</th><th>权限</th><th>典型功能</th></tr></thead><tbody><tr><td>管理员</td><td>0</td><td>全部</td><td>管理企业&#x2F;学生&#x2F;岗位</td></tr><tr><td>企业</td><td>1</td><td>管理自己的岗位</td><td>发布岗位、查看投递</td></tr><tr><td>学生</td><td>2</td><td>查看岗位、投递简历</td><td>搜索岗位、投递简历</td></tr></tbody></table><h3 id="1-2-登录流程"><a href="#1-2-登录流程" class="headerlink" title="1.2 登录流程"></a>1.2 登录流程</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping(&quot;/login&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">login</span><span class="params">(<span class="meta">@RequestBody</span> Map&lt;String, String&gt; params)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> params.get(<span class="string">&quot;username&quot;</span>);</span><br><span class="line">    <span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> params.get(<span class="string">&quot;password&quot;</span>);</span><br><span class="line">    <span class="type">Integer</span> <span class="variable">type</span> <span class="operator">=</span> Integer.parseInt(params.get(<span class="string">&quot;type&quot;</span>)); <span class="comment">// 0/1/2</span></span><br><span class="line">    </span><br><span class="line">    <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userService.login(username, password, type);</span><br><span class="line">    <span class="keyword">if</span> (user == <span class="literal">null</span>) <span class="keyword">return</span> R.error(<span class="string">&quot;账号或密码不正确&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> jwtUtils.generateToken(</span><br><span class="line">        user.getId(), username, <span class="string">&quot;job&quot;</span>, type.toString());</span><br><span class="line">    <span class="keyword">return</span> R.ok().put(<span class="string">&quot;token&quot;</span>, token).put(<span class="string">&quot;type&quot;</span>, type);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-3-角色数据隔离"><a href="#1-3-角色数据隔离" class="headerlink" title="1.3 角色数据隔离"></a>1.3 角色数据隔离</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/jobs/list&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">list</span><span class="params">(HttpServletRequest request)</span> &#123;</span><br><span class="line">    <span class="type">Integer</span> <span class="variable">type</span> <span class="operator">=</span> (Integer) request.getSession().getAttribute(<span class="string">&quot;type&quot;</span>);</span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> (Long) request.getSession().getAttribute(<span class="string">&quot;userId&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (type == <span class="number">1</span>) &#123;</span><br><span class="line">        <span class="comment">// 企业用户：只查自己发布的岗位</span></span><br><span class="line">        <span class="keyword">return</span> R.success(jobService.listByEnterpriseId(userId));</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 管理员/学生：查所有岗位</span></span><br><span class="line">        <span class="keyword">return</span> R.success(jobService.list());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-4-简历投递流程"><a href="#1-4-简历投递流程" class="headerlink" title="1.4 简历投递流程"></a>1.4 简历投递流程</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">学生搜索岗位 → 查看详情 → 投递简历（status=0待处理）</span><br><span class="line">    → 企业查看投递 → 通过(status=1) / 拒绝(status=2)</span><br><span class="line">    → 学生查看结果</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">apply</span><span class="params">(Long studentId, Long jobId, Long resumeId)</span> &#123;</span><br><span class="line">    <span class="comment">// 检查是否已投递</span></span><br><span class="line">    <span class="type">Application</span> <span class="variable">existing</span> <span class="operator">=</span> applicationMapper.findByStudentAndJob(studentId, jobId);</span><br><span class="line">    <span class="keyword">if</span> (existing != <span class="literal">null</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;已投递过该岗位&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 检查岗位是否在招聘中</span></span><br><span class="line">    <span class="type">Job</span> <span class="variable">job</span> <span class="operator">=</span> jobMapper.selectById(jobId);</span><br><span class="line">    <span class="keyword">if</span> (job.getStatus() != <span class="number">1</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;该岗位已停止招聘&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="type">Application</span> <span class="variable">app</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Application</span>();</span><br><span class="line">    app.setStudentId(studentId);</span><br><span class="line">    app.setJobId(jobId);</span><br><span class="line">    app.setResumeId(resumeId);</span><br><span class="line">    app.setStatus(<span class="number">0</span>); <span class="comment">// 待处理</span></span><br><span class="line">    app.setApplyTime(<span class="keyword">new</span> <span class="title class_">Date</span>());</span><br><span class="line">    applicationMapper.insert(app);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-5-ECharts-数据可视化"><a href="#1-5-ECharts-数据可视化" class="headerlink" title="1.5 ECharts 数据可视化"></a>1.5 ECharts 数据可视化</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 就业率仪表盘</span></span><br><span class="line"><span class="title function_">initGauge</span>(<span class="params">id, rate</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> chart = echarts.<span class="title function_">init</span>(<span class="variable language_">document</span>.<span class="title function_">getElementById</span>(id))</span><br><span class="line">    chart.<span class="title function_">setOption</span>(&#123;</span><br><span class="line">        <span class="attr">series</span>: [&#123;</span><br><span class="line">            <span class="attr">type</span>: <span class="string">&#x27;gauge&#x27;</span>,</span><br><span class="line">            <span class="attr">data</span>: [&#123; <span class="attr">value</span>: rate, <span class="attr">name</span>: <span class="string">&#x27;就业率&#x27;</span> &#125;],</span><br><span class="line">            <span class="attr">detail</span>: &#123; <span class="attr">formatter</span>: <span class="string">&#x27;&#123;value&#125;%&#x27;</span> &#125;</span><br><span class="line">        &#125;]</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 薪资分布柱状图</span></span><br><span class="line"><span class="title function_">initBar</span>(<span class="params">id, data</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> chart = echarts.<span class="title function_">init</span>(<span class="variable language_">document</span>.<span class="title function_">getElementById</span>(id))</span><br><span class="line">    chart.<span class="title function_">setOption</span>(&#123;</span><br><span class="line">        <span class="attr">series</span>: [&#123;</span><br><span class="line">            <span class="attr">type</span>: <span class="string">&#x27;bar&#x27;</span>,</span><br><span class="line">            <span class="attr">data</span>: data.<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =&gt;</span> (&#123; <span class="attr">name</span>: item.<span class="property">range</span>, <span class="attr">value</span>: item.<span class="property">count</span> &#125;))</span><br><span class="line">        &#125;]</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-6-智能推荐"><a href="#1-6-智能推荐" class="headerlink" title="1.6 智能推荐"></a>1.6 智能推荐</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 基于专业的推荐</span></span><br><span class="line"><span class="keyword">public</span> List&lt;Job&gt; <span class="title function_">recommendByMajor</span><span class="params">(Long studentId)</span> &#123;</span><br><span class="line">    <span class="type">Student</span> <span class="variable">student</span> <span class="operator">=</span> studentMapper.selectById(studentId);</span><br><span class="line">    <span class="keyword">return</span> jobMapper.findByRequiredMajor(student.getMajor(), student.getDegree());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 基于行为的推荐</span></span><br><span class="line"><span class="keyword">public</span> List&lt;Job&gt; <span class="title function_">recommendByBehavior</span><span class="params">(Long studentId)</span> &#123;</span><br><span class="line">    List&lt;Long&gt; viewedJobIds = viewHistoryMapper.findJobIdsByStudentId(studentId);</span><br><span class="line">    List&lt;Long&gt; similarStudentIds = viewHistoryMapper.findSimilarStudents(viewedJobIds, studentId);</span><br><span class="line">    <span class="keyword">return</span> jobMapper.findJobsByViewers(similarStudentIds, viewedJobIds);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="第二章：音乐网站全栈开发"><a href="#第二章：音乐网站全栈开发" class="headerlink" title="第二章：音乐网站全栈开发"></a>第二章：音乐网站全栈开发</h2><h3 id="2-1-核心功能"><a href="#2-1-核心功能" class="headerlink" title="2.1 核心功能"></a>2.1 核心功能</h3><p>歌曲管理、歌手管理、歌单管理、评论系统、评分系统、收藏系统、协同过滤推荐。</p><h3 id="2-2-评分系统"><a href="#2-2-评分系统" class="headerlink" title="2.2 评分系统"></a>2.2 评分系统</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping(&quot;/song/rate&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">rate</span><span class="params">(<span class="meta">@RequestBody</span> Map&lt;String, Object&gt; params, HttpServletRequest request)</span> &#123;</span><br><span class="line">    <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> getCurrentUserId(request);</span><br><span class="line">    <span class="type">Long</span> <span class="variable">songId</span> <span class="operator">=</span> Long.valueOf(params.get(<span class="string">&quot;songId&quot;</span>).toString());</span><br><span class="line">    <span class="type">Integer</span> <span class="variable">score</span> <span class="operator">=</span> Integer.valueOf(params.get(<span class="string">&quot;score&quot;</span>).toString());</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (score &lt; <span class="number">1</span> || score &gt; <span class="number">5</span>) <span class="keyword">return</span> R.error(<span class="string">&quot;评分范围为1-5分&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 检查是否已评分（更新 or 新增）</span></span><br><span class="line">    <span class="type">Rating</span> <span class="variable">existing</span> <span class="operator">=</span> ratingMapper.findByUserAndSong(userId, songId);</span><br><span class="line">    <span class="keyword">if</span> (existing != <span class="literal">null</span>) &#123;</span><br><span class="line">        existing.setScore(score);</span><br><span class="line">        ratingMapper.updateById(existing);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="type">Rating</span> <span class="variable">rating</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Rating</span>();</span><br><span class="line">        rating.setUserId(userId);</span><br><span class="line">        rating.setSongId(songId);</span><br><span class="line">        rating.setScore(score);</span><br><span class="line">        ratingMapper.insert(rating);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> R.success(<span class="string">&quot;评分成功&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取平均评分（Redis缓存）</span></span><br><span class="line"><span class="meta">@GetMapping(&quot;/song/&#123;id&#125;/rating&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">getRating</span><span class="params">(<span class="meta">@PathVariable</span> Long id)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">cacheKey</span> <span class="operator">=</span> <span class="string">&quot;song:rating:&quot;</span> + id;</span><br><span class="line">    <span class="type">Double</span> <span class="variable">avgScore</span> <span class="operator">=</span> redisTemplate.opsForValue().get(cacheKey);</span><br><span class="line">    <span class="keyword">if</span> (avgScore == <span class="literal">null</span>) &#123;</span><br><span class="line">        avgScore = ratingMapper.selectAvgScore(id);</span><br><span class="line">        <span class="keyword">if</span> (avgScore == <span class="literal">null</span>) avgScore = <span class="number">0.0</span>;</span><br><span class="line">        redisTemplate.opsForValue().set(cacheKey, avgScore, <span class="number">10</span>, TimeUnit.MINUTES);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> R.success()</span><br><span class="line">        .put(<span class="string">&quot;avgScore&quot;</span>, Math.round(avgScore * <span class="number">10</span>) / <span class="number">10.0</span>)</span><br><span class="line">        .put(<span class="string">&quot;totalRatings&quot;</span>, ratingMapper.countBySong(id));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-3-协同过滤推荐"><a href="#2-3-协同过滤推荐" class="headerlink" title="2.3 协同过滤推荐"></a>2.3 协同过滤推荐</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RecommendService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> List&lt;Long&gt; <span class="title function_">recommend</span><span class="params">(Long userId, <span class="type">int</span> topN)</span> &#123;</span><br><span class="line">        <span class="comment">// Redis缓存优先</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">cacheKey</span> <span class="operator">=</span> <span class="string">&quot;recommend:user:&quot;</span> + userId + <span class="string">&quot;:top&quot;</span> + topN;</span><br><span class="line">        List&lt;Object&gt; cached = redisTemplate.opsForList().range(cacheKey, <span class="number">0</span>, -<span class="number">1</span>);</span><br><span class="line">        <span class="keyword">if</span> (cached != <span class="literal">null</span> &amp;&amp; !cached.isEmpty()) &#123;</span><br><span class="line">            <span class="keyword">return</span> cached.stream().map(o -&gt; Long.valueOf(o.toString())).collect(Collectors.toList());</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        List&lt;Rating&gt; userRatings = ratingMapper.findByUserId(userId);</span><br><span class="line">        <span class="keyword">if</span> (userRatings.isEmpty()) &#123;</span><br><span class="line">            <span class="keyword">return</span> recommendHotSongs(topN);  <span class="comment">// 冷启动</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Item-based 协同过滤</span></span><br><span class="line">        List&lt;Long&gt; recommendations = itemBasedRecommend(userRatings, topN);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 缓存</span></span><br><span class="line">        redisTemplate.opsForList().rightPushAll(cacheKey,</span><br><span class="line">            recommendations.stream().map(String::valueOf).collect(Collectors.toList()));</span><br><span class="line">        redisTemplate.expire(cacheKey, <span class="number">1</span>, TimeUnit.HOURS);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> recommendations;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-4-评论系统（树形结构）"><a href="#2-4-评论系统（树形结构）" class="headerlink" title="2.4 评论系统（树形结构）"></a>2.4 评论系统（树形结构）</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/comment/song/&#123;songId&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">getComments</span><span class="params">(<span class="meta">@PathVariable</span> Long songId)</span> &#123;</span><br><span class="line">    List&lt;Comment&gt; comments = commentMapper.findBySongId(songId);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 构建评论树（支持回复）</span></span><br><span class="line">    Map&lt;Long, Comment&gt; commentMap = comments.stream()</span><br><span class="line">        .collect(Collectors.toMap(Comment::getId, c -&gt; c));</span><br><span class="line">    </span><br><span class="line">    List&lt;Comment&gt; tree = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span> (Comment comment : comments) &#123;</span><br><span class="line">        <span class="keyword">if</span> (comment.getParentId() == <span class="literal">null</span> || comment.getParentId() == <span class="number">0</span>) &#123;</span><br><span class="line">            tree.add(comment);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="type">Comment</span> <span class="variable">parent</span> <span class="operator">=</span> commentMap.get(comment.getParentId());</span><br><span class="line">            <span class="keyword">if</span> (parent != <span class="literal">null</span>) parent.getReplies().add(comment);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> R.success(tree);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="第三章：钢铁工厂全栈展示系统"><a href="#第三章：钢铁工厂全栈展示系统" class="headerlink" title="第三章：钢铁工厂全栈展示系统"></a>第三章：钢铁工厂全栈展示系统</h2><h3 id="3-1-登录频率限制"><a href="#3-1-登录频率限制" class="headerlink" title="3.1 登录频率限制"></a>3.1 登录频率限制</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">LOGIN_RATE_LIMIT_SECONDS</span> <span class="operator">=</span> <span class="number">2</span> * <span class="number">60</span> * <span class="number">60</span>; <span class="comment">// 2小时</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Admin <span class="title function_">login</span><span class="params">(Account account, HttpServletRequest request)</span> &#123;</span><br><span class="line">    <span class="comment">// ★ 登录频率限制</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">loginRateKey</span> <span class="operator">=</span> <span class="string">&quot;login_rate:admin:&quot;</span> + account.getUsername();</span><br><span class="line">    <span class="type">Object</span> <span class="variable">lastLoginTime</span> <span class="operator">=</span> redisTemplate.opsForValue().get(loginRateKey);</span><br><span class="line">    <span class="keyword">if</span> (lastLoginTime != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="type">long</span> <span class="variable">timeDiff</span> <span class="operator">=</span> (System.currentTimeMillis() </span><br><span class="line">            - Long.parseLong(lastLoginTime.toString())) / <span class="number">1000</span>;</span><br><span class="line">        <span class="keyword">if</span> (timeDiff &lt; LOGIN_RATE_LIMIT_SECONDS) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">CustomerException</span>(<span class="string">&quot;登录过于频繁，请2小时后再试&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// BCrypt密码验证</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">isValid</span> <span class="operator">=</span> passwordEncoder.matches(</span><br><span class="line">        account.getPassword(), dbAdmin.getPassword());</span><br><span class="line">    <span class="keyword">if</span> (!isValid) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">CustomerException</span>(<span class="string">&quot;账号或密码错误&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 记录登录时间</span></span><br><span class="line">    redisTemplate.opsForValue().set(loginRateKey, </span><br><span class="line">        System.currentTimeMillis(), LOGIN_RATE_LIMIT_SECONDS, TimeUnit.SECONDS);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 生成Token</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">clientIp</span> <span class="operator">=</span> getClientIp(request);</span><br><span class="line">    <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> TokenUtils.createToken(</span><br><span class="line">        dbAdmin.getId() + <span class="string">&quot;-ADMIN&quot;</span>, dbAdmin.getPassword(), clientIp);</span><br><span class="line">    dbAdmin.setToken(token);</span><br><span class="line">    <span class="keyword">return</span> dbAdmin;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-2-中文全文搜索"><a href="#3-2-中文全文搜索" class="headerlink" title="3.2 中文全文搜索"></a>3.2 中文全文搜索</h3><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- Flyway迁移：添加ngram全文索引</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> `obtain_information` </span><br><span class="line"><span class="keyword">ADD</span> FULLTEXT INDEX `ft_title_content` (`title`, `content`) </span><br><span class="line"><span class="keyword">WITH</span> PARSER ngram;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Mapper</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">NewsMapper</span> &#123;</span><br><span class="line">    <span class="meta">@Select(&quot;SELECT * FROM obtain_information &quot; +</span></span><br><span class="line"><span class="meta">            &quot;WHERE MATCH(title, content) &quot; +</span></span><br><span class="line"><span class="meta">            &quot;AGAINST(#&#123;keyword&#125; IN NATURAL LANGUAGE MODE) &quot; +</span></span><br><span class="line"><span class="meta">            &quot;LIMIT #&#123;offset&#125;, #&#123;limit&#125;&quot;)</span></span><br><span class="line">    List&lt;News&gt; <span class="title function_">search</span><span class="params">(<span class="meta">@Param(&quot;keyword&quot;)</span> String keyword,</span></span><br><span class="line"><span class="params">                       <span class="meta">@Param(&quot;offset&quot;)</span> <span class="type">int</span> offset, <span class="meta">@Param(&quot;limit&quot;)</span> <span class="type">int</span> limit)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-3-微服务版-UniApp-小程序"><a href="#3-3-微服务版-UniApp-小程序" class="headerlink" title="3.3 微服务版 + UniApp 小程序"></a>3.3 微服务版 + UniApp 小程序</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">steel-pipe-factory-microservices/</span><br><span class="line">├── common/           # 公共模块</span><br><span class="line">├── api-gateway/     # API网关 (8080)</span><br><span class="line">├── user-service/    # 用户服务 (8081, 库steel_user_db)</span><br><span class="line">├── product-service/ # 产品服务 (8082, 库steel_product_db)</span><br><span class="line">├── content-service/ # 内容服务 (8083, 库steel_content_db)</span><br><span class="line">├── file-service/    # 文件服务 (8084)</span><br><span class="line">└── migrate-miniprogram/  # UniApp微信小程序</span><br></pre></td></tr></table></figure><h3 id="3-4-UniApp-小程序集成"><a href="#3-4-UniApp-小程序集成" class="headerlink" title="3.4 UniApp 小程序集成"></a>3.4 UniApp 小程序集成</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// HTTP请求封装</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">HttpRequest</span> &#123;</span><br><span class="line">    request&lt;T&gt;(<span class="attr">options</span>: <span class="title class_">RequestOptions</span>): <span class="title class_">Promise</span>&lt;<span class="title class_">ResponseData</span>&lt;T&gt;&gt; &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">            uni.<span class="title function_">request</span>(&#123;</span><br><span class="line">                <span class="attr">url</span>: <span class="string">`<span class="subst">$&#123;<span class="variable language_">this</span>.baseURL&#125;</span><span class="subst">$&#123;config.url&#125;</span>`</span>,</span><br><span class="line">                <span class="attr">method</span>: config.<span class="property">method</span> || <span class="string">&#x27;GET&#x27;</span>,</span><br><span class="line">                <span class="attr">data</span>: config.<span class="property">data</span>,</span><br><span class="line">                <span class="attr">header</span>: config.<span class="property">header</span>,</span><br><span class="line">                <span class="attr">success</span>: <span class="function">(<span class="params">res</span>) =&gt;</span> &#123;</span><br><span class="line">                    <span class="keyword">switch</span> (res.<span class="property">statusCode</span>) &#123;</span><br><span class="line">                        <span class="keyword">case</span> <span class="number">200</span>: <span class="title function_">resolve</span>(res.<span class="property">data</span>); <span class="keyword">break</span>;</span><br><span class="line">                        <span class="keyword">case</span> <span class="number">401</span>: uni.<span class="title function_">showToast</span>(&#123; <span class="attr">title</span>: <span class="string">&#x27;登录已过期&#x27;</span>, <span class="attr">icon</span>: <span class="string">&#x27;none&#x27;</span> &#125;); <span class="keyword">break</span>;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;,</span><br><span class="line">                <span class="attr">fail</span>: <span class="function">(<span class="params">err</span>) =&gt;</span> <span class="title function_">reject</span>(<span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;网络请求失败&#x27;</span>))</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 后端UniApp专用接口</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/uniapp&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UniappController</span> &#123;</span><br><span class="line">    <span class="meta">@GetMapping(&quot;/getSteelPriceData&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> R <span class="title function_">getSteelPriceData</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 通过@LoadBalanced RestTemplate调用product-service</span></span><br><span class="line">        <span class="type">Object</span> <span class="variable">data</span> <span class="operator">=</span> restTemplate.getForObject(</span><br><span class="line">            <span class="string">&quot;http://product-service/steelProductPrice/selectAll&quot;</span>, Object.class);</span><br><span class="line">        <span class="keyword">return</span> R.success(data);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="第四章：单体-vs-微服务架构对比"><a href="#第四章：单体-vs-微服务架构对比" class="headerlink" title="第四章：单体 vs 微服务架构对比"></a>第四章：单体 vs 微服务架构对比</h2><h3 id="4-1-对比基础"><a href="#4-1-对比基础" class="headerlink" title="4.1 对比基础"></a>4.1 对比基础</h3><table><thead><tr><th>业务</th><th>单体版</th><th>微服务版</th><th>服务数</th></tr></thead><tbody><tr><td>社区团购</td><td>community-group-buying-system</td><td>communitygroupbuyingsystemMicroserviceCase</td><td>6</td></tr><tr><td>钢铁工厂</td><td>steel-factory-fullstack</td><td>steel-pipe-factory-microservices</td><td>4</td></tr></tbody></table><h3 id="4-2-代码量对比"><a href="#4-2-代码量对比" class="headerlink" title="4.2 代码量对比"></a>4.2 代码量对比</h3><table><thead><tr><th>维度</th><th>单体版</th><th>微服务版</th><th>变化</th></tr></thead><tbody><tr><td>Controller数</td><td>15</td><td>~18</td><td>+20%</td></tr><tr><td>数据库数</td><td>1</td><td>5</td><td>+400%</td></tr><tr><td>配置文件</td><td>1个yml</td><td>Nacos + 每服务1个yml</td><td>+500%</td></tr><tr><td>中间件</td><td>MySQL + Redis</td><td>+Nacos + Seata + RocketMQ</td><td>+3个</td></tr></tbody></table><h3 id="4-3-创建订单对比"><a href="#4-3-创建订单对比" class="headerlink" title="4.3 创建订单对比"></a>4.3 创建订单对比</h3><p><strong>单体版</strong>（4行代码，一个事务）：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transactional(rollbackFor = Exception.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">createOrder</span><span class="params">(OrdersEntity entity)</span> &#123;</span><br><span class="line">    productDao.decreaseStock(entity.getProductId(), entity.getQuantity());</span><br><span class="line">    ordersDao.insert(entity);</span><br><span class="line">    yonghuDao.addPoints(entity.getUserId(), entity.getTotalPrice().doubleValue());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>微服务版</strong>（涉及Feign+Seata+RocketMQ三个组件）：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GlobalTransactional(name = &quot;cgb-create-order&quot;, rollbackFor = Exception.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">createOrder</span><span class="params">(OrdersEntity entity)</span> &#123;</span><br><span class="line">    <span class="type">var</span> <span class="variable">stockResult</span> <span class="operator">=</span> feignProductService.decreaseStock(</span><br><span class="line">        entity.getProductId(), entity.getQuantity());</span><br><span class="line">    <span class="keyword">if</span> (stockResult.getCode() != <span class="number">0</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">EIException</span>(<span class="string">&quot;库存扣减失败&quot;</span>);</span><br><span class="line">    ordersDao.insert(entity);</span><br><span class="line">    sendOrderStatusMessage(entity, MQTopics.TAG_ORDER_CREATED);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-4-部署复杂度"><a href="#4-4-部署复杂度" class="headerlink" title="4.4 部署复杂度"></a>4.4 部署复杂度</h3><p><strong>单体版</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mvn package</span><br><span class="line">java -jar app.jar</span><br></pre></td></tr></table></figure><p><strong>微服务版</strong>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">nacos:</span>        <span class="comment"># 必须先启动</span></span><br><span class="line">  <span class="attr">seata-server:</span>  <span class="comment"># 必须在nacos之后</span></span><br><span class="line">  <span class="attr">rocketmq-namesrv:</span></span><br><span class="line">  <span class="attr">rocketmq-broker:</span></span><br><span class="line">  <span class="attr">mysql:</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">  <span class="attr">cgb-gateway:</span></span><br><span class="line">  <span class="attr">cgb-user-service:</span></span><br><span class="line">  <span class="attr">cgb-product-service:</span></span><br><span class="line">  <span class="comment"># ... 4 more services</span></span><br></pre></td></tr></table></figure><h3 id="4-5-何时该拆？"><a href="#4-5-何时该拆？" class="headerlink" title="4.5 何时该拆？"></a>4.5 何时该拆？</h3><table><thead><tr><th>信号</th><th>说明</th><th>社区团购满足？</th><th>钢铁工厂满足？</th></tr></thead><tbody><tr><td>团队 &gt; 5人</td><td>需要独立 ownership</td><td>❌</td><td>❌</td></tr><tr><td>QPS &gt; 1000</td><td>需要独立扩展</td><td>❌</td><td>❌</td></tr><tr><td>独立部署需求</td><td>改A不影响B</td><td>❌</td><td>✅（小程序需求）</td></tr><tr><td>数据库瓶颈</td><td>单库连接池不够</td><td>❌</td><td>❌</td></tr></tbody></table><p><strong>社区团购不需要微服务</strong>——毕设场景，单体完全够用。<strong>钢铁工厂微服务有合理性</strong>——小程序+管理端需要独立路由和安全策略。</p><h3 id="4-6-资源消耗对比"><a href="#4-6-资源消耗对比" class="headerlink" title="4.6 资源消耗对比"></a>4.6 资源消耗对比</h3><table><thead><tr><th>资源</th><th>单体版</th><th>微服务版</th></tr></thead><tbody><tr><td>JVM进程</td><td>1</td><td>6+</td></tr><tr><td>内存</td><td>~512MB</td><td>~2GB+</td></tr><tr><td>数据库连接</td><td>1个池(20连接)</td><td>5个池(100连接)</td></tr><tr><td>Docker容器</td><td>3</td><td>10+</td></tr></tbody></table><hr><h2 id="第五章：架构选型决策框架"><a href="#第五章：架构选型决策框架" class="headerlink" title="第五章：架构选型决策框架"></a>第五章：架构选型决策框架</h2><h3 id="5-1-三种架构对比"><a href="#5-1-三种架构对比" class="headerlink" title="5.1 三种架构对比"></a>5.1 三种架构对比</h3><table><thead><tr><th>维度</th><th>单体</th><th>模块化单体</th><th>微服务</th></tr></thead><tbody><tr><td>开发效率</td><td>高</td><td>高</td><td>中</td></tr><tr><td>运维复杂度</td><td>低</td><td>低</td><td>高</td></tr><tr><td>可扩展性</td><td>低</td><td>中</td><td>高</td></tr><tr><td>故障隔离</td><td>差</td><td>差</td><td>好</td></tr><tr><td>团队协作</td><td>差</td><td>中</td><td>好</td></tr><tr><td>适合规模</td><td>小型</td><td>中型</td><td>大型</td></tr></tbody></table><h3 id="5-2-选型决策树"><a href="#5-2-选型决策树" class="headerlink" title="5.2 选型决策树"></a>5.2 选型决策树</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">团队 &lt; 5人？</span><br><span class="line">├── 是 → QPS &lt; 1000？</span><br><span class="line">│   ├── 是 → 单体（不需要微服务）</span><br><span class="line">│   └── 否 → 模块化单体 + 按需拆分热点模块</span><br><span class="line">└── 否 → 多团队协作？</span><br><span class="line">    ├── 是 → 微服务（按业务域拆分）</span><br><span class="line">    └── 否 → 模块化单体</span><br></pre></td></tr></table></figure><h3 id="5-3-行业项目的技术选型"><a href="#5-3-行业项目的技术选型" class="headerlink" title="5.3 行业项目的技术选型"></a>5.3 行业项目的技术选型</h3><table><thead><tr><th>项目</th><th>架构</th><th>选型理由</th></tr></thead><tbody><tr><td>就业平台</td><td>单体</td><td>三角色系统，QPS低，单体够用</td></tr><tr><td>音乐网站</td><td>单体</td><td>141个测试，协同过滤推荐，单体够用</td></tr><tr><td>钢铁工厂单体版</td><td>单体</td><td>展示系统，QPS低</td></tr><tr><td>钢铁工厂微服务版</td><td>微服务</td><td>小程序+管理端需要独立路由</td></tr><tr><td>社区团购单体版</td><td>单体</td><td>毕设项目，单体够用</td></tr><tr><td>社区团购微服务版</td><td>微服务</td><td>教学演示，展示微服务架构</td></tr></tbody></table><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><table><thead><tr><th>项目</th><th>核心挑战</th><th>技术方案</th></tr></thead><tbody><tr><td>就业平台</td><td>三角色权限+数据隔离</td><td>type字段区分 + 查询条件过滤</td></tr><tr><td>音乐网站</td><td>推荐算法+评分系统</td><td>Item-based协同过滤 + Redis缓存</td></tr><tr><td>钢铁工厂</td><td>中文搜索+登录频率限制</td><td>ngram全文索引 + Redis登录时间</td></tr><tr><td>钢铁工厂微服务</td><td>小程序+管理端独立</td><td>4微服务+3独立DB+UniApp</td></tr></tbody></table><p><strong>核心思想</strong>：没有最好的架构，只有最适合的架构。单体没有错——当业务复杂度增长到单体无法承受时，拆分才有价值。拆分是为了解决真实痛点，不是为了”看起来更高级”。</p>]]>
    </content>
    <id>http://example.com/2026/07/15/20260715/2026071505/</id>
    <link href="http://example.com/2026/07/15/20260715/2026071505/"/>
    <published>2026-07-15T01:08:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>就业平台的三角色系统、音乐网站的协同过滤推荐、钢铁工厂的中文搜索——三个不同行业的全栈项目，每个都有独特的业务挑战。再加上两组同业务双版本（单体vs微服务）的架构对比，本文把行业实战和架构选型的完整图景讲透。</p>
</blockquote>
<]]>
    </summary>
    <title>行业全栈项目实战与架构选型</title>
    <updated>2026-07-15T01:31:25.029Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="Java" scheme="http://example.com/categories/Java/"/>
    <category term="网络安全" scheme="http://example.com/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="GodzillaX" scheme="http://example.com/tags/GodzillaX/"/>
    <content>
      <![CDATA[<h1 id=""><a href="#" class="headerlink" title=""></a></h1><blockquote><p><em>本文以防御视角讨论安全测试工具的架构设计，所有内容仅用于授权渗透测试和安全研究。</em></p></blockquote><p>WebShell 管理工具是渗透测试中验证 Web 漏洞利用链的关键组件。AES 加密是数据保护的基础。Javassist 字节码操作是理解运行时注入的关键。本文从安全研究角度，把加密通信和字节码操作的完整知识体系讲透。</p><hr><h2 id="第一章：WebShell-管理工具架构演进"><a href="#第一章：WebShell-管理工具架构演进" class="headerlink" title="第一章：WebShell 管理工具架构演进"></a>第一章：WebShell 管理工具架构演进</h2><h3 id="1-1-桌面版（GodzillaX）"><a href="#1-1-桌面版（GodzillaX）" class="headerlink" title="1.1 桌面版（GodzillaX）"></a>1.1 桌面版（GodzillaX）</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">┌──────────────────────────────────────┐</span><br><span class="line">│         Java Swing + FlatLaf        │</span><br><span class="line">│  ┌─────────┐ ┌──────────────────┐   │</span><br><span class="line">│  │ 连接管理  │ │   插件系统(47+)   │   │</span><br><span class="line">│  │         │ │ SocksProxy       │   │</span><br><span class="line">│  │ 目标树   │ │ Meterpreter      │   │</span><br><span class="line">│  │         │ │ MemoryShell      │   │</span><br><span class="line">│  │ 终端模拟  │ │ RealCmd          │   │</span><br><span class="line">│  └─────────┘ └──────────────────┘   │</span><br><span class="line">│         SQLite (本地存储)            │</span><br><span class="line">└──────────────────────────────────────┘</span><br></pre></td></tr></table></figure><h3 id="1-2-Web版（GodzillaWeb）"><a href="#1-2-Web版（GodzillaWeb）" class="headerlink" title="1.2 Web版（GodzillaWeb）"></a>1.2 Web版（GodzillaWeb）</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">┌────────────┐     HTTP      ┌─────────────────┐</span><br><span class="line">│  Vue 3 前端 │ ────────────&gt; │ Spring Boot 后端 │</span><br><span class="line">│  管理界面   │ &lt;──────────── │  22个REST API    │</span><br><span class="line">└────────────┘               │  插件系统(8/51)   │</span><br><span class="line">                             │  SQLite + JPA    │</span><br><span class="line">                             └─────────────────┘</span><br></pre></td></tr></table></figure><p><strong>演进动机</strong>：桌面版无法多人协作 → Web版支持多用户；桌面版更新需重新分发 → Web版热更新。</p><h3 id="1-3-插件系统架构"><a href="#1-3-插件系统架构" class="headerlink" title="1.3 插件系统架构"></a>1.3 插件系统架构</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 插件接口</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Plugin</span> &#123;</span><br><span class="line">    String <span class="title function_">getName</span><span class="params">()</span>;</span><br><span class="line">    String <span class="title function_">getDescription</span><span class="params">()</span>;</span><br><span class="line">    PluginType <span class="title function_">getType</span><span class="params">()</span>;  <span class="comment">// COMMAND/PROXY/TUNNEL/MEMORY_SHELL</span></span><br><span class="line">    String <span class="title function_">execute</span><span class="params">(ShellEntity shell, Map&lt;String, Object&gt; params)</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 插件管理（Java SPI机制）</span></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PluginManager</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Map&lt;String, Plugin&gt; plugins = <span class="keyword">new</span> <span class="title class_">ConcurrentHashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 动态加载插件JAR</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">loadPlugin</span><span class="params">(File jarFile)</span> &#123;</span><br><span class="line">        <span class="type">URLClassLoader</span> <span class="variable">loader</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">URLClassLoader</span>(</span><br><span class="line">            <span class="keyword">new</span> <span class="title class_">URL</span>[]&#123;jarFile.toURI().toURL()&#125;);</span><br><span class="line">        ServiceLoader&lt;Plugin&gt; serviceLoader = </span><br><span class="line">            ServiceLoader.load(Plugin.class, loader);</span><br><span class="line">        <span class="keyword">for</span> (Plugin plugin : serviceLoader) &#123;</span><br><span class="line">            plugins.put(plugin.getName(), plugin);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="第二章：PBKDF2-AES-256-GCM-加密"><a href="#第二章：PBKDF2-AES-256-GCM-加密" class="headerlink" title="第二章：PBKDF2 + AES-256-GCM 加密"></a>第二章：PBKDF2 + AES-256-GCM 加密</h2><h3 id="2-1-加密方案全景"><a href="#2-1-加密方案全景" class="headerlink" title="2.1 加密方案全景"></a>2.1 加密方案全景</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">用户密码(明文)</span><br><span class="line">    │</span><br><span class="line">    ▼ PBKDF2密钥派生</span><br><span class="line">    │ + 随机盐 + 65536次迭代</span><br><span class="line">    │</span><br><span class="line">AES-256密钥</span><br><span class="line">    │</span><br><span class="line">    ▼ AES-256-GCM加密</span><br><span class="line">    │ + 随机IV + 附加认证数据</span><br><span class="line">    │</span><br><span class="line">密文 + IV + 认证标签</span><br><span class="line">    │</span><br><span class="line">    ▼ Base64编码</span><br><span class="line">    │</span><br><span class="line">可存储/传输的加密字符串</span><br></pre></td></tr></table></figure><h3 id="2-2-PBKDF2-密钥派生"><a href="#2-2-PBKDF2-密钥派生" class="headerlink" title="2.2 PBKDF2 密钥派生"></a>2.2 PBKDF2 密钥派生</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">KeyDerivationUtil</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">ITERATIONS</span> <span class="operator">=</span> <span class="number">65536</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">KEY_LENGTH</span> <span class="operator">=</span> <span class="number">256</span>;  <span class="comment">// 256位</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">ALGORITHM</span> <span class="operator">=</span> <span class="string">&quot;PBKDF2WithHmacSHA256&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> SecretKey <span class="title function_">deriveKey</span><span class="params">(<span class="type">char</span>[] password, <span class="type">byte</span>[] salt)</span> </span><br><span class="line">            <span class="keyword">throws</span> GeneralSecurityException &#123;</span><br><span class="line">        <span class="type">PBEKeySpec</span> <span class="variable">spec</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PBEKeySpec</span>(password, salt, ITERATIONS, KEY_LENGTH);</span><br><span class="line">        <span class="type">SecretKeyFactory</span> <span class="variable">factory</span> <span class="operator">=</span> SecretKeyFactory.getInstance(ALGORITHM);</span><br><span class="line">        <span class="type">SecretKey</span> <span class="variable">tmp</span> <span class="operator">=</span> factory.generateSecret(spec);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SecretKeySpec</span>(tmp.getEncoded(), <span class="string">&quot;AES&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">byte</span>[] generateSalt() &#123;</span><br><span class="line">        <span class="type">byte</span>[] salt = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">16</span>];  <span class="comment">// 128位盐</span></span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">SecureRandom</span>().nextBytes(salt);</span><br><span class="line">        <span class="keyword">return</span> salt;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>迭代次数的作用</strong>：65536 次迭代约 100ms。攻击者暴力破解每个密码需要 100ms，大幅增加成本。</p><h3 id="2-3-AES-256-GCM-加密"><a href="#2-3-AES-256-GCM-加密" class="headerlink" title="2.3 AES-256-GCM 加密"></a>2.3 AES-256-GCM 加密</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AesGcmCrypto</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">TRANSFORMATION</span> <span class="operator">=</span> <span class="string">&quot;AES/GCM/NoPadding&quot;</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">IV_LENGTH</span> <span class="operator">=</span> <span class="number">12</span>;     <span class="comment">// 96位IV</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">TAG_LENGTH</span> <span class="operator">=</span> <span class="number">128</span>;   <span class="comment">// 128位认证标签</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">byte</span>[] encrypt(<span class="type">byte</span>[] plaintext, SecretKey key, </span><br><span class="line">                                 <span class="type">byte</span>[] iv, <span class="type">byte</span>[] aad) <span class="keyword">throws</span> GeneralSecurityException &#123;</span><br><span class="line">        <span class="type">Cipher</span> <span class="variable">cipher</span> <span class="operator">=</span> Cipher.getInstance(TRANSFORMATION);</span><br><span class="line">        <span class="type">GCMParameterSpec</span> <span class="variable">spec</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">GCMParameterSpec</span>(TAG_LENGTH, iv);</span><br><span class="line">        cipher.init(Cipher.ENCRYPT_MODE, key, spec);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> (aad != <span class="literal">null</span>) cipher.updateAAD(aad);  <span class="comment">// 附加认证数据</span></span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> cipher.doFinal(plaintext);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">byte</span>[] decrypt(<span class="type">byte</span>[] ciphertext, SecretKey key, </span><br><span class="line">                                 <span class="type">byte</span>[] iv, <span class="type">byte</span>[] aad) <span class="keyword">throws</span> GeneralSecurityException &#123;</span><br><span class="line">        <span class="type">Cipher</span> <span class="variable">cipher</span> <span class="operator">=</span> Cipher.getInstance(TRANSFORMATION);</span><br><span class="line">        <span class="type">GCMParameterSpec</span> <span class="variable">spec</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">GCMParameterSpec</span>(TAG_LENGTH, iv);</span><br><span class="line">        cipher.init(Cipher.DECRYPT_MODE, key, spec);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> (aad != <span class="literal">null</span>) cipher.updateAAD(aad);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> cipher.doFinal(ciphertext);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">byte</span>[] generateIV() &#123;</span><br><span class="line">        <span class="type">byte</span>[] iv = <span class="keyword">new</span> <span class="title class_">byte</span>[IV_LENGTH];</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">SecureRandom</span>().nextBytes(iv);</span><br><span class="line">        <span class="keyword">return</span> iv;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-4-GCM-vs-CBC-vs-ECB"><a href="#2-4-GCM-vs-CBC-vs-ECB" class="headerlink" title="2.4 GCM vs CBC vs ECB"></a>2.4 GCM vs CBC vs ECB</h3><table><thead><tr><th>模式</th><th>认证</th><th>并行</th><th>IV要求</th><th>安全性</th></tr></thead><tbody><tr><td>ECB</td><td>❌</td><td>✅</td><td>无</td><td>❌ 不安全</td></tr><tr><td>CBC</td><td>❌</td><td>❌</td><td>不可重复</td><td>中</td></tr><tr><td><strong>GCM</strong></td><td>✅</td><td>✅</td><td>不可重复</td><td>✅ 推荐</td></tr></tbody></table><p><strong>GCM &#x3D; Galois&#x2F;Counter Mode</strong>：加密的同时生成认证标签（MAC），任何篡改都会导致解密失败。</p><h3 id="2-5-完整加密-解密流程"><a href="#2-5-完整加密-解密流程" class="headerlink" title="2.5 完整加密&#x2F;解密流程"></a>2.5 完整加密&#x2F;解密流程</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CryptoService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">encrypt</span><span class="params">(String plaintext, String password)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">byte</span>[] salt = KeyDerivationUtil.generateSalt();</span><br><span class="line">            <span class="type">SecretKey</span> <span class="variable">key</span> <span class="operator">=</span> KeyDerivationUtil.deriveKey(password.toCharArray(), salt);</span><br><span class="line">            <span class="type">byte</span>[] iv = AesGcmCrypto.generateIV();</span><br><span class="line">            <span class="type">byte</span>[] encrypted = AesGcmCrypto.encrypt(</span><br><span class="line">                plaintext.getBytes(StandardCharsets.UTF_8), key, iv, <span class="literal">null</span>);</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 拼接 salt + iv + ciphertext</span></span><br><span class="line">            <span class="type">ByteBuffer</span> <span class="variable">buffer</span> <span class="operator">=</span> ByteBuffer.allocate(</span><br><span class="line">                salt.length + iv.length + encrypted.length);</span><br><span class="line">            buffer.put(salt);</span><br><span class="line">            buffer.put(iv);</span><br><span class="line">            buffer.put(encrypted);</span><br><span class="line">            </span><br><span class="line">            <span class="keyword">return</span> Base64.getEncoder().encodeToString(buffer.array());</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;加密失败&quot;</span>, e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">decrypt</span><span class="params">(String encryptedStr, String password)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">byte</span>[] data = Base64.getDecoder().decode(encryptedStr);</span><br><span class="line">            <span class="type">ByteBuffer</span> <span class="variable">buffer</span> <span class="operator">=</span> ByteBuffer.wrap(data);</span><br><span class="line">            </span><br><span class="line">            <span class="type">byte</span>[] salt = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">16</span>]; buffer.get(salt);</span><br><span class="line">            <span class="type">byte</span>[] iv = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">12</span>]; buffer.get(iv);</span><br><span class="line">            <span class="type">byte</span>[] ciphertext = <span class="keyword">new</span> <span class="title class_">byte</span>[buffer.remaining()]; buffer.get(ciphertext);</span><br><span class="line">            </span><br><span class="line">            <span class="type">SecretKey</span> <span class="variable">key</span> <span class="operator">=</span> KeyDerivationUtil.deriveKey(password.toCharArray(), salt);</span><br><span class="line">            <span class="type">byte</span>[] decrypted = AesGcmCrypto.decrypt(ciphertext, key, iv, <span class="literal">null</span>);</span><br><span class="line">            </span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">String</span>(decrypted, StandardCharsets.UTF_8);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;解密失败&quot;</span>, e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-6-常见安全陷阱"><a href="#2-6-常见安全陷阱" class="headerlink" title="2.6 常见安全陷阱"></a>2.6 常见安全陷阱</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ IV固定 → 相同明文加密结果相同，暴露数据模式</span></span><br><span class="line"><span class="type">byte</span>[] iv = <span class="string">&quot;123456789012&quot;</span>.getBytes();</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ IV每次随机</span></span><br><span class="line"><span class="type">byte</span>[] iv = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">12</span>];</span><br><span class="line"><span class="keyword">new</span> <span class="title class_">SecureRandom</span>().nextBytes(iv);</span><br><span class="line"></span><br><span class="line"><span class="comment">// ❌ ECB模式 → 相同明文块加密结果相同</span></span><br><span class="line"><span class="type">Cipher</span> <span class="variable">cipher</span> <span class="operator">=</span> Cipher.getInstance(<span class="string">&quot;AES/ECB/PKCS5Padding&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ GCM模式</span></span><br><span class="line"><span class="type">Cipher</span> <span class="variable">cipher</span> <span class="operator">=</span> Cipher.getInstance(<span class="string">&quot;AES/GCM/NoPadding&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// ❌ 密码直接做密钥 → 长度不够、熵不足</span></span><br><span class="line"><span class="type">byte</span>[] key = password.getBytes();</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ PBKDF2派生</span></span><br><span class="line"><span class="type">SecretKey</span> <span class="variable">key</span> <span class="operator">=</span> KeyDerivationUtil.deriveKey(password.toCharArray(), salt);</span><br></pre></td></tr></table></figure><hr><h2 id="第三章：Javassist-字节码操作"><a href="#第三章：Javassist-字节码操作" class="headerlink" title="第三章：Javassist 字节码操作"></a>第三章：Javassist 字节码操作</h2><h3 id="3-1-运行时给方法加日志"><a href="#3-1-运行时给方法加日志" class="headerlink" title="3.1 运行时给方法加日志"></a>3.1 运行时给方法加日志</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LoggingInjector</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">injectLogging</span><span class="params">(Class&lt;?&gt; targetClass, String methodName)</span> </span><br><span class="line">            <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="type">ClassPool</span> <span class="variable">pool</span> <span class="operator">=</span> ClassPool.getDefault();</span><br><span class="line">        <span class="type">CtClass</span> <span class="variable">ctClass</span> <span class="operator">=</span> pool.get(targetClass.getName());</span><br><span class="line">        </span><br><span class="line">        <span class="type">CtMethod</span> <span class="variable">method</span> <span class="operator">=</span> ctClass.getDeclaredMethod(methodName);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 方法前：记录开始时间</span></span><br><span class="line">        method.insertBefore(<span class="string">&quot;&#123; _startTime = System.currentTimeMillis(); &#125;&quot;</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 方法后：计算耗时</span></span><br><span class="line">        method.insertAfter(</span><br><span class="line">            <span class="string">&quot;&#123; System.out.println(\&quot;方法&quot;</span> + methodName + <span class="string">&quot;耗时: \&quot; + &quot;</span> +</span><br><span class="line">            <span class="string">&quot;(System.currentTimeMillis() - _startTime) + \&quot;ms\&quot;); &#125;&quot;</span></span><br><span class="line">        );</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 添加startTime字段</span></span><br><span class="line">        <span class="type">CtField</span> <span class="variable">startTime</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CtField</span>(CtClass.longType, <span class="string">&quot;_startTime&quot;</span>, ctClass);</span><br><span class="line">        startTime.setModifiers(Modifier.PRIVATE);</span><br><span class="line">        ctClass.addField(startTime);</span><br><span class="line">        </span><br><span class="line">        ctClass.toClass();  <span class="comment">// 热加载到JVM</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-2-动态实现接口"><a href="#3-2-动态实现接口" class="headerlink" title="3.2 动态实现接口"></a>3.2 动态实现接口</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DynamicProxyCreator</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Object <span class="title function_">createProxy</span><span class="params">(Class&lt;?&gt; interfaceClass)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="type">ClassPool</span> <span class="variable">pool</span> <span class="operator">=</span> ClassPool.getDefault();</span><br><span class="line">        <span class="type">CtClass</span> <span class="variable">proxyClass</span> <span class="operator">=</span> pool.makeClass(interfaceClass.getSimpleName() + <span class="string">&quot;Proxy&quot;</span>);</span><br><span class="line">        proxyClass.addInterface(pool.get(interfaceClass.getName()));</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">for</span> (Method method : interfaceClass.getMethods()) &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">methodSrc</span> <span class="operator">=</span> <span class="string">&quot;public &quot;</span> + method.getReturnType().getName() + </span><br><span class="line">                <span class="string">&quot; &quot;</span> + method.getName() + <span class="string">&quot;() &#123;&quot;</span> +</span><br><span class="line">                <span class="string">&quot;  System.out.println(\&quot;调用: &quot;</span> + method.getName() + <span class="string">&quot;\&quot;);&quot;</span> +</span><br><span class="line">                <span class="string">&quot;  return null;&quot;</span> +</span><br><span class="line">                <span class="string">&quot;&#125;&quot;</span>;</span><br><span class="line">            proxyClass.addMethod(CtNewMethod.make(methodSrc, proxyClass));</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        Class&lt;?&gt; proxyClazz = proxyClass.toClass();</span><br><span class="line">        <span class="keyword">return</span> proxyClazz.getDeclaredConstructor().newInstance();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-3-安全检测视角"><a href="#3-3-安全检测视角" class="headerlink" title="3.3 安全检测视角"></a>3.3 安全检测视角</h3><p>从防御角度，理解字节码操作有助于：</p><ul><li><strong>WAF 规则编写</strong>：识别加密流量特征</li><li><strong>入侵检测</strong>：检测异常 Filter&#x2F;Listener 注册行为</li><li><strong>安全审计</strong>：发现系统中的内存马后门</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 检测类是否被运行时修改</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">isClassModified</span><span class="params">(Class&lt;?&gt; clazz)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    <span class="comment">// 从JAR中读取原始字节码 vs JVM中当前字节码</span></span><br><span class="line">    <span class="comment">// 比较差异...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="第四章：桌面版-vs-Web版对比"><a href="#第四章：桌面版-vs-Web版对比" class="headerlink" title="第四章：桌面版 vs Web版对比"></a>第四章：桌面版 vs Web版对比</h2><table><thead><tr><th>维度</th><th>桌面版 (GodzillaX)</th><th>Web版 (GodzillaWeb)</th></tr></thead><tbody><tr><td>UI</td><td>Java Swing + FlatLaf</td><td>Vue 3 + Element Plus</td></tr><tr><td>插件</td><td>URLClassLoader + SPI</td><td>Spring Bean</td></tr><tr><td>加密</td><td>BouncyCastle STEALTH_GCM</td><td>PBKDF2 + AES-256-GCM</td></tr><tr><td>存储</td><td>SQLite JDBC</td><td>Spring Data JPA + SQLite</td></tr><tr><td>终端</td><td>JediTerm</td><td>WebSocket</td></tr><tr><td>部署</td><td>Fat JAR</td><td>Docker</td></tr></tbody></table><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><table><thead><tr><th>技术点</th><th>核心实现</th></tr></thead><tbody><tr><td>密钥派生</td><td>PBKDF2 + 65536次迭代 + 256位密钥</td></tr><tr><td>对称加密</td><td>AES-256-GCM + 随机IV + AAD</td></tr><tr><td>字节码操作</td><td>Javassist <code>insertBefore/insertAfter</code></td></tr><tr><td>插件系统</td><td>Java SPI + URLClassLoader</td></tr></tbody></table><p><strong>核心思想</strong>：安全工具的架构设计和安全防护是硬币的两面——理解攻击工具的加密和注入方式，才能写出更有效的检测和防御规则。</p>]]>
    </content>
    <id>http://example.com/2026/07/15/20260715/2026071504/</id>
    <link href="http://example.com/2026/07/15/20260715/2026071504/"/>
    <published>2026-07-15T00:58:22.000Z</published>
    <summary>
      <![CDATA[<h1 id=""><a href="#" class="headerlink" title=""></a></h1><blockquote>
<p><em>本文以防御视角讨论安全测试工具的架构设计，所有内容仅用于授权渗透测试和安全研究。</em></p>
</blockquot]]>
    </summary>
    <title>安全工具开发：加密通信与字节码操作</title>
    <updated>2026-07-15T01:30:17.579Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="Spring cloud" scheme="http://example.com/categories/Spring-cloud/"/>
    <category term="Java" scheme="http://example.com/categories/Spring-cloud/Java/"/>
    <category term="Spring cloud" scheme="http://example.com/tags/Spring-cloud/"/>
    <category term="微服务" scheme="http://example.com/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    <category term="RuoYi" scheme="http://example.com/tags/RuoYi/"/>
    <content>
      <![CDATA[<h1 id=""><a href="#" class="headerlink" title=""></a></h1><blockquote><p>一个脚手架拆成 12 个 Maven 模块——每个公共能力独立成模块，按需引入。另一个项目用经典五层分层。还有一个用 RuoYi 18 功能模块开箱即用。三种架构设计思路有什么区别？本文把模块化架构的完整知识体系从头到尾讲透。</p></blockquote><hr><h2 id="第一章：为什么要拆模块？"><a href="#第一章：为什么要拆模块？" class="headerlink" title="第一章：为什么要拆模块？"></a>第一章：为什么要拆模块？</h2><h3 id="1-1-单体-vs-模块化"><a href="#1-1-单体-vs-模块化" class="headerlink" title="1.1 单体 vs 模块化"></a>1.1 单体 vs 模块化</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">单体（所有代码在一个module）           模块化（按职责拆分）</span><br><span class="line">┌──────────────────────┐            ┌─────────────┐</span><br><span class="line">│                      │            │ common-core │ ← 基础CRUD</span><br><span class="line">│  所有配置、工具、     │            ├─────────────┤</span><br><span class="line">│  实体、安全、日志     │            │common-cache │ ← Redis</span><br><span class="line">│  全部在一起           │            ├─────────────┤</span><br><span class="line">│                      │            │common-security│ ← JWT</span><br><span class="line">│  问题：              │            ├─────────────┤</span><br><span class="line">│  • 改一个工具要全量   │            │ common-log  │ ← AOP日志</span><br><span class="line">│    编译             │            ├─────────────┤</span><br><span class="line">│  • 引入安全模块就    │            │ common-db   │ ← 数据访问聚合</span><br><span class="line">│    连日志也一起引入   │            ├─────────────┤</span><br><span class="line">│  • 无法按需依赖      │            │ server-auth │ ← 可启动服务</span><br><span class="line">│                      │            └─────────────┘</span><br><span class="line">└──────────────────────┘</span><br></pre></td></tr></table></figure><hr><h2 id="第二章：springboot-core-arch-的-12-模块设计"><a href="#第二章：springboot-core-arch-的-12-模块设计" class="headerlink" title="第二章：springboot-core-arch 的 12 模块设计"></a>第二章：springboot-core-arch 的 12 模块设计</h2><h3 id="2-1-模块清单"><a href="#2-1-模块清单" class="headerlink" title="2.1 模块清单"></a>2.1 模块清单</h3><table><thead><tr><th>模块</th><th>职责</th><th>依赖</th><th>有无代码</th></tr></thead><tbody><tr><td>common-common</td><td>公共基础（注解、枚举、DTO）</td><td>无内部依赖</td><td>有</td></tr><tr><td>common-response</td><td>统一响应封装</td><td>common-common</td><td>有</td></tr><tr><td>common-exception</td><td>异常处理</td><td>common-common, common-response</td><td>有</td></tr><tr><td>common-security</td><td>安全（JWT、过滤器、策略）</td><td>common-common, common-dto, common-entity, common-response, common-exception</td><td>有</td></tr><tr><td>common-entity</td><td>实体类</td><td>common-common, common-security</td><td>有</td></tr><tr><td>common-dto</td><td>数据传输对象</td><td>common-common, common-entity</td><td>有</td></tr><tr><td>common-core</td><td>泛型CRUD基类</td><td>common-response</td><td>有</td></tr><tr><td>common-cache</td><td>Redis配置和工具</td><td>common-common, common-security</td><td>有</td></tr><tr><td>common-log</td><td>AOP审计日志</td><td>common-common, common-security</td><td>有</td></tr><tr><td>common-db</td><td>数据访问聚合</td><td>大部分common模块</td><td>聚合点</td></tr><tr><td>server-auth</td><td>认证服务（可启动）</td><td>common-core, common-db, common-security</td><td>有</td></tr><tr><td>server-user</td><td>用户服务（预留）</td><td>大部分common模块</td><td>仅启动类</td></tr></tbody></table><h3 id="2-2-模块依赖图"><a href="#2-2-模块依赖图" class="headerlink" title="2.2 模块依赖图"></a>2.2 模块依赖图</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">           common-common (最底层)</span><br><span class="line">                │</span><br><span class="line">    ┌───────────┼───────────┐</span><br><span class="line">    ▼           ▼           ▼</span><br><span class="line">common-response common-exception</span><br><span class="line">    │           │           │</span><br><span class="line">    ▼           │           │</span><br><span class="line">common-core     │           │</span><br><span class="line">                ▼           │</span><br><span class="line">         common-security &lt;──┘</span><br><span class="line">                │</span><br><span class="line">    ┌───────────┤</span><br><span class="line">    ▼           ▼</span><br><span class="line">common-entity  common-cache  common-log</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">common-dto</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">common-db (聚合点)</span><br><span class="line">    │</span><br><span class="line">    ├──&gt; server-auth</span><br><span class="line">    └──&gt; server-user</span><br></pre></td></tr></table></figure><p><strong>common-db 是聚合点</strong>：它本身没有代码，只是把所有 common 模块的依赖集中起来，让 server-auth&#x2F;server-user 只需依赖一个 common-db 即可获得全部公共能力。</p><h3 id="2-3-泛型-CRUD-基类（common-core）"><a href="#2-3-泛型-CRUD-基类（common-core）" class="headerlink" title="2.3 泛型 CRUD 基类（common-core）"></a>2.3 泛型 CRUD 基类（common-core）</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">BaseController</span>&lt;T, ID&gt; &#123;</span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">final</span> BaseService&lt;T, ID&gt; baseService;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">protected</span> <span class="title function_">BaseController</span><span class="params">(BaseService&lt;T, ID&gt; baseService)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.baseService = baseService;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> R&lt;T&gt; <span class="title function_">getById</span><span class="params">(<span class="meta">@PathVariable</span> ID id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> R.success(baseService.selectById(id));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping</span></span><br><span class="line">    <span class="keyword">public</span> R&lt;List&lt;T&gt;&gt; <span class="title function_">getAll</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> R.success(baseService.selectAll());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostMapping</span></span><br><span class="line">    <span class="keyword">public</span> R&lt;T&gt; <span class="title function_">create</span><span class="params">(<span class="meta">@RequestBody</span> T entity)</span> &#123;</span><br><span class="line">        baseService.insert(entity);</span><br><span class="line">        <span class="keyword">return</span> R.success(entity);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PutMapping</span></span><br><span class="line">    <span class="keyword">public</span> R&lt;T&gt; <span class="title function_">update</span><span class="params">(<span class="meta">@RequestBody</span> T entity)</span> &#123;</span><br><span class="line">        baseService.update(entity);</span><br><span class="line">        <span class="keyword">return</span> R.success(entity);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@DeleteMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> R&lt;Void&gt; <span class="title function_">delete</span><span class="params">(<span class="meta">@PathVariable</span> ID id)</span> &#123;</span><br><span class="line">        baseService.deleteById(id);</span><br><span class="line">        <span class="keyword">return</span> R.ok();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>使用方式</strong>：子类继承即可获得标准 CRUD 接口：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/admin&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AdminController</span> <span class="keyword">extends</span> <span class="title class_">BaseController</span>&lt;Admin, String&gt; &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">AdminController</span><span class="params">(AdminService service)</span> &#123; <span class="built_in">super</span>(service); &#125;</span><br><span class="line">    <span class="comment">// 自动有 getById, getAll, create, update, delete</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-4-统一响应（common-response）"><a href="#2-4-统一响应（common-response）" class="headerlink" title="2.4 统一响应（common-response）"></a>2.4 统一响应（common-response）</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">R</span>&lt;T&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">boolean</span> success;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> code;</span><br><span class="line">    <span class="keyword">private</span> String message;</span><br><span class="line">    <span class="keyword">private</span> T data;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; R&lt;T&gt; <span class="title function_">success</span><span class="params">(T data)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">R</span>&lt;&gt;(<span class="literal">true</span>, <span class="number">20000</span>, <span class="string">&quot;成功&quot;</span>, data);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">enum</span> <span class="title class_">ResultCodeEnum</span> &#123;</span><br><span class="line">    SUCCESS(<span class="literal">true</span>, <span class="number">20000</span>, <span class="string">&quot;成功&quot;</span>),</span><br><span class="line">    UNAUTHORIZED(<span class="literal">false</span>, <span class="number">40100</span>, <span class="string">&quot;未登录或Token过期&quot;</span>),</span><br><span class="line">    FORBIDDEN(<span class="literal">false</span>, <span class="number">40300</span>, <span class="string">&quot;无权限访问&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-5-安全策略模式（common-security）"><a href="#2-5-安全策略模式（common-security）" class="headerlink" title="2.5 安全策略模式（common-security）"></a>2.5 安全策略模式（common-security）</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 策略接口</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">RoleStrategy</span> &#123;</span><br><span class="line">    String <span class="title function_">getRole</span><span class="params">()</span>;</span><br><span class="line">    LoginResult <span class="title function_">login</span><span class="params">(Account account)</span>;</span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">updatePassword</span><span class="params">(Account account)</span>;</span><br><span class="line">    Account <span class="title function_">selectById</span><span class="params">(String userId)</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 上下文（自动收集所有策略实现）</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RoleStrategyContext</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Map&lt;String, RoleStrategy&gt; strategyMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">RoleStrategyContext</span><span class="params">(List&lt;RoleStrategy&gt; strategies)</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> (RoleStrategy strategy : strategies) &#123;</span><br><span class="line">            strategyMap.put(strategy.getRole().toUpperCase(), strategy);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> RoleStrategy <span class="title function_">getStrategy</span><span class="params">(String roleCode)</span> &#123;</span><br><span class="line">        RoleEnum.fromCode(roleCode);</span><br><span class="line">        <span class="keyword">return</span> strategyMap.get(roleCode.toUpperCase());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-6-Token-自动续期（common-security）"><a href="#2-6-Token-自动续期（common-security）" class="headerlink" title="2.6 Token 自动续期（common-security）"></a>2.6 Token 自动续期（common-security）</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// JwtAuthenticationFilter 中</span></span><br><span class="line"><span class="keyword">if</span> (shouldRenewToken(token, <span class="number">30</span> * <span class="number">60</span> * <span class="number">1000</span>)) &#123;  <span class="comment">// 剩余&lt;30分钟</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">newToken</span> <span class="operator">=</span> createToken(userId + <span class="string">&quot;-&quot;</span> + role, </span><br><span class="line">        account.getPassword(), <span class="number">24</span>);  <span class="comment">// 新Token 24小时</span></span><br><span class="line">    response.setHeader(<span class="string">&quot;Renew-Token&quot;</span>, newToken);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 前端只需检查响应头</span></span><br><span class="line"><span class="type">const</span> <span class="variable">renewToken</span> <span class="operator">=</span> response.headers[<span class="string">&#x27;renew-token&#x27;</span>];</span><br><span class="line"><span class="keyword">if</span> (renewToken) localStorage.setItem(<span class="string">&#x27;token&#x27;</span>, renewToken);</span><br></pre></td></tr></table></figure><h3 id="2-7-AOP-审计日志（common-log）"><a href="#2-7-AOP-审计日志（common-log）" class="headerlink" title="2.7 AOP 审计日志（common-log）"></a>2.7 AOP 审计日志（common-log）</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Aspect</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AuditLogAspect</span> &#123;</span><br><span class="line">    <span class="meta">@Around(&quot;@annotation(auditLogRecord)&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Object <span class="title function_">handleAudit</span><span class="params">(ProceedingJoinPoint joinPoint, </span></span><br><span class="line"><span class="params">                               AuditLogRecord auditLogRecord)</span> <span class="keyword">throws</span> Throwable &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> SecurityContextHolder.getContext()</span><br><span class="line">                .getAuthentication().getName();</span><br><span class="line">        <span class="type">String</span> <span class="variable">ip</span> <span class="operator">=</span> ((ServletRequestAttributes) RequestContextHolder</span><br><span class="line">                .currentRequestAttributes()).getRequest().getRemoteAddr();</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> joinPoint.proceed();</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="type">AuditLog</span> <span class="variable">log</span> <span class="operator">=</span> AuditLog.builder()</span><br><span class="line">                    .username(username)</span><br><span class="line">                    .action(auditLogRecord.action())</span><br><span class="line">                    .resource(auditLogRecord.resource())</span><br><span class="line">                    .ipAddress(ip)</span><br><span class="line">                    .details(Arrays.toString(joinPoint.getArgs()))</span><br><span class="line">                    .build();</span><br><span class="line">            auditLogService.saveLog(log);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-8-常见架构陷阱"><a href="#2-8-常见架构陷阱" class="headerlink" title="2.8 常见架构陷阱"></a>2.8 常见架构陷阱</h3><p><strong>循环依赖</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">common-security 依赖 common-entity（需要Account实体）</span><br><span class="line">common-entity 依赖 common-security（需要Account继承Security的BaseEntity）</span><br><span class="line">    ↑ 循环！Maven编译报错</span><br></pre></td></tr></table></figure><p><strong>解法</strong>：提取公共接口到 common-common。</p><p><strong>两套 JWT 体系并存</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">JwtUtil.java   → subject + roles claim → AuthController使用</span><br><span class="line">TokenUtils.java → audience=&quot;userId-role&quot; → JwtAuthenticationFilter使用</span><br></pre></td></tr></table></figure><p>两套 Token 格式不兼容。<strong>解法</strong>：统一为一套。</p><hr><h2 id="第三章：springbootmultimodule-的五层分层"><a href="#第三章：springbootmultimodule-的五层分层" class="headerlink" title="第三章：springbootmultimodule 的五层分层"></a>第三章：springbootmultimodule 的五层分层</h2><h3 id="3-1-五层结构"><a href="#3-1-五层结构" class="headerlink" title="3.1 五层结构"></a>3.1 五层结构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">┌──────────────────────────────────┐</span><br><span class="line">│           web (Controller)        │  接收请求、返回响应</span><br><span class="line">├──────────────────────────────────┤</span><br><span class="line">│          server (Service)         │  业务逻辑</span><br><span class="line">├──────────────────────────────────┤</span><br><span class="line">│           dao (Mapper)            │  数据访问</span><br><span class="line">├──────────────────────────────────┤</span><br><span class="line">│          model (Entity)           │  数据模型</span><br><span class="line">├──────────────────────────────────┤</span><br><span class="line">│           base (Utils)            │  基础工具</span><br><span class="line">└──────────────────────────────────┘</span><br></pre></td></tr></table></figure><h3 id="3-2-依赖关系"><a href="#3-2-依赖关系" class="headerlink" title="3.2 依赖关系"></a>3.2 依赖关系</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">web 依赖 server</span><br><span class="line">server 依赖 dao, model, base</span><br><span class="line">dao 依赖 model, base</span><br><span class="line">model 依赖 base</span><br><span class="line">base 不依赖任何内部模块</span><br></pre></td></tr></table></figure><p><strong>依赖方向</strong>：上层依赖下层，下层不依赖上层。<code>base</code> 是最底层。</p><h3 id="3-3-各层代码"><a href="#3-3-各层代码" class="headerlink" title="3.3 各层代码"></a>3.3 各层代码</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// base 模块</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtUtils</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">createToken</span><span class="params">(String data, String sign)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> JWT.create().withAudience(data)</span><br><span class="line">                .withExpiresAt(DateUtil.offsetDay(<span class="keyword">new</span> <span class="title class_">Date</span>(), <span class="number">1</span>))</span><br><span class="line">                .sign(Algorithm.HMAC256(sign));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// model 模块</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@TableName(&quot;vue_login&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> &#123;</span><br><span class="line">    <span class="meta">@TableId(type = IdType.AUTO)</span></span><br><span class="line">    <span class="keyword">private</span> Integer id;</span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line">    <span class="keyword">private</span> String password;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// dao 模块</span></span><br><span class="line"><span class="meta">@Mapper</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">UserMapper</span> <span class="keyword">extends</span> <span class="title class_">BaseMapper</span>&lt;User&gt; &#123;</span><br><span class="line">    <span class="meta">@Select(&quot;SELECT * FROM vue_login WHERE username = #&#123;username&#125;&quot;)</span></span><br><span class="line">    User <span class="title function_">findByUsername</span><span class="params">(<span class="meta">@Param(&quot;username&quot;)</span> String username)</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// server 模块</span></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserServiceImpl</span> <span class="keyword">extends</span> <span class="title class_">ServiceImpl</span>&lt;UserMapper, User&gt; <span class="keyword">implements</span> <span class="title class_">UserService</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">login</span><span class="params">(String username, String password)</span> &#123;</span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userMapper.findByUsername(username);</span><br><span class="line">        <span class="keyword">if</span> (user == <span class="literal">null</span> || !BCrypt.checkpw(password, user.getPassword())) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;用户名或密码错误&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> user;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// web 模块</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/user&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostMapping(&quot;/login&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> R <span class="title function_">login</span><span class="params">(<span class="meta">@RequestBody</span> Map&lt;String, String&gt; params)</span> &#123;</span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userService.login(params.get(<span class="string">&quot;username&quot;</span>), params.get(<span class="string">&quot;password&quot;</span>));</span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> JwtUtils.createToken(</span><br><span class="line">            user.getId() + <span class="string">&quot;-&quot;</span> + user.getRole(), user.getPassword());</span><br><span class="line">        <span class="keyword">return</span> R.success().put(<span class="string">&quot;token&quot;</span>, token);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-4-五层-vs-三层"><a href="#3-4-五层-vs-三层" class="headerlink" title="3.4 五层 vs 三层"></a>3.4 五层 vs 三层</h3><table><thead><tr><th>维度</th><th>三层</th><th>五层</th></tr></thead><tbody><tr><td>模块数</td><td>3 (Controller&#x2F;Service&#x2F;DAO)</td><td>5 (+Model&#x2F;Base)</td></tr><tr><td>实体归属</td><td>在Service或DAO中</td><td>独立model模块</td></tr><tr><td>工具归属</td><td>散落各处</td><td>独立base模块</td></tr><tr><td>复用性</td><td>中</td><td>高</td></tr><tr><td>适合</td><td>小项目</td><td>多团队协作</td></tr></tbody></table><hr><h2 id="第四章：RuoYi-框架二次开发"><a href="#第四章：RuoYi-框架二次开发" class="headerlink" title="第四章：RuoYi 框架二次开发"></a>第四章：RuoYi 框架二次开发</h2><h3 id="4-1-7-模块结构"><a href="#4-1-7-模块结构" class="headerlink" title="4.1 7 模块结构"></a>4.1 7 模块结构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Vue3-RuoYi/</span><br><span class="line">├── ruoyi-admin      # 主启动模块（可部署）</span><br><span class="line">├── ruoyi-common     # 通用工具</span><br><span class="line">├── ruoyi-framework  # 框架核心（安全、缓存、AOP）</span><br><span class="line">├── ruoyi-generator  # 代码生成器</span><br><span class="line">├── ruoyi-quartz     # 定时任务</span><br><span class="line">├── ruoyi-system     # 系统模块（用户、角色、菜单）</span><br><span class="line">└── ruoyi-ui        # Vue前端</span><br></pre></td></tr></table></figure><h3 id="4-2-18-个内置功能"><a href="#4-2-18-个内置功能" class="headerlink" title="4.2 18 个内置功能"></a>4.2 18 个内置功能</h3><p>用户管理、部门管理、岗位管理、菜单管理、角色管理、字典管理、参数配置、通知公告、操作日志、登录日志、在线用户、定时任务、代码生成、API文档、服务器监控、缓存监控、表单构建、连接池监控。</p><h3 id="4-3-代码生成器"><a href="#4-3-代码生成器" class="headerlink" title="4.3 代码生成器"></a>4.3 代码生成器</h3><p>使用 Velocity 模板引擎，从数据库表结构自动生成 CRUD 代码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">## controller.java.vm</span><br><span class="line">@RestController</span><br><span class="line">@RequestMapping(&quot;/$&#123;moduleName&#125;/$&#123;businessName&#125;&quot;)</span><br><span class="line">public class $&#123;ClassName&#125;Controller extends BaseController &#123;</span><br><span class="line"></span><br><span class="line">    @Autowired</span><br><span class="line">    private I$&#123;ClassName&#125;Service $&#123;className&#125;Service;</span><br><span class="line"></span><br><span class="line">    @PreAuthorize(&quot;@ss.hasPermi(&#x27;$&#123;moduleName&#125;:$&#123;businessName&#125;:list&#x27;)&quot;)</span><br><span class="line">    @GetMapping(&quot;/list&quot;)</span><br><span class="line">    public TableDataInfo list($&#123;ClassName&#125; $&#123;className&#125;) &#123;</span><br><span class="line">        startPage();</span><br><span class="line">        List&lt;$&#123;ClassName&#125;&gt; list = $&#123;className&#125;Service.select$&#123;ClassName&#125;List($&#123;className&#125;);</span><br><span class="line">        return getDataTable(list);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-4-RBAC-五表"><a href="#4-4-RBAC-五表" class="headerlink" title="4.4 RBAC 五表"></a>4.4 RBAC 五表</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">用户(sys_user) ── 用户角色(sys_user_role) ── 角色(sys_role)</span><br><span class="line">                                              │</span><br><span class="line">                                    角色菜单(sys_role_menu)</span><br><span class="line">                                              │</span><br><span class="line">                                        菜单(sys_menu)</span><br></pre></td></tr></table></figure><h3 id="4-5-权限校验"><a href="#4-5-权限校验" class="headerlink" title="4.5 权限校验"></a>4.5 权限校验</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PreAuthorize(&quot;@ss.hasPermi(&#x27;system:user:list&#x27;)&quot;)</span>    <span class="comment">// 查询</span></span><br><span class="line"><span class="meta">@PreAuthorize(&quot;@ss.hasPermi(&#x27;system:user:add&#x27;)&quot;)</span>     <span class="comment">// 新增</span></span><br><span class="line"><span class="meta">@PreAuthorize(&quot;@ss.hasPermi(&#x27;system:user:edit&#x27;)&quot;)</span>    <span class="comment">// 修改</span></span><br><span class="line"><span class="meta">@PreAuthorize(&quot;@ss.hasPermi(&#x27;system:user:remove&#x27;)&quot;)</span>   <span class="comment">// �删除</span></span><br></pre></td></tr></table></figure><p><strong>格式</strong>：<code>模块:业务:操作</code></p><h3 id="4-6-Quartz-定时任务"><a href="#4-6-Quartz-定时任务" class="headerlink" title="4.6 Quartz 定时任务"></a>4.6 Quartz 定时任务</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component(&quot;ryTask&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RyTask</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">ryParams</span><span class="params">(String params)</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;执行定时任务，参数：&quot;</span> + params);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在管理界面配置调用目标：<code>ryTask.ryParams(&#39;hello&#39;)</code>，Cron表达式：<code>0 0/5 * * * ?</code>。</p><h3 id="4-7-三种架构对比"><a href="#4-7-三种架构对比" class="headerlink" title="4.7 三种架构对比"></a>4.7 三种架构对比</h3><table><thead><tr><th>维度</th><th>springboot-core-arch</th><th>springbootmultimodule</th><th>Vue3-RuoYi</th></tr></thead><tbody><tr><td>模块数</td><td>12</td><td>5</td><td>7</td></tr><tr><td>模块粒度</td><td>每个公共能力独立</td><td>按功能域聚合</td><td>按功能域聚合</td></tr><tr><td>可启动模块</td><td>2 (auth&#x2F;user)</td><td>1 (web)</td><td>1 (admin)</td></tr><tr><td>代码生成器</td><td>无</td><td>无</td><td>有</td></tr><tr><td>定时任务</td><td>无</td><td>无</td><td>Quartz</td></tr><tr><td>复杂度</td><td>高</td><td>低</td><td>中</td></tr><tr><td>适合场景</td><td>学习架构设计</td><td>小项目</td><td>快速开发后台</td></tr></tbody></table><hr><h2 id="第五章：分层原则"><a href="#第五章：分层原则" class="headerlink" title="第五章：分层原则"></a>第五章：分层原则</h2><h3 id="5-1-依赖方向"><a href="#5-1-依赖方向" class="headerlink" title="5.1 依赖方向"></a>5.1 依赖方向</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">✅ 正确：web → server → dao → model → base</span><br><span class="line">❌ 错误：dao → server（下层依赖上层）</span><br><span class="line">❌ 错误：model → dao（实体依赖数据访问）</span><br></pre></td></tr></table></figure><h3 id="5-2-职责边界"><a href="#5-2-职责边界" class="headerlink" title="5.2 职责边界"></a>5.2 职责边界</h3><table><thead><tr><th>层</th><th>应该做</th><th>不应该做</th></tr></thead><tbody><tr><td>web</td><td>参数校验、调用Service、封装响应</td><td>业务逻辑、数据库操作</td></tr><tr><td>server</td><td>业务逻辑、事务管理</td><td>HTTP相关代码、SQL</td></tr><tr><td>dao</td><td>数据库CRUD</td><td>业务逻辑</td></tr><tr><td>model</td><td>数据定义</td><td>任何逻辑</td></tr><tr><td>base</td><td>通用工具</td><td>业务相关代码</td></tr></tbody></table><h3 id="5-3-典型违反"><a href="#5-3-典型违反" class="headerlink" title="5.3 典型违反"></a>5.3 典型违反</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ Controller中写了业务逻辑</span></span><br><span class="line"><span class="meta">@PostMapping(&quot;/login&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">login</span><span class="params">(<span class="meta">@RequestBody</span> User user)</span> &#123;</span><br><span class="line">    <span class="type">User</span> <span class="variable">dbUser</span> <span class="operator">=</span> userMapper.findByUsername(user.getUsername());  <span class="comment">// 直接操作DAO</span></span><br><span class="line">    <span class="keyword">if</span> (dbUser == <span class="literal">null</span>) <span class="keyword">return</span> R.error(<span class="string">&quot;用户不存在&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (!BCrypt.checkpw(user.getPassword(), dbUser.getPassword())) &#123;  <span class="comment">// 业务逻辑在Controller</span></span><br><span class="line">        <span class="keyword">return</span> R.error(<span class="string">&quot;密码错误&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> R.success();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ 正确：Controller只做调度</span></span><br><span class="line"><span class="meta">@PostMapping(&quot;/login&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">login</span><span class="params">(<span class="meta">@RequestBody</span> User user)</span> &#123;</span><br><span class="line">    <span class="type">User</span> <span class="variable">result</span> <span class="operator">=</span> userService.login(user.getUsername(), user.getPassword());</span><br><span class="line">    <span class="keyword">return</span> R.success(result);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><table><thead><tr><th>设计原则</th><th>说明</th></tr></thead><tbody><tr><td>单一职责</td><td>每个模块只做一件事（缓存就是缓存，日志就是日志）</td></tr><tr><td>依赖方向</td><td>公共模块不依赖业务模块，业务模块按需引入公共模块</td></tr><tr><td>聚合点</td><td>common-db 聚合所有公共依赖，减少 server 层的依赖声明</td></tr><tr><td>可启动</td><td>只有 server-auth&#x2F;server-user 有 main 方法，common 模块是 jar 依赖</td></tr><tr><td>开闭原则</td><td>新增角色只需新增策略类，不改已有代码</td></tr></tbody></table><p><strong>核心思想</strong>：模块化不是为了炫技，而是让团队可以按需引入——只需要缓存的模块不引入安全，只需要日志的模块不引入数据库。每个模块独立编译、独立测试、独立版本管理。</p>]]>
    </content>
    <id>http://example.com/2026/07/15/20260715/2026071503/</id>
    <link href="http://example.com/2026/07/15/20260715/2026071503/"/>
    <published>2026-07-15T00:48:22.000Z</published>
    <summary>
      <![CDATA[<h1 id=""><a href="#" class="headerlink" title=""></a></h1><blockquote>
<p>一个脚手架拆成 12 个 Maven 模块——每个公共能力独立成模块，按需引入。另一个项目用经典五层分层。还有一个用 RuoYi]]>
    </summary>
    <title>模块化架构设计：从五层分层到 12 模块拆分再到 RuoYi 二开</title>
    <updated>2026-07-15T01:30:17.507Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="Spring cloud" scheme="http://example.com/categories/Spring-cloud/"/>
    <category term="Java" scheme="http://example.com/categories/Spring-cloud/Java/"/>
    <category term="Spring cloud" scheme="http://example.com/tags/Spring-cloud/"/>
    <category term="微服务" scheme="http://example.com/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    <content>
      <![CDATA[<blockquote><p>Feign Fallback 解决的是”调用失败后怎么办”，但如果商品服务持续故障，每次调用都等超时——系统会被拖死。Sentinel 在故障扩散前就熔断。Seata 保证跨服务操作的一致性。RocketMQ 把同步链变成异步扇出。本文继续拆解微服务的高级特性。</p></blockquote><hr><h2 id="第一章：Sentinel-熔断限流"><a href="#第一章：Sentinel-熔断限流" class="headerlink" title="第一章：Sentinel 熔断限流"></a>第一章：Sentinel 熔断限流</h2><h3 id="1-1-雪崩效应"><a href="#1-1-雪崩效应" class="headerlink" title="1.1 雪崩效应"></a>1.1 雪崩效应</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">订单服务 → 商品服务（持续故障）</span><br><span class="line">    │ 第1次调用：等10秒超时 → Fallback</span><br><span class="line">    │ 第2次调用：等10秒超时 → Fallback</span><br><span class="line">    │ 线程池被超时请求占满 → 订单服务也挂了</span><br><span class="line">    ▼</span><br><span class="line">雪崩：一个服务故障导致整个系统不可用</span><br></pre></td></tr></table></figure><h3 id="1-2-熔断器三态"><a href="#1-2-熔断器三态" class="headerlink" title="1.2 熔断器三态"></a>1.2 熔断器三态</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">┌─────────┐</span><br><span class="line">│ CLOSED  │ ← 正常状态，请求放行</span><br><span class="line">└────┬────┘</span><br><span class="line">     │ 错误率超阈值</span><br><span class="line">     ▼</span><br><span class="line">┌─────────┐</span><br><span class="line">│  OPEN   │ ← 熔断状态，直接返回Fallback</span><br><span class="line">└────┬────┘</span><br><span class="line">     │ 等待恢复时间</span><br><span class="line">     ▼</span><br><span class="line">┌─────────┐</span><br><span class="line">│HALF_OPEN│ ← 半开，放行一个探测请求</span><br><span class="line">└────┬────┘</span><br><span class="line">     │ 成功 → CLOSED</span><br><span class="line">     │ 失败 → OPEN</span><br></pre></td></tr></table></figure><h3 id="1-3-配置"><a href="#1-3-配置" class="headerlink" title="1.3 配置"></a>1.3 配置</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-sentinel<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">sentinel:</span></span><br><span class="line">      <span class="attr">transport:</span></span><br><span class="line">        <span class="attr">dashboard:</span> <span class="string">localhost:8088</span></span><br><span class="line">        <span class="attr">port:</span> <span class="number">8719</span></span><br><span class="line">      <span class="attr">eager:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">openfeign:</span></span><br><span class="line">      <span class="attr">sentinel:</span></span><br><span class="line">        <span class="attr">enabled:</span> <span class="literal">true</span>  <span class="comment"># ★ 开启Feign对Sentinel的支持</span></span><br></pre></td></tr></table></figure><h3 id="1-4-三重保护链路"><a href="#1-4-三重保护链路" class="headerlink" title="1.4 三重保护链路"></a>1.4 三重保护链路</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">请求到达</span><br><span class="line">    │</span><br><span class="line">    ▼ Sentinel检查</span><br><span class="line">    │  OPEN → 直接Fallback（不发起调用）</span><br><span class="line">    │  CLOSED → 继续</span><br><span class="line">    │  HALF_OPEN → 放行1个探测</span><br><span class="line">    │</span><br><span class="line">    ▼ Feign调用</span><br><span class="line">    │  超时 → Fallback</span><br><span class="line">    │  5xx → Fallback</span><br><span class="line">    │  200 → 返回结果</span><br></pre></td></tr></table></figure><h3 id="1-5-滑动窗口算法"><a href="#1-5-滑动窗口算法" class="headerlink" title="1.5 滑动窗口算法"></a>1.5 滑动窗口算法</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">时间轴 →</span><br><span class="line">┌────┬────┬────┬────┬────┐</span><br><span class="line">│ W1 │ W2 │ W3 │ W4 │ W5 │  每个窗口100ms</span><br><span class="line">│ 3次 │ 2次 │ 5次 │ 1次 │ 2次 │</span><br><span class="line">└────┴────┴────┴────┴────┘</span><br><span class="line">      ↑                    ↑</span><br><span class="line">      └── 最近1秒窗口 ──────┘</span><br><span class="line">          总请求数 = 2+5+1+2 = 10</span><br></pre></td></tr></table></figure><p>固定窗口在窗口切换瞬间会丢失前一个窗口的数据；滑动窗口始终保留最近 N 个窗口的完整统计。</p><hr><h2 id="第二章：Seata-AT-分布式事务"><a href="#第二章：Seata-AT-分布式事务" class="headerlink" title="第二章：Seata AT 分布式事务"></a>第二章：Seata AT 分布式事务</h2><h3 id="2-1-分布式事务的难题"><a href="#2-1-分布式事务的难题" class="headerlink" title="2.1 分布式事务的难题"></a>2.1 分布式事务的难题</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">订单服务                        商品服务</span><br><span class="line">    │ 1.扣减库存(Feign) ──────────&gt;│ UPDATE product SET stock=stock-1</span><br><span class="line">    │ &lt;── 成功 ──────────────────│</span><br><span class="line">    │ 2.创建订单(本地)              │</span><br><span class="line">    │ INSERT INTO orders ... ✗失败 │</span><br><span class="line">    │                              │</span><br><span class="line">    │ 库存已扣，订单没创建 → 超卖！  │</span><br></pre></td></tr></table></figure><h3 id="2-2-Seata-三大角色"><a href="#2-2-Seata-三大角色" class="headerlink" title="2.2 Seata 三大角色"></a>2.2 Seata 三大角色</h3><table><thead><tr><th>角色</th><th>职责</th><th>示例</th></tr></thead><tbody><tr><td>TC</td><td>全局事务协调者</td><td>Seata Server（独立部署）</td></tr><tr><td>TM</td><td>事务发起者</td><td>订单服务（标注<code>@GlobalTransactional</code>）</td></tr><tr><td>RM</td><td>资源管理者</td><td>商品服务（执行SQL，记录undo_log）</td></tr></tbody></table><h3 id="2-3-AT-模式执行流程"><a href="#2-3-AT-模式执行流程" class="headerlink" title="2.3 AT 模式执行流程"></a>2.3 AT 模式执行流程</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">TM: 订单服务</span><br><span class="line">  │ ① 开启全局事务，获取XID</span><br><span class="line">  │ ② Feign调用商品服务（XID通过Header传递）</span><br><span class="line">  │──────────────────────────────────────&gt; RM: 商品服务</span><br><span class="line">  │                                       │ ③ 注册分支事务到TC</span><br><span class="line">  │                                       │ ④ 执行SQL前：记录before image</span><br><span class="line">  │                                       │ ⑤ 执行UPDATE（扣库存）</span><br><span class="line">  │                                       │ ⑥ 执行SQL后：记录after image</span><br><span class="line">  │                                       │ ⑦ 生成undo_log，注册到TC</span><br><span class="line">  │ &lt;────────────────────────────────────│</span><br><span class="line">  │ ⑧ 本地INSERT（创建订单）✗ 失败</span><br><span class="line">  │ ⑨ TM通知TC回滚</span><br><span class="line">  │──────────────────────────────────────&gt; TC通知所有RM回滚</span><br><span class="line">  │                                       │ RM用undo_log反向回滚库存</span><br><span class="line">  │                                       │ UPDATE product SET stock=stock+1</span><br></pre></td></tr></table></figure><h3 id="2-4-创建订单（全局事务）"><a href="#2-4-创建订单（全局事务）" class="headerlink" title="2.4 创建订单（全局事务）"></a>2.4 创建订单（全局事务）</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@GlobalTransactional(name = &quot;cgb-create-order&quot;, rollbackFor = Exception.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">createOrder</span><span class="params">(OrdersEntity entity)</span> &#123;</span><br><span class="line">    <span class="comment">// ① 远程调用商品服务扣减库存（RM端）</span></span><br><span class="line">    <span class="type">var</span> <span class="variable">stockResult</span> <span class="operator">=</span> feignProductService.decreaseStock(</span><br><span class="line">        entity.getProductId(), entity.getQuantity());</span><br><span class="line">    <span class="keyword">if</span> (stockResult.getCode() != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">EIException</span>(<span class="string">&quot;库存扣减失败: &quot;</span> + stockResult.getMsg());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ② 计算总价</span></span><br><span class="line">    entity.setTotalPrice(entity.getUnitPrice()</span><br><span class="line">        .multiply(BigDecimal.valueOf(entity.getQuantity())));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ③ 保存订单（本地TM端）</span></span><br><span class="line">    ordersDao.insert(entity);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ④ 发送MQ消息</span></span><br><span class="line">    sendOrderStatusMessage(entity, MQTopics.TAG_ORDER_CREATED);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>如果 ③ 失败</strong>：Seata TC 通知商品服务 RM，用 undo_log 自动回滚库存。</p><h3 id="2-5-四处全局事务"><a href="#2-5-四处全局事务" class="headerlink" title="2.5 四处全局事务"></a>2.5 四处全局事务</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 参团：人数+1 + 扣库存 + 发MQ</span></span><br><span class="line"><span class="meta">@GlobalTransactional(name = &quot;cgb-join-groupbuy&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">joinGroupBuy</span><span class="params">(Long groupBuyId, Long userId, Integer quantity)</span> &#123; ... &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建订单：扣库存 + 创建订单 + 发MQ</span></span><br><span class="line"><span class="meta">@GlobalTransactional(name = &quot;cgb-create-order&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">createOrder</span><span class="params">(OrdersEntity entity)</span> &#123; ... &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 取消订单：更新状态 + 回补库存 + 发MQ</span></span><br><span class="line"><span class="meta">@GlobalTransactional(name = &quot;cgb-cancel-order&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">cancel</span><span class="params">(String orderId, Long userId)</span> &#123; ... &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 购物车结算：批量创建订单 + 清空购物车</span></span><br><span class="line"><span class="meta">@GlobalTransactional(name = &quot;cgb-cart-checkout&quot;)</span></span><br><span class="line"><span class="keyword">public</span> List&lt;OrderVO&gt; <span class="title function_">checkout</span><span class="params">(Long userId)</span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><h3 id="2-6-RM-端：商品服务扣减库存"><a href="#2-6-RM-端：商品服务扣减库存" class="headerlink" title="2.6 RM 端：商品服务扣减库存"></a>2.6 RM 端：商品服务扣减库存</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> R&lt;?&gt; decreaseStock(Long id, Integer quantity) &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> STOCK_KEY_PREFIX + id;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// ① Redis预扣减（高性能防超卖）</span></span><br><span class="line">    <span class="type">Long</span> <span class="variable">remain</span> <span class="operator">=</span> redisTemplate.opsForValue().decrement(key, quantity);</span><br><span class="line">    <span class="keyword">if</span> (remain != <span class="literal">null</span> &amp;&amp; remain &lt; <span class="number">0</span>) &#123;</span><br><span class="line">        redisTemplate.opsForValue().increment(key, quantity);  <span class="comment">// 回滚Redis</span></span><br><span class="line">        <span class="keyword">return</span> R.fail(<span class="string">&quot;库存不足&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// ② 数据库扣减（Seata AT自动管理undo_log）</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">rows</span> <span class="operator">=</span> shangpinDao.decreaseStock(id, quantity);</span><br><span class="line">    <span class="keyword">if</span> (rows == <span class="number">0</span>) &#123;</span><br><span class="line">        redisTemplate.opsForValue().increment(key, quantity);  <span class="comment">// 回滚Redis</span></span><br><span class="line">        <span class="keyword">return</span> R.fail(<span class="string">&quot;库存不足&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> R.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Redis + DB 双重扣减</strong>：Redis 先扣（高性能防超卖），DB 再扣（持久化）。Seata 回滚 DB 但不会回滚 Redis——需要通过 RocketMQ 消息补偿。</p><h3 id="2-7-undo-log-表"><a href="#2-7-undo-log-表" class="headerlink" title="2.7 undo_log 表"></a>2.7 undo_log 表</h3><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> `undo_log` (</span><br><span class="line">    `branch_id` <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    `xid` <span class="type">VARCHAR</span>(<span class="number">128</span>) <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;全局事务ID&#x27;</span>,</span><br><span class="line">    `rollback_info` LONGBLOB <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;before/after image&#x27;</span>,</span><br><span class="line">    `log_status` <span class="type">INT</span> <span class="keyword">NOT NULL</span>,</span><br><span class="line">    <span class="keyword">PRIMARY KEY</span> (`branch_id`)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>回滚时：<code>UPDATE product SET stock = 100 WHERE id = 1</code>（用 beforeImage 反向操作）。</p><hr><h2 id="第三章：RocketMQ-异步消息"><a href="#第三章：RocketMQ-异步消息" class="headerlink" title="第三章：RocketMQ 异步消息"></a>第三章：RocketMQ 异步消息</h2><h3 id="3-1-同步-vs-异步"><a href="#3-1-同步-vs-异步" class="headerlink" title="3.1 同步 vs 异步"></a>3.1 同步 vs 异步</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">同步链路（慢）：</span><br><span class="line">用户支付 → 加积分(200ms) → 更新销量(150ms) → 生成公告(100ms) → 返回</span><br><span class="line">         总耗时：450ms（用户等待）</span><br><span class="line"></span><br><span class="line">异步扇出（快）：</span><br><span class="line">用户支付 → 发送MQ消息(5ms) → 返回成功</span><br><span class="line">              │</span><br><span class="line">              ├── 用户服务消费 → 加积分(异步)</span><br><span class="line">              ├── 商品服务消费 → 更新销量(异步)</span><br><span class="line">              └── 内容服务消费 → 生成公告(异步)</span><br><span class="line">         用户感知耗时：5ms</span><br></pre></td></tr></table></figure><h3 id="3-2-消息主题与标签"><a href="#3-2-消息主题与标签" class="headerlink" title="3.2 消息主题与标签"></a>3.2 消息主题与标签</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">MQTopics</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">ORDER_STATUS_CHANGE</span> <span class="operator">=</span> <span class="string">&quot;ORDER_STATUS_CHANGE&quot;</span>;</span><br><span class="line">    <span class="type">String</span> <span class="variable">GROUPBUY_STATUS_CHANGE</span> <span class="operator">=</span> <span class="string">&quot;GROUPBUY_STATUS_CHANGE&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">String</span> <span class="variable">TAG_ORDER_CREATED</span> <span class="operator">=</span> <span class="string">&quot;ORDER_CREATED&quot;</span>;</span><br><span class="line">    <span class="type">String</span> <span class="variable">TAG_ORDER_PAID</span> <span class="operator">=</span> <span class="string">&quot;ORDER_PAID&quot;</span>;</span><br><span class="line">    <span class="type">String</span> <span class="variable">TAG_ORDER_CANCELLED</span> <span class="operator">=</span> <span class="string">&quot;ORDER_CANCELLED&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">String</span> <span class="variable">TAG_GROUPBUY_JOINED</span> <span class="operator">=</span> <span class="string">&quot;GROUPBUY_JOINED&quot;</span>;</span><br><span class="line">    <span class="type">String</span> <span class="variable">TAG_GROUPBUY_COMPLETED</span> <span class="operator">=</span> <span class="string">&quot;GROUPBUY_COMPLETED&quot;</span>;</span><br><span class="line">    <span class="type">String</span> <span class="variable">TAG_GROUPBUY_EXPIRED</span> <span class="operator">=</span> <span class="string">&quot;GROUPBUY_EXPIRED&quot;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-3-消息生产者"><a href="#3-3-消息生产者" class="headerlink" title="3.3 消息生产者"></a>3.3 消息生产者</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">sendOrderStatusMessage</span><span class="params">(OrdersEntity order, String tag)</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="type">OrderStatusMessage</span> <span class="variable">msg</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">OrderStatusMessage</span>();</span><br><span class="line">        msg.setOrderId(order.getOrderNo());</span><br><span class="line">        msg.setUserId(order.getUserId());</span><br><span class="line">        msg.setProductId(order.getProductId());</span><br><span class="line">        msg.setTotalPrice(order.getTotalPrice());</span><br><span class="line">        msg.setStatus(order.getStatus());</span><br><span class="line"></span><br><span class="line">        <span class="comment">// destination格式：topic:tag</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">destination</span> <span class="operator">=</span> MQTopics.ORDER_STATUS_CHANGE + <span class="string">&quot;:&quot;</span> + tag;</span><br><span class="line">        rocketMQTemplate.syncSend(destination, </span><br><span class="line">            MessageBuilder.withPayload(msg).build());</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        log.error(<span class="string">&quot;消息发送失败: orderId=&#123;&#125;, tag=&#123;&#125;&quot;</span>, order.getOrderNo(), tag, e);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-4-四个消费者"><a href="#3-4-四个消费者" class="headerlink" title="3.4 四个消费者"></a>3.4 四个消费者</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 消费者1：用户服务消费支付消息（加积分）</span></span><br><span class="line"><span class="meta">@RocketMQMessageListener(</span></span><br><span class="line"><span class="meta">    topic = MQTopics.ORDER_STATUS_CHANGE,</span></span><br><span class="line"><span class="meta">    selectorExpression = MQTopics.TAG_ORDER_PAID,</span></span><br><span class="line"><span class="meta">    consumerGroup = &quot;cgb-user-order-consumer-group&quot;</span></span><br><span class="line"><span class="meta">)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserOrderMessageConsumer</span> <span class="keyword">implements</span> <span class="title class_">RocketMQListener</span>&lt;OrderStatusMessage&gt; &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onMessage</span><span class="params">(OrderStatusMessage message)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (message.getStatus() == <span class="number">1</span>) &#123;</span><br><span class="line">            yonghuService.addPoints(message.getUserId(), </span><br><span class="line">                message.getTotalPrice().doubleValue());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 消费者2：商品服务消费订单消息（更新销量）</span></span><br><span class="line"><span class="meta">@RocketMQMessageListener(</span></span><br><span class="line"><span class="meta">    topic = MQTopics.ORDER_STATUS_CHANGE,</span></span><br><span class="line"><span class="meta">    selectorExpression = &quot;*&quot;,</span></span><br><span class="line"><span class="meta">    consumerGroup = &quot;cgb-product-order-consumer-group&quot;</span></span><br><span class="line"><span class="meta">)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProductOrderMessageConsumer</span> <span class="keyword">implements</span> <span class="title class_">RocketMQListener</span>&lt;OrderStatusMessage&gt; &#123; ... &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 消费者3：内容服务消费成团消息（生成公告）</span></span><br><span class="line"><span class="meta">@RocketMQMessageListener(</span></span><br><span class="line"><span class="meta">    topic = MQTopics.GROUPBUY_STATUS_CHANGE,</span></span><br><span class="line"><span class="meta">    selectorExpression = MQTopics.TAG_GROUPBUY_COMPLETED,</span></span><br><span class="line"><span class="meta">    consumerGroup = &quot;cgb-content-groupbuy-consumer-group&quot;</span></span><br><span class="line"><span class="meta">)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ContentGroupBuyConsumer</span> <span class="keyword">implements</span> <span class="title class_">RocketMQListener</span>&lt;GroupBuyMessage&gt; &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onMessage</span><span class="params">(GroupBuyMessage message)</span> &#123;</span><br><span class="line">        <span class="type">NewsEntity</span> <span class="variable">news</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">NewsEntity</span>();</span><br><span class="line">        news.setTitle(<span class="string">&quot;🎉 团购成团通知&quot;</span>);</span><br><span class="line">        news.setContent(String.format(<span class="string">&quot;团购ID=%d，共%d人参团成功&quot;</span>, </span><br><span class="line">            message.getGroupBuyId(), message.getCurrentMemberCount()));</span><br><span class="line">        newsService.save(news);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 消费者4：团购服务消费过期消息（回补库存）</span></span><br><span class="line"><span class="meta">@RocketMQMessageListener(</span></span><br><span class="line"><span class="meta">    topic = MQTopics.GROUPBUY_STATUS_CHANGE,</span></span><br><span class="line"><span class="meta">    selectorExpression = &quot;*&quot;,</span></span><br><span class="line"><span class="meta">    consumerGroup = &quot;cgb-groupbuy-status-consumer-group&quot;</span></span><br><span class="line"><span class="meta">)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GroupBuyStatusConsumer</span> <span class="keyword">implements</span> <span class="title class_">RocketMQListener</span>&lt;GroupBuyMessage&gt; &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onMessage</span><span class="params">(GroupBuyMessage message)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (message.getStatus() == <span class="number">2</span>) &#123;  <span class="comment">// 过期</span></span><br><span class="line">            feignProductService.increaseStock(</span><br><span class="line">                message.getProductId(), message.getQuantity());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-5-消费幂等"><a href="#3-5-消费幂等" class="headerlink" title="3.5 消费幂等"></a>3.5 消费幂等</h3><p>RocketMQ 可能重复投递消息，消费者必须幂等：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onMessage</span><span class="params">(OrderStatusMessage message)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">processedKey</span> <span class="operator">=</span> <span class="string">&quot;mq:processed:&quot;</span> + message.getOrderId();</span><br><span class="line">    <span class="comment">// SETNX：第一次设置返回true，重复设置返回false</span></span><br><span class="line">    <span class="keyword">if</span> (redisTemplate.opsForValue().setIfAbsent(</span><br><span class="line">            processedKey, <span class="string">&quot;1&quot;</span>, <span class="number">24</span>, TimeUnit.HOURS)) &#123;</span><br><span class="line">        yonghuService.addPoints(message.getUserId(), points);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;消息已处理，跳过: orderId=&#123;&#125;&quot;</span>, message.getOrderId());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-6-消息流转全景"><a href="#3-6-消息流转全景" class="headerlink" title="3.6 消息流转全景"></a>3.6 消息流转全景</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">订单服务                      用户服务</span><br><span class="line">  │ 支付成功                     │</span><br><span class="line">  │  │                          │</span><br><span class="line">  │  ▼ 发送MQ                    │</span><br><span class="line">  │  ORDER_STATUS_CHANGE:ORDER_PAID</span><br><span class="line">  │  │                          │</span><br><span class="line">  │  │ ┌────────────────────────┘</span><br><span class="line">  │  ▼ ▼ 消费者1 → 加积分</span><br><span class="line">  │  用户服务.onMessage()</span><br><span class="line">  │</span><br><span class="line">  │  │ ┌────────────────────────┐</span><br><span class="line">  │  ▼ ▼ 消费者2 → 更新销量</span><br><span class="line">  │  商品服务.onMessage()</span><br><span class="line">  │</span><br><span class="line">  │  发送MQ</span><br><span class="line">  │  GROUPBUY_STATUS_CHANGE:GROUPBUY_COMPLETED</span><br><span class="line">  │  │</span><br><span class="line">  │  ▼ 消费者3 → 生成公告</span><br><span class="line">  │  内容服务.onMessage()</span><br></pre></td></tr></table></figure><hr><h2 id="第四章：雪花算法-ID-生成"><a href="#第四章：雪花算法-ID-生成" class="headerlink" title="第四章：雪花算法 ID 生成"></a>第四章：雪花算法 ID 生成</h2><h3 id="4-1-分布式唯一-ID"><a href="#4-1-分布式唯一-ID" class="headerlink" title="4.1 分布式唯一 ID"></a>4.1 分布式唯一 ID</h3><p>在 Spring Cloud 2024 Demo 项目中，订单号用雪花算法生成：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> cn.hutool.core.util.IdUtil;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Transactional(rollbackFor = Exception.class)</span></span><br><span class="line"><span class="keyword">public</span> R&lt;Order&gt; <span class="title function_">createOrder</span><span class="params">(Long userId, Long productId, Integer quantity)</span> &#123;</span><br><span class="line">    <span class="comment">// 远程调用商品服务查询和扣减</span></span><br><span class="line">    R&lt;?&gt; productResult = productFeignClient.getProduct(productId);</span><br><span class="line">    R&lt;Void&gt; stockResult = productFeignClient.deductStock(productId, quantity);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ★ 雪花算法生成分布式唯一订单号</span></span><br><span class="line">    <span class="type">Order</span> <span class="variable">order</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Order</span>();</span><br><span class="line">    order.setOrderNo(IdUtil.getSnowflakeNextIdStr());</span><br><span class="line">    order.setUserId(userId);</span><br><span class="line">    order.setProductId(productId);</span><br><span class="line">    save(order);</span><br><span class="line">    <span class="keyword">return</span> R.ok(order);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-2-雪花算法原理"><a href="#4-2-雪花算法原理" class="headerlink" title="4.2 雪花算法原理"></a>4.2 雪花算法原理</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">┌─────────────────────────────────────────────────┐</span><br><span class="line">│  1位符号  │  41位时间戳  │ 10位机器ID │ 12位序列号 │</span><br><span class="line">│  (不用)   │ (约69年)    │ (1024台)  │ (4096/ms) │</span><br><span class="line">└─────────────────────────────────────────────────┘</span><br></pre></td></tr></table></figure><ul><li>时间戳：保证趋势递增（数据库索引友好）</li><li>机器ID：分布式唯一（每台机器不同）</li><li>序列号：同一毫秒内的递增序号</li></ul><hr><h2 id="第五章：乐观锁防超卖"><a href="#第五章：乐观锁防超卖" class="headerlink" title="第五章：乐观锁防超卖"></a>第五章：乐观锁防超卖</h2><h3 id="5-1-MyBatis-Plus-乐观锁"><a href="#5-1-MyBatis-Plus-乐观锁" class="headerlink" title="5.1 MyBatis Plus 乐观锁"></a>5.1 MyBatis Plus 乐观锁</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyBatisPlusConfig</span> &#123;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> MybatisPlusInterceptor <span class="title function_">mybatisPlusInterceptor</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">MybatisPlusInterceptor</span> <span class="variable">interceptor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MybatisPlusInterceptor</span>();</span><br><span class="line">        interceptor.addInnerInterceptor(<span class="keyword">new</span> <span class="title class_">PaginationInnerInterceptor</span>(DbType.MYSQL));</span><br><span class="line">        interceptor.addInnerInterceptor(<span class="keyword">new</span> <span class="title class_">OptimisticLockerInnerInterceptor</span>());</span><br><span class="line">        <span class="keyword">return</span> interceptor;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-2-Product-实体"><a href="#5-2-Product-实体" class="headerlink" title="5.2 Product 实体"></a>5.2 Product 实体</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@TableName(&quot;product&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Product</span> <span class="keyword">extends</span> <span class="title class_">BaseEntity</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line">    <span class="keyword">private</span> BigDecimal price;</span><br><span class="line">    <span class="keyword">private</span> Integer stock;</span><br><span class="line">    <span class="comment">/** 版本号（乐观锁） */</span></span><br><span class="line">    <span class="keyword">private</span> Integer version;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-3-扣减库存"><a href="#5-3-扣减库存" class="headerlink" title="5.3 扣减库存"></a>5.3 扣减库存</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Transactional(rollbackFor = Exception.class)</span></span><br><span class="line"><span class="keyword">public</span> R&lt;Void&gt; <span class="title function_">deductStock</span><span class="params">(Long productId, Integer quantity)</span> &#123;</span><br><span class="line">    <span class="type">Product</span> <span class="variable">product</span> <span class="operator">=</span> getById(productId);</span><br><span class="line">    <span class="keyword">if</span> (product.getStock() &lt; quantity) &#123;</span><br><span class="line">        <span class="keyword">return</span> R.failed(ResultCode.STOCK_NOT_ENOUGH);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    product.setStock(product.getStock() - quantity);</span><br><span class="line">    <span class="comment">// ★ 乐观锁：updateById 会自动加 WHERE version = ?</span></span><br><span class="line">    <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> updateById(product);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!success) &#123;</span><br><span class="line">        <span class="keyword">return</span> R.failed(<span class="string">&quot;库存扣减失败，请重试&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    redisTemplate.delete(<span class="string">&quot;product:info:&quot;</span> + productId);  <span class="comment">// 删除缓存</span></span><br><span class="line">    <span class="keyword">return</span> R.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>乐观锁 vs 悲观锁</strong>：乐观锁不阻塞，CAS 重试；悲观锁 <code>SELECT FOR UPDATE</code> 阻塞。</p><hr><h2 id="第六章：Token-黑名单"><a href="#第六章：Token-黑名单" class="headerlink" title="第六章：Token 黑名单"></a>第六章：Token 黑名单</h2><h3 id="6-1-登出时加入黑名单"><a href="#6-1-登出时加入黑名单" class="headerlink" title="6.1 登出时加入黑名单"></a>6.1 登出时加入黑名单</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping(&quot;/logout&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R&lt;?&gt; logout(HttpServletRequest request) &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> JwtUtil.getTokenFromRequest(request);</span><br><span class="line">    <span class="keyword">if</span> (token != <span class="literal">null</span> &amp;&amp; JwtUtil.isTokenValid(token)) &#123;</span><br><span class="line">        <span class="comment">// 将Token加入Redis黑名单（剩余有效期内）</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">expire</span> <span class="operator">=</span> JwtUtil.getRemainingTime(token);</span><br><span class="line">        redisUtil.set(<span class="string">&quot;blacklist:&quot;</span> + token, <span class="string">&quot;true&quot;</span>, expire, TimeUnit.MILLISECONDS);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> R.ok(<span class="string">&quot;登出成功&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-2-网关检查黑名单"><a href="#6-2-网关检查黑名单" class="headerlink" title="6.2 网关检查黑名单"></a>6.2 网关检查黑名单</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 网关过滤器中检查</span></span><br><span class="line"><span class="type">Boolean</span> <span class="variable">isBlacklisted</span> <span class="operator">=</span> redisTemplate.hasKey(<span class="string">&quot;token:blacklist:&quot;</span> + token);</span><br><span class="line"><span class="keyword">if</span> (Boolean.TRUE.equals(isBlacklisted)) &#123;</span><br><span class="line">    <span class="keyword">return</span> unauthorized(exchange, <span class="string">&quot;Token已失效&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><table><thead><tr><th>技术</th><th>解决的问题</th><th>核心实现</th></tr></thead><tbody><tr><td>Sentinel</td><td>雪崩效应</td><td>滑动窗口 + 熔断三态</td></tr><tr><td>Seata AT</td><td>分布式事务一致性</td><td><code>@GlobalTransactional</code> + undo_log</td></tr><tr><td>RocketMQ</td><td>异步解耦</td><td>topic:tag + 消费者组</td></tr><tr><td>雪花算法</td><td>分布式唯一ID</td><td>时间戳 + 机器ID + 序列号</td></tr><tr><td>乐观锁</td><td>并发扣减</td><td>version字段 + CAS</td></tr><tr><td>Token黑名单</td><td>登出失效</td><td>Redis + TTL</td></tr></tbody></table><p><strong>三道防线</strong>：Sentinel 熔断（最快切断）→ Feign 超时（防止无限等待）→ Fallback 降级（返回友好响应）。Seata 保证数据一致性，RocketMQ 解耦异步逻辑。这就是微服务高级特性的完整图景。</p>]]>
    </content>
    <id>http://example.com/2026/07/15/20260715/2026071502/</id>
    <link href="http://example.com/2026/07/15/20260715/2026071502/"/>
    <published>2026-07-15T00:28:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>Feign Fallback 解决的是”调用失败后怎么办”，但如果商品服务持续故障，每次调用都等超时——系统会被拖死。Sentinel 在故障扩散前就熔断。Seata 保证跨服务操作的一致性。RocketMQ 把同步链变成异步扇出。本文继续拆解微服]]>
    </summary>
    <title>Spring Cloud 微服务架构全解（下）：熔断、事务与消息</title>
    <updated>2026-07-15T01:30:17.476Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="Spring cloud" scheme="http://example.com/categories/Spring-cloud/"/>
    <category term="Spring cloud" scheme="http://example.com/tags/Spring-cloud/"/>
    <category term="微服务" scheme="http://example.com/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    <content>
      <![CDATA[<blockquote><p>同一个社区团购业务，单体版 1 个应用，微服务版 6 个服务。怎么拆？服务怎么互相发现？请求怎么路由？跨服务调用怎么降级？本文用 3 个真实微服务项目的代码，把 Spring Cloud 的基础设施层从头到尾讲透。</p></blockquote><hr><h2 id="第一章：从单体到微服务的拆分决策"><a href="#第一章：从单体到微服务的拆分决策" class="headerlink" title="第一章：从单体到微服务的拆分决策"></a>第一章：从单体到微服务的拆分决策</h2><h3 id="1-1-单体的痛点"><a href="#1-1-单体的痛点" class="headerlink" title="1.1 单体的痛点"></a>1.1 单体的痛点</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">community-group-buying-system (单体)</span><br><span class="line">├── 15 个 Controller</span><br><span class="line">├── 14 个 DAO</span><br><span class="line">├── 1 个数据库</span><br><span class="line">├── 1 个 Redis</span><br><span class="line">└── 2 个前端</span><br></pre></td></tr></table></figure><p>当业务增长后：</p><ul><li>商品模块改代码要重新部署整个应用 → 影响用户登录</li><li>订单高峰期打满线程池 → 商品浏览也卡死</li><li>团购逻辑和商品逻辑耦合在同一代码库 → 团队协作冲突</li></ul><h3 id="1-2-拆分方案"><a href="#1-2-拆分方案" class="headerlink" title="1.2 拆分方案"></a>1.2 拆分方案</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">单体 1 个DB                    微服务 5 个DB</span><br><span class="line">┌──────────────┐              ┌──────────────┐</span><br><span class="line">│ springboot   │    ─────&gt;    │ cgb_user_db  │ ← 用户服务(8001)</span><br><span class="line">│ 2c1hu        │              ├──────────────┤</span><br><span class="line">│              │              │ cgb_product  │ ← 商品服务(8002)</span><br><span class="line">└──────────────┘              ├──────────────┤</span><br><span class="line">                              │ cgb_groupbuy │ ← 团购服务(8003)</span><br><span class="line">                              ├──────────────┤</span><br><span class="line">                              │ cgb_order    │ ← 订单服务(8004)</span><br><span class="line">                              ├──────────────┤</span><br><span class="line">                              │ cgb_content  │ ← 内容服务(8005)</span><br><span class="line">                              └──────────────┘</span><br></pre></td></tr></table></figure><h3 id="1-3-7-模块结构"><a href="#1-3-7-模块结构" class="headerlink" title="1.3 7 模块结构"></a>1.3 7 模块结构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">community-group-buying-microservices/</span><br><span class="line">├── cgb-common           # 公共模块（Feign客户端、MQ消息体、认证、工具类）</span><br><span class="line">├── cgb-gateway          # API网关 (端口8000)</span><br><span class="line">├── cgb-user-service      # 用户服务 (端口8001, 库cgb_user)</span><br><span class="line">├── cgb-product-service   # 商品服务 (端口8002, 库cgb_product)</span><br><span class="line">├── cgb-groupbuy-service # 团购服务 (端口8003, 库cgb_groupbuy)</span><br><span class="line">├── cgb-order-service     # 订单服务 (端口8004, 库cgb_order)</span><br><span class="line">└── cgb-content-service   # 内容服务 (端口8005, 库cgb_content)</span><br></pre></td></tr></table></figure><h3 id="1-4-父-POM-版本管理"><a href="#1-4-父-POM-版本管理" class="headerlink" title="1.4 父 POM 版本管理"></a>1.4 父 POM 版本管理</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">properties</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">spring-boot.version</span>&gt;</span>3.4.1<span class="tag">&lt;/<span class="name">spring-boot.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">spring-cloud.version</span>&gt;</span>2024.0.0<span class="tag">&lt;/<span class="name">spring-cloud.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">spring-cloud-alibaba.version</span>&gt;</span>2023.0.3.2<span class="tag">&lt;/<span class="name">spring-cloud-alibaba.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">seata.version</span>&gt;</span>2.2.0<span class="tag">&lt;/<span class="name">seata.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">rocketmq-spring.version</span>&gt;</span>2.3.1<span class="tag">&lt;/<span class="name">rocketmq-spring.version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">properties</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="1-5-单体-vs-微服务代码对比"><a href="#1-5-单体-vs-微服务代码对比" class="headerlink" title="1.5 单体 vs 微服务代码对比"></a>1.5 单体 vs 微服务代码对比</h3><p><strong>单体版</strong>（一个方法内完成，本地事务）：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transactional(rollbackFor = Exception.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">createOrder</span><span class="params">(OrdersEntity entity)</span> &#123;</span><br><span class="line">    <span class="type">ProductEntity</span> <span class="variable">product</span> <span class="operator">=</span> productDao.selectById(entity.getProductId());</span><br><span class="line">    productDao.decreaseStock(entity.getProductId(), entity.getQuantity());</span><br><span class="line">    ordersDao.insert(entity);</span><br><span class="line">    yonghuDao.addPoints(entity.getUserId(), entity.getTotalPrice().doubleValue());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>微服务版</strong>（跨服务调用 + 分布式事务 + 异步消息）：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GlobalTransactional(name = &quot;cgb-create-order&quot;, rollbackFor = Exception.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">createOrder</span><span class="params">(OrdersEntity entity)</span> &#123;</span><br><span class="line">    <span class="type">var</span> <span class="variable">stockResult</span> <span class="operator">=</span> feignProductService.decreaseStock(</span><br><span class="line">        entity.getProductId(), entity.getQuantity());  <span class="comment">// Feign远程调用</span></span><br><span class="line">    <span class="keyword">if</span> (stockResult.getCode() != <span class="number">0</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">EIException</span>(<span class="string">&quot;库存扣减失败&quot;</span>);</span><br><span class="line">    ordersDao.insert(entity);</span><br><span class="line">    sendOrderStatusMessage(entity, MQTopics.TAG_ORDER_CREATED);  <span class="comment">// MQ异步消息</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-6-拆分成本评估"><a href="#1-6-拆分成本评估" class="headerlink" title="1.6 拆分成本评估"></a>1.6 拆分成本评估</h3><table><thead><tr><th>维度</th><th>单体</th><th>微服务</th><th>变化</th></tr></thead><tbody><tr><td>部署</td><td>1个JAR</td><td>6个JAR + 中间件</td><td>↑↑↑</td></tr><tr><td>运维</td><td>1台服务器</td><td>6台+Nacos+Seata+RocketMQ</td><td>↑↑↑</td></tr><tr><td>开发效率</td><td>改代码全量部署</td><td>按服务独立部署</td><td>↓</td></tr><tr><td>扩展性</td><td>整体扩容</td><td>按需扩容</td><td>↓</td></tr></tbody></table><p><strong>结论</strong>：微服务用运维复杂度换取开发灵活性和扩展性。团队 &lt; 5 人、QPS &lt; 1000，单体是更好的选择。</p><hr><h2 id="第二章：Nacos-服务注册与配置中心"><a href="#第二章：Nacos-服务注册与配置中心" class="headerlink" title="第二章：Nacos 服务注册与配置中心"></a>第二章：Nacos 服务注册与配置中心</h2><h3 id="2-1-Nacos-双重角色"><a href="#2-1-Nacos-双重角色" class="headerlink" title="2.1 Nacos 双重角色"></a>2.1 Nacos 双重角色</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">              ┌─────────────────┐</span><br><span class="line">              │     Nacos       │</span><br><span class="line">服务注册 ────&gt; │  服务列表        │ &lt;──── 服务发现</span><br><span class="line">              │  cgb-user:8001  │</span><br><span class="line">              │  cgb-product:8002│</span><br><span class="line">              │                 │</span><br><span class="line">配置推送 ────&gt; │  配置存储        │ &lt;──── 配置拉取</span><br><span class="line">              │  common-redis   │</span><br><span class="line">              │  cgb-order.yml  │</span><br><span class="line">              └─────────────────┘</span><br></pre></td></tr></table></figure><h3 id="2-2-服务注册"><a href="#2-2-服务注册" class="headerlink" title="2.2 服务注册"></a>2.2 服务注册</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-nacos-discovery<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">$&#123;NACOS_ADDR:127.0.0.1:8848&#125;</span></span><br><span class="line">      <span class="attr">discovery:</span></span><br><span class="line">        <span class="attr">namespace:</span> <span class="string">cgb-dev</span>          <span class="comment"># 命名空间隔离（dev/test/prod）</span></span><br><span class="line">        <span class="attr">group:</span> <span class="string">CGB_GROUP</span>           <span class="comment"># 分组</span></span><br><span class="line">        <span class="attr">service:</span> <span class="string">cgb-order-service</span>  <span class="comment"># 服务名</span></span><br></pre></td></tr></table></figure><h3 id="2-3-命名空间-vs-分组"><a href="#2-3-命名空间-vs-分组" class="headerlink" title="2.3 命名空间 vs 分组"></a>2.3 命名空间 vs 分组</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Nacos</span><br><span class="line">├── namespace: cgb-dev</span><br><span class="line">│   ├── CGB_GROUP</span><br><span class="line">│   │   ├── cgb-user-service (8001)</span><br><span class="line">│   │   ├── cgb-product-service (8002)</span><br><span class="line">│   │   └── cgb-order-service (8004)</span><br><span class="line">│   └── SHARED_GROUP</span><br><span class="line">│       └── (共享配置)</span><br><span class="line">│</span><br><span class="line">├── namespace: cgb-test</span><br><span class="line">│   └── (测试环境服务)</span><br><span class="line">│</span><br><span class="line">└── namespace: public (默认)</span><br></pre></td></tr></table></figure><ul><li>命名空间：物理隔离（dev&#x2F;test&#x2F;prod 互不可见）</li><li>分组：逻辑分类（同一环境内的不同业务组）</li></ul><h3 id="2-4-共享配置（多服务复用）"><a href="#2-4-共享配置（多服务复用）" class="headerlink" title="2.4 共享配置（多服务复用）"></a>2.4 共享配置（多服务复用）</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Nacos中 data-id: common-redis.yml | group: SHARED_GROUP</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">data:</span></span><br><span class="line">    <span class="attr">redis:</span></span><br><span class="line">      <span class="attr">host:</span> <span class="string">$&#123;REDIS_HOST:127.0.0.1&#125;</span></span><br><span class="line">      <span class="attr">port:</span> <span class="string">$&#123;REDIS_PORT:6379&#125;</span></span><br><span class="line">      <span class="attr">database:</span> <span class="number">0</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">10s</span></span><br><span class="line">      <span class="attr">lettuce:</span></span><br><span class="line">        <span class="attr">pool:</span></span><br><span class="line">          <span class="attr">max-active:</span> <span class="number">8</span></span><br><span class="line">          <span class="attr">max-idle:</span> <span class="number">8</span></span><br><span class="line">          <span class="attr">min-idle:</span> <span class="number">0</span></span><br></pre></td></tr></table></figure><h3 id="2-5-从-Nacos-导入配置"><a href="#2-5-从-Nacos-导入配置" class="headerlink" title="2.5 从 Nacos 导入配置"></a>2.5 从 Nacos 导入配置</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">config:</span></span><br><span class="line">        <span class="attr">namespace:</span> <span class="string">cgb-dev</span></span><br><span class="line">        <span class="attr">group:</span> <span class="string">CGB_GROUP</span></span><br><span class="line">        <span class="attr">file-extension:</span> <span class="string">yml</span></span><br><span class="line">        <span class="attr">shared-configs:</span>                    <span class="comment"># 引入共享配置</span></span><br><span class="line">          <span class="bullet">-</span> <span class="attr">data-id:</span> <span class="string">common-redis.yml</span></span><br><span class="line">            <span class="attr">group:</span> <span class="string">SHARED_GROUP</span></span><br><span class="line">            <span class="attr">refresh:</span> <span class="literal">true</span>                  <span class="comment"># 支持动态刷新</span></span><br><span class="line">          <span class="bullet">-</span> <span class="attr">data-id:</span> <span class="string">common-mybatis.yml</span></span><br><span class="line">            <span class="attr">group:</span> <span class="string">SHARED_GROUP</span></span><br><span class="line">            <span class="attr">refresh:</span> <span class="literal">true</span></span><br><span class="line">        <span class="attr">refresh-enabled:</span> <span class="literal">true</span>              <span class="comment"># 开启配置动态刷新</span></span><br><span class="line">        <span class="attr">import:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">optional:nacos:cgb-order-service.yml?group=CGB_GROUP&amp;refreshEnabled=true</span></span><br></pre></td></tr></table></figure><h3 id="2-6-动态配置刷新"><a href="#2-6-动态配置刷新" class="headerlink" title="2.6 动态配置刷新"></a>2.6 动态配置刷新</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RefreshScope</span>  <span class="comment">// ★ 配置变更时自动刷新</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderController</span> &#123;</span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;order.timeout:300&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> orderTimeout;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 Nacos 控制台修改 <code>order.timeout</code> 的值，不重启应用即可生效。</p><hr><h2 id="第三章：Spring-Cloud-Gateway-网关"><a href="#第三章：Spring-Cloud-Gateway-网关" class="headerlink" title="第三章：Spring Cloud Gateway 网关"></a>第三章：Spring Cloud Gateway 网关</h2><h3 id="3-1-网关核心职责"><a href="#3-1-网关核心职责" class="headerlink" title="3.1 网关核心职责"></a>3.1 网关核心职责</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">客户端请求</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">┌──────────────────────────────────────┐</span><br><span class="line">│           API Gateway               │</span><br><span class="line">│  1. 路由：路径 → 服务映射             │</span><br><span class="line">│  2. 鉴权：JWT验证                    │</span><br><span class="line">│  3. 限流：Sentinel                   │</span><br><span class="line">│  4. 跨域：CORS                       │</span><br><span class="line">│  5. 用户信息透传                     │</span><br><span class="line">└──────────┬───────────────────────────┘</span><br><span class="line">           │</span><br><span class="line">    ┌──────┼──────┐</span><br><span class="line">    ▼      ▼      ▼</span><br><span class="line">  user   product  order</span><br></pre></td></tr></table></figure><h3 id="3-2-路由配置"><a href="#3-2-路由配置" class="headerlink" title="3.2 路由配置"></a>3.2 路由配置</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="attr">routes:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">cgb-user-service</span></span><br><span class="line">          <span class="attr">uri:</span> <span class="string">lb://cgb-user-service</span>      <span class="comment"># lb://服务名（负载均衡）</span></span><br><span class="line">          <span class="attr">predicates:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">Path=/user/**</span>               <span class="comment"># 路径匹配</span></span><br><span class="line">          <span class="attr">filters:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">StripPrefix=1</span>                <span class="comment"># 去掉第一级前缀</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">cgb-product-service</span></span><br><span class="line">          <span class="attr">uri:</span> <span class="string">lb://cgb-product-service</span></span><br><span class="line">          <span class="attr">predicates:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">Path=/product/**</span></span><br><span class="line">          <span class="attr">filters:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">StripPrefix=1</span></span><br></pre></td></tr></table></figure><p><strong>路由流程</strong>：<code>/user/login</code> → <code>StripPrefix=1</code> 去掉 <code>/user</code> → 转发到 <code>cgb-user-service</code> 的 <code>/login</code>。</p><h3 id="3-3-双前缀路由（钢铁工厂项目）"><a href="#3-3-双前缀路由（钢铁工厂项目）" class="headerlink" title="3.3 双前缀路由（钢铁工厂项目）"></a>3.3 双前缀路由（钢铁工厂项目）</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="attr">routes:</span></span><br><span class="line">        <span class="comment"># 管理前端 /api/v1 前缀路由</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">user-service-v1</span></span><br><span class="line">          <span class="attr">uri:</span> <span class="string">lb://user-service</span></span><br><span class="line">          <span class="attr">predicates:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">Path=/api/v1/login,/api/v1/admin/**,/api/v1/user/**</span></span><br><span class="line">          <span class="attr">filters:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">StripPrefix=2</span>       <span class="comment"># /api/v1/admin/list → /admin/list</span></span><br><span class="line"></span><br><span class="line">        <span class="comment"># 小程序直连路由（无前缀）</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">user-service</span></span><br><span class="line">          <span class="attr">uri:</span> <span class="string">lb://user-service</span></span><br><span class="line">          <span class="attr">predicates:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">Path=/login,/admin/**,/user/**</span></span><br><span class="line">          <span class="comment"># 无 StripPrefix，直接透传</span></span><br></pre></td></tr></table></figure><h3 id="3-4-全局跨域配置"><a href="#3-4-全局跨域配置" class="headerlink" title="3.4 全局跨域配置"></a>3.4 全局跨域配置</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="attr">globalcors:</span></span><br><span class="line">        <span class="attr">cors-configurations:</span></span><br><span class="line">          <span class="string">&#x27;[/**]&#x27;</span><span class="string">:</span></span><br><span class="line">            <span class="attr">allowed-origins:</span> <span class="string">&quot;*&quot;</span></span><br><span class="line">            <span class="attr">allowed-methods:</span> <span class="string">&quot;*&quot;</span></span><br><span class="line">            <span class="attr">allowed-headers:</span> <span class="string">&quot;*&quot;</span></span><br><span class="line">            <span class="attr">allow-credentials:</span> <span class="literal">true</span></span><br><span class="line">            <span class="attr">max-age:</span> <span class="number">3600</span></span><br></pre></td></tr></table></figure><h3 id="3-5-JWT-鉴权过滤器"><a href="#3-5-JWT-鉴权过滤器" class="headerlink" title="3.5 JWT 鉴权过滤器"></a>3.5 JWT 鉴权过滤器</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AuthGlobalFilter</span> <span class="keyword">implements</span> <span class="title class_">GlobalFilter</span>, Ordered &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RedisTemplate&lt;String, Object&gt; redisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> List&lt;String&gt; WHITE_LIST = Arrays.asList(</span><br><span class="line">            <span class="string">&quot;/login&quot;</span>, <span class="string">&quot;/api/v1/login&quot;</span>,</span><br><span class="line">            <span class="string">&quot;/files/download&quot;</span>, <span class="string">&quot;/api/v1/files/download&quot;</span>,</span><br><span class="line">            <span class="string">&quot;/uniapp/getObtainInformationDataList&quot;</span>,  <span class="comment">// 小程序公开接口</span></span><br><span class="line">            <span class="string">&quot;/hello&quot;</span>, <span class="string">&quot;/actuator&quot;</span></span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Void&gt; <span class="title function_">filter</span><span class="params">(ServerWebExchange exchange, GatewayFilterChain chain)</span> &#123;</span><br><span class="line">        <span class="type">ServerHttpRequest</span> <span class="variable">request</span> <span class="operator">=</span> exchange.getRequest();</span><br><span class="line">        <span class="type">String</span> <span class="variable">path</span> <span class="operator">=</span> request.getURI().getPath();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 1. 白名单</span></span><br><span class="line">        <span class="keyword">if</span> (isWhiteListed(path)) <span class="keyword">return</span> chain.filter(exchange);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2. 提取Token</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeaders().getFirst(<span class="string">&quot;token&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(token)) &#123;</span><br><span class="line">            token = request.getQueryParams().getFirst(<span class="string">&quot;token&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(token)) <span class="keyword">return</span> unauthorized(exchange);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3. 解析Token</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">audience</span> <span class="operator">=</span> JWT.decode(token).getAudience().get(<span class="number">0</span>);</span><br><span class="line">        String[] split = audience.split(<span class="string">&quot;-&quot;</span>);</span><br><span class="line">        <span class="type">String</span> <span class="variable">userId</span> <span class="operator">=</span> split[<span class="number">0</span>];</span><br><span class="line">        <span class="type">String</span> <span class="variable">role</span> <span class="operator">=</span> split[<span class="number">1</span>];</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 4. 从Redis获取密码验证签名</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> (String) redisTemplate.opsForValue()</span><br><span class="line">                .get(<span class="string">&quot;token:password:&quot;</span> + audience);</span><br><span class="line">        <span class="keyword">if</span> (password == <span class="literal">null</span>) <span class="keyword">return</span> unauthorized(exchange);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 5. 验证JWT签名</span></span><br><span class="line">        <span class="type">JWTVerifier</span> <span class="variable">verifier</span> <span class="operator">=</span> JWT.require(Algorithm.HMAC256(password)).build();</span><br><span class="line">        verifier.verify(token);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 6. IP绑定验证</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">clientIp</span> <span class="operator">=</span> getClientIp(request);</span><br><span class="line">        <span class="type">String</span> <span class="variable">storedIp</span> <span class="operator">=</span> (String) redisTemplate.opsForValue().get(<span class="string">&quot;token:&quot;</span> + audience);</span><br><span class="line">        <span class="keyword">if</span> (storedIp == <span class="literal">null</span> || !clientIp.equals(storedIp)) &#123;</span><br><span class="line">            <span class="keyword">return</span> unauthorized(exchange);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 7. ★ 用户信息透传给下游微服务</span></span><br><span class="line">        <span class="type">ServerHttpRequest</span> <span class="variable">mutatedRequest</span> <span class="operator">=</span> request.mutate()</span><br><span class="line">                .header(<span class="string">&quot;X-User-Id&quot;</span>, userId)</span><br><span class="line">                .header(<span class="string">&quot;X-User-Role&quot;</span>, role)</span><br><span class="line">                .build();</span><br><span class="line">        <span class="keyword">return</span> chain.filter(exchange.mutate().request(mutatedRequest).build());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">getOrder</span><span class="params">()</span> &#123; <span class="keyword">return</span> -<span class="number">1</span>; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-6-用户信息透传原理"><a href="#3-6-用户信息透传原理" class="headerlink" title="3.6 用户信息透传原理"></a>3.6 用户信息透传原理</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">客户端                    网关                        下游服务</span><br><span class="line">  │── 请求(Token) ────────&gt;│                             │</span><br><span class="line">  │                        │ 解析Token获取userId/role     │</span><br><span class="line">  │                        │ 添加 X-User-Id Header ──────&gt;│</span><br><span class="line">  │                        │ 添加 X-User-Role Header ─────&gt;│</span><br><span class="line">  │                        │                             │ request.getHeader(&quot;X-User-Id&quot;)</span><br><span class="line">  │&lt;── 响应 ──────────────│&lt;─────────────────────────────│</span><br></pre></td></tr></table></figure><p>下游服务不需要再次解析 JWT——网关统一鉴权后，通过自定义 Header 传递用户信息。</p><hr><h2 id="第四章：OpenFeign-声明式调用"><a href="#第四章：OpenFeign-声明式调用" class="headerlink" title="第四章：OpenFeign 声明式调用"></a>第四章：OpenFeign 声明式调用</h2><h3 id="4-1-Feign-客户端接口"><a href="#4-1-Feign-客户端接口" class="headerlink" title="4.1 Feign 客户端接口"></a>4.1 Feign 客户端接口</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@FeignClient(</span></span><br><span class="line"><span class="meta">    name = &quot;cgb-product-service&quot;,                    // 目标服务名</span></span><br><span class="line"><span class="meta">    contextId = &quot;product&quot;,                           // 上下文ID（防Bean名冲突）</span></span><br><span class="line"><span class="meta">    fallbackFactory = FeignProductServiceFallbackFactory.class  // 降级工厂</span></span><br><span class="line"><span class="meta">)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">FeignProductService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/shangpin/internal/productDetail&quot;)</span></span><br><span class="line">    R&lt;?&gt; getProductDetail(<span class="meta">@RequestParam(&quot;id&quot;)</span> Long id);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostMapping(&quot;/shangpin/internal/decreaseStock&quot;)</span></span><br><span class="line">    R&lt;?&gt; decreaseStock(<span class="meta">@RequestParam(&quot;id&quot;)</span> Long id, <span class="meta">@RequestParam(&quot;quantity&quot;)</span> Integer quantity);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostMapping(&quot;/shangpin/internal/increaseStock&quot;)</span></span><br><span class="line">    R&lt;?&gt; increaseStock(<span class="meta">@RequestParam(&quot;id&quot;)</span> Long id, <span class="meta">@RequestParam(&quot;quantity&quot;)</span> Integer quantity);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>像调本地方法一样调远程接口</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderServiceImpl</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> FeignProductService feignProductService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">createOrder</span><span class="params">(Long productId, Integer quantity)</span> &#123;</span><br><span class="line">        R&lt;?&gt; result = feignProductService.decreaseStock(productId, quantity);</span><br><span class="line">        <span class="keyword">if</span> (result.getCode() != <span class="number">0</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;库存扣减失败&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-2-Fallback-降级"><a href="#4-2-Fallback-降级" class="headerlink" title="4.2 Fallback 降级"></a>4.2 Fallback 降级</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FeignProductServiceFallbackFactory</span> </span><br><span class="line">        <span class="keyword">implements</span> <span class="title class_">FallbackFactory</span>&lt;FeignProductService&gt; &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> FeignProductService <span class="title function_">create</span><span class="params">(Throwable cause)</span> &#123;</span><br><span class="line">        log.error(<span class="string">&quot;商品服务调用失败&quot;</span>, cause);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">FeignProductService</span>() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> R&lt;?&gt; decreaseStock(Long id, Integer quantity) &#123;</span><br><span class="line">                <span class="keyword">return</span> R.fail(<span class="string">&quot;商品服务暂不可用，扣减库存失败&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> R&lt;?&gt; increaseStock(Long id, Integer quantity) &#123;</span><br><span class="line">                <span class="keyword">return</span> R.fail(<span class="string">&quot;商品服务暂不可用，回补库存失败&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> R&lt;?&gt; getProductDetail(Long id) &#123;</span><br><span class="line">                <span class="keyword">return</span> R.fail(<span class="string">&quot;商品服务暂不可用&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>降级触发场景</strong>：服务宕机、超时、HTTP 500、Sentinel 熔断器打开。</p><h3 id="4-3-内部接口鉴权"><a href="#4-3-内部接口鉴权" class="headerlink" title="4.3 内部接口鉴权"></a>4.3 内部接口鉴权</h3><p>微服务的 <code>/internal/**</code> 接口只允许服务间调用：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Feign拦截器：自动注入内部Token</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">InternalAuthFeignInterceptor</span> <span class="keyword">implements</span> <span class="title class_">RequestInterceptor</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">apply</span><span class="params">(RequestTemplate template)</span> &#123;</span><br><span class="line">        template.header(InternalAuthConstants.HEADER_NAME, InternalAuthConstants.TOKEN);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 服务端过滤器：验证内部Token</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">InternalAuthFilter</span> <span class="keyword">extends</span> <span class="title class_">OncePerRequestFilter</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doFilterInternal</span><span class="params">(HttpServletRequest request, ...)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (request.getRequestURI().contains(<span class="string">&quot;/internal/&quot;</span>)) &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(InternalAuthConstants.HEADER_NAME);</span><br><span class="line">            <span class="keyword">if</span> (!InternalAuthConstants.isValid(token)) &#123;</span><br><span class="line">                response.setStatus(<span class="number">401</span>);</span><br><span class="line">                response.getWriter().write(<span class="string">&quot;内部接口无权访问&quot;</span>);</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        filterChain.doFilter(request, response);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-4-超时配置"><a href="#4-4-超时配置" class="headerlink" title="4.4 超时配置"></a>4.4 超时配置</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">openfeign:</span></span><br><span class="line">      <span class="attr">client:</span></span><br><span class="line">        <span class="attr">config:</span></span><br><span class="line">          <span class="attr">default:</span></span><br><span class="line">            <span class="attr">connect-timeout:</span> <span class="number">3000</span>    <span class="comment"># 连接超时3秒</span></span><br><span class="line">            <span class="attr">read-timeout:</span> <span class="number">10000</span>     <span class="comment"># 读超时10秒</span></span><br><span class="line">          <span class="attr">cgb-product-service:</span></span><br><span class="line">            <span class="attr">read-timeout:</span> <span class="number">5000</span>      <span class="comment"># 商品服务5秒</span></span><br></pre></td></tr></table></figure><hr><h2 id="第五章：多语言微服务"><a href="#第五章：多语言微服务" class="headerlink" title="第五章：多语言微服务"></a>第五章：多语言微服务</h2><h3 id="5-1-Java-Python-TypeScript-架构"><a href="#5-1-Java-Python-TypeScript-架构" class="headerlink" title="5.1 Java + Python + TypeScript 架构"></a>5.1 Java + Python + TypeScript 架构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">      ┌─────────────┐</span><br><span class="line">      │  Vue 3 前端  │ (TypeScript)</span><br><span class="line">      └──────┬──────┘</span><br><span class="line">      ┌──────▼──────┐</span><br><span class="line">      │ API Gateway  │ (Java + Spring Cloud Gateway)</span><br><span class="line">      └──────┬──────┘</span><br><span class="line">      ┌──────▼──────┐</span><br><span class="line">      │   Nacos     │ ← 服务注册中心</span><br><span class="line">      └──┬──────┬───┘</span><br><span class="line">┌────────▼┐   ┌─▼────────────┐</span><br><span class="line">│ Java     │   │ Python        │</span><br><span class="line">│ user-svc │   │ demo-service  │</span><br><span class="line">└──────────┘   └──────────────┘</span><br></pre></td></tr></table></figure><h3 id="5-2-Python-服务注册到-Nacos"><a href="#5-2-Python-服务注册到-Nacos" class="headerlink" title="5.2 Python 服务注册到 Nacos"></a>5.2 Python 服务注册到 Nacos</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> flask <span class="keyword">import</span> Flask, jsonify</span><br><span class="line"><span class="keyword">import</span> requests, threading, time</span><br><span class="line"></span><br><span class="line">app = Flask(__name__)</span><br><span class="line">NACOS_ADDR = <span class="string">&quot;localhost:8848&quot;</span></span><br><span class="line">SERVICE_NAME = <span class="string">&quot;python-demo-service&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">register_to_nacos</span>():</span><br><span class="line">    data = &#123;</span><br><span class="line">        <span class="string">&quot;serviceName&quot;</span>: SERVICE_NAME,</span><br><span class="line">        <span class="string">&quot;ip&quot;</span>: <span class="string">&quot;127.0.0.1&quot;</span>,</span><br><span class="line">        <span class="string">&quot;port&quot;</span>: <span class="number">5000</span>,</span><br><span class="line">        <span class="string">&quot;groupName&quot;</span>: <span class="string">&quot;DEFAULT_GROUP&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">    requests.post(<span class="string">f&quot;http://<span class="subst">&#123;NACOS_ADDR&#125;</span>/nacos/v1/ns/instance&quot;</span>, data=data)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">heartbeat</span>():</span><br><span class="line">    <span class="string">&quot;&quot;&quot;每5秒发送心跳&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            requests.put(</span><br><span class="line">                <span class="string">f&quot;http://<span class="subst">&#123;NACOS_ADDR&#125;</span>/nacos/v1/ns/instance/beat&quot;</span>,</span><br><span class="line">                data=&#123;<span class="string">&quot;serviceName&quot;</span>: SERVICE_NAME, <span class="string">&quot;ip&quot;</span>: <span class="string">&quot;127.0.0.1&quot;</span>, <span class="string">&quot;port&quot;</span>: <span class="number">5000</span>&#125;)</span><br><span class="line">        <span class="keyword">except</span>: <span class="keyword">pass</span></span><br><span class="line">        time.sleep(<span class="number">5</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.route(<span class="params"><span class="string">&quot;/api/demo/data&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_data</span>():</span><br><span class="line">    <span class="keyword">return</span> jsonify(&#123;<span class="string">&quot;data&quot;</span>: [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>], <span class="string">&quot;source&quot;</span>: <span class="string">&quot;python-flask&quot;</span>&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    register_to_nacos()</span><br><span class="line">    threading.Thread(target=heartbeat, daemon=<span class="literal">True</span>).start()</span><br><span class="line">    app.run(host=<span class="string">&quot;0.0.0.0&quot;</span>, port=<span class="number">5000</span>)</span><br></pre></td></tr></table></figure><h3 id="5-3-网关统一路由"><a href="#5-3-网关统一路由" class="headerlink" title="5.3 网关统一路由"></a>5.3 网关统一路由</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="attr">routes:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">user-service</span></span><br><span class="line">          <span class="attr">uri:</span> <span class="string">lb://user-service</span>          <span class="comment"># Java服务</span></span><br><span class="line">          <span class="attr">predicates:</span> [<span class="string">Path=/api/users/**</span>]</span><br><span class="line">        <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">python-demo-service</span></span><br><span class="line">          <span class="attr">uri:</span> <span class="string">lb://python-demo-service</span>    <span class="comment"># Python服务</span></span><br><span class="line">          <span class="attr">predicates:</span> [<span class="string">Path=/api/demo/**</span>]</span><br></pre></td></tr></table></figure><p><strong>关键点</strong>：<code>lb://python-demo-service</code> 从 Nacos 获取 Python 服务的实例列表并负载均衡。网关不关心服务用什么语言写。</p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><table><thead><tr><th>组件</th><th>作用</th><th>关键配置</th></tr></thead><tbody><tr><td>Nacos</td><td>服务注册 + 配置中心</td><td><code>spring.cloud.nacos.discovery/config</code></td></tr><tr><td>Gateway</td><td>路由 + 鉴权 + 透传</td><td>routes + GlobalFilter</td></tr><tr><td>OpenFeign</td><td>声明式调用 + 降级</td><td><code>@FeignClient</code> + <code>fallbackFactory</code></td></tr><tr><td>内部鉴权</td><td><code>/internal/**</code> 接口保护</td><td><code>InternalAuthFeignInterceptor</code></td></tr><tr><td>多语言</td><td>Java + Python 混合</td><td>Nacos 统一注册</td></tr></tbody></table><p><strong>核心思想</strong>：微服务的基础设施层解决”服务在哪里”（Nacos）、”请求怎么到”（Gateway）、”服务怎么调”（Feign）三个问题。语言差异被 HTTP + JSON 标准化了。</p>]]>
    </content>
    <id>http://example.com/2026/07/15/20260715/2026071501/</id>
    <link href="http://example.com/2026/07/15/20260715/2026071501/"/>
    <published>2026-07-15T00:28:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>同一个社区团购业务，单体版 1 个应用，微服务版 6 个服务。怎么拆？服务怎么互相发现？请求怎么路由？跨服务调用怎么降级？本文用 3 个真实微服务项目的代码，把 Spring Cloud 的基础设施层从头到尾讲透。</p>
</blockquote]]>
    </summary>
    <title>Spring Cloud 微服务架构全解（上）：拆分、注册、网关与调用</title>
    <updated>2026-07-15T01:06:57.094Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="安全操作系统" scheme="http://example.com/categories/%E5%AE%89%E5%85%A8%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    <category term="GrapheneOS" scheme="http://example.com/tags/GrapheneOS/"/>
    <category term="渗透测试" scheme="http://example.com/tags/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="信息收集" scheme="http://example.com/tags/%E4%BF%A1%E6%81%AF%E6%94%B6%E9%9B%86/"/>
    <content>
      <![CDATA[<blockquote><p>这不是一篇简单的”刷机教程”。本文从项目起源、安全架构、隐私机制、功能特性、安装方法到横向对比，事无巨细地拆解 GrapheneOS——目前公认最安全的安卓操作系统。</p></blockquote><hr><h2 id="一、GrapheneOS-是什么"><a href="#一、GrapheneOS-是什么" class="headerlink" title="一、GrapheneOS 是什么"></a>一、GrapheneOS 是什么</h2><p>GrapheneOS 是一款基于 <strong>AOSP（Android 开源项目）</strong> 的开源移动操作系统，兼容 Android 应用，是一个非营利<a href="https://grapheneos.org/source">开源</a>项目。以<strong>隐私和安全</strong>为核心设计目标。它由加拿大开发者 <strong>Daniel Micay</strong> 主导创建，它致力于隐私和安全技术的研究与开发，包括对沙箱、漏洞缓解和权限模型的重大改进。前身是 CopperheadOS 的社区分支，后独立发展成为今天的 GrapheneOS。</p><p>一句话定位：<strong>保留 Android 的应用生态兼容性，但从内核到应用层全面加固，同时彻底剥离 Google 的数据采集链路。</strong></p><p>它的几个关键标签：</p><table><thead><tr><th>标签</th><th>说明</th></tr></thead><tbody><tr><td>开源</td><td>基于 AOSP，代码完全公开，可自行构建</td></tr><tr><td>非营利</td><td>项目保持商业化距离，避免利益冲突</td></tr><tr><td>Pixel 专属</td><td>仅支持 Google Pixel 系列手机（原因后文详解）</td></tr><tr><td>去 Google 化</td><td>默认不含任何 Google 服务和应用</td></tr><tr><td>安全加固</td><td>从内存分配器到内核，系统性硬化</td></tr></tbody></table><hr><h2 id=""><a href="#" class="headerlink" title=""></a><img src="/../images/2026-07-08_08-14-26.jpg" alt="2026-07-08_08-14-26.jpg"></h2><h2 id="二、为什么只支持-Pixel"><a href="#二、为什么只支持-Pixel" class="headerlink" title="二、为什么只支持 Pixel"></a>二、为什么只支持 Pixel</h2><p>这是被问得最多的问题。GrapheneOS 只支持 Google Pixel 系列，不是因为”和 Google 关系好”，而是因为<strong>安全需要硬件级支撑</strong>：</p><ol><li><strong>Titan M&#x2F;M2 安全芯片</strong>：Pixel 内置的专用安全芯片，用于加密密钥存储、Verified Boot 验证。其他厂商手机的芯片方案不够开放或文档不足。</li><li><strong>可解锁的 Bootloader</strong>：Pixel 允许用户解锁 bootloader 刷入自定义系统，刷完后还能重新锁定——这是 Verified Boot 生效的前提。大多数厂商手机要么不能解锁，要么解锁后无法重新锁定。</li><li><strong>长期安全更新承诺</strong>：Google 为 Pixel 提供 5 年以上的安全补丁，GrapheneOS 依赖上游补丁来维持安全基线。</li><li><strong>AOSP 参考设备</strong>：Pixel 是 AOSP 的首选参考平台，驱动和固件文档最完整，兼容性最好。</li></ol><p>截至本文写作时，支持的设备覆盖 <strong>Pixel 6 系列到 Pixel 10 系列</strong>，包括折叠屏 Pixel Fold 和平板 Pixel Tablet。第 4、5 代 Pixel 已接近或到达生命周期末期。</p><blockquote><p>这意味着：想用 GrapheneOS，你得先有一台 Pixel 6 及以上的手机。</p></blockquote><hr><h2 id="三、核心理念：零信任安全架构"><a href="#三、核心理念：零信任安全架构" class="headerlink" title="三、核心理念：零信任安全架构"></a>三、核心理念：零信任安全架构</h2><p>GrapheneOS 的安全哲学不是”在应用层加一层防护”，而是构建一套**”从不信任，始终验证”的零信任架构**。它的防御策略分三道防线：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">第一道：攻击面缩减 —— 能关的都关，能删的都删</span><br><span class="line">第二道：漏洞利用缓解 —— 就算被攻破，也让攻击者寸步难行</span><br><span class="line">第三道：反持久化 —— 攻击者无法在设备上&quot;住下来&quot;</span><br></pre></td></tr></table></figure><p>这三道防线层层递进，下面逐一拆解。</p><hr><h2 id="四、攻击面缩减：把门关到最紧"><a href="#四、攻击面缩减：把门关到最紧" class="headerlink" title="四、攻击面缩减：把门关到最紧"></a>四、攻击面缩减：把门关到最紧</h2><h3 id="4-1-锁屏后禁用-USB-C-数据"><a href="#4-1-锁屏后禁用-USB-C-数据" class="headerlink" title="4.1 锁屏后禁用 USB-C 数据"></a>4.1 锁屏后禁用 USB-C 数据</h3><p>这是 GrapheneOS 最实用的安全特性之一。设备锁定后，USB-C 端口在<strong>硬件层面和操作系统层面双重阻止新的 USB 数据连接</strong>，仅保留充电。</p><p>五种模式可选：</p><table><thead><tr><th>模式</th><th>行为</th></tr></thead><tbody><tr><td>Off</td><td>最高安全，连充电都禁用（关机&#x2F;fastboot 时不适用，不会变砖）</td></tr><tr><td>Charging-only</td><td>仅充电，无数据</td></tr><tr><td><strong>Charging-only when locked</strong>（默认）</td><td>锁定后硬件层面切断数据线</td></tr><tr><td>Charging-only when locked, except before first unlock</td><td>BFU 状态下允许数据</td></tr><tr><td>On</td><td>完全开放</td></tr></tbody></table><p>技术细节上，它远比标准 Android 的 USB HAL 切换安全——后者只在操作系统高层禁用 USB 处理，不阻止新连接，不在硬件层面禁用数据线，也不禁用 USB-C 替代模式（如 DisplayPort）。</p><h3 id="4-2-其他攻击面缩减"><a href="#4-2-其他攻击面缩减" class="headerlink" title="4.2 其他攻击面缩减"></a>4.2 其他攻击面缩减</h3><ul><li><strong>可选无线模块关闭</strong>：NFC、蓝牙、UWB 等功能可按需禁用</li><li><strong>蓝牙&#x2F;Wi-Fi 超时自动关闭</strong></li><li><strong>ptrace 访问阻止</strong>：对内置应用默认阻止 ptrace（进程调试），减少本地攻击面</li><li><strong>锁定后禁用摄像头访问</strong></li><li><strong>LTE-only 模式</strong>：禁用 2G&#x2F;3G&#x2F;5G，大幅减少蜂窝无线电攻击面（2G 尤其不安全，易被伪基站利用）</li></ul><hr><h2 id="五、漏洞利用缓解：让-0-day-也难受"><a href="#五、漏洞利用缓解：让-0-day-也难受" class="headerlink" title="五、漏洞利用缓解：让 0-day 也难受"></a>五、漏洞利用缓解：让 0-day 也难受</h2><p>这是 GrapheneOS 技术含量最高的部分。即使攻击者发现了未知漏洞（0-day），GrapheneOS 的大量缓解措施也会大幅提高利用难度。</p><h3 id="5-1-Hardened-Malloc（硬化内存分配器）"><a href="#5-1-Hardened-Malloc（硬化内存分配器）" class="headerlink" title="5.1 Hardened Malloc（硬化内存分配器）"></a>5.1 Hardened Malloc（硬化内存分配器）</h3><p>GrapheneOS 自研的内存分配器 <a href="https://github.com/GrapheneOS/hardened_malloc"><code>hardened_malloc</code></a>，是整个安全架构的基石之一。内存损坏（堆溢出、use-after-free 等）是最常见的漏洞类型，hardened_malloc 针对性防御：</p><table><thead><tr><th>特性</th><th>作用</th></tr></thead><tbody><tr><td>完全离线元数据 + 损坏保护</td><td>排除传统分配器元数据篡改利用</td></tr><tr><td>独立内存区域 + 高熵随机基址</td><td>不同区域间无地址空间重用，增大预测难度</td></tr><tr><td>确定性检测无效 free</td><td>任何非法释放立即被发现</td></tr><tr><td>Free 时清零</td><td>释放的内存立即清零，减少敏感数据驻留</td></tr><tr><td>write-after-free 检测</td><td>重新分配前检查内存是否仍为零</td></tr><tr><td>延迟重用 + 随机隔离</td><td>缓解 use-after-free，延长重用周期</td></tr><tr><td>受保护内存区域</td><td>大于 16KB 的分配有保护区，128KB+ 随机化保护区大小</td></tr><tr><td>slab 级保护区</td><td>小分配的 slab 前后各 4KB 保护区</td></tr><tr><td>随机 canary + 前导零</td><td>阻止 C 字符串溢出，检测线性溢出</td></tr><tr><td>硬件内存标记（MTE）</td><td>ARMv9 上提供 use-after-free 和跨对象溢出的概率检测</td></tr></tbody></table><h3 id="5-2-加固内核"><a href="#5-2-加固内核" class="headerlink" title="5.2 加固内核"></a>5.2 加固内核</h3><p>内核是攻击者最想拿下的目标。GrapheneOS 的内核加固包括：</p><ul><li><strong>4 级页表</strong>：48 位地址空间（标准为 39 位），ASLR 熵从 24 位提升到 33 位——攻击者猜测内存布局的难度指数级增加</li><li><strong>内核硬件内存标记</strong>：检测 use-after-free 和跨对象溢出</li><li><strong>内核堆 canary</strong>：随机 canary + 前导零，检测堆溢出</li><li><strong>内存释放立即清零</strong>：页分配器和 slub 分配器双重清零，敏感数据不留痕迹</li><li><strong>启动早期清零所有未使用内存</strong>：消除上次启动遗留的数据</li><li><strong>内核栈分配清零</strong>：未初始化数据使用漏洞无害化</li><li><strong>强制内核模块签名</strong>：RSA 4096 + SHA256，锁定强制机密性模式</li><li><strong>ARMv9 启用 BTI + Clang 类型 CFI + ShadowCallStack + PAC</strong>：多重控制流完整性保护</li><li><strong>禁用 perf、ptrace</strong>等攻击面</li></ul><h3 id="5-3-JIT-和动态代码限制"><a href="#5-3-JIT-和动态代码限制" class="headerlink" title="5.3 JIT 和动态代码限制"></a>5.3 JIT 和动态代码限制</h3><p>JIT 编译器是攻击者的最爱——它需要可写可执行的内存，是利用链中的关键跳板。GrapheneOS 的处理：</p><ul><li><strong>Android Runtime JIT 完全禁用</strong>，替换为完整的提前编译（AOT）</li><li><strong>基础操作系统动态代码加载阻止</strong>：几乎所有基础系统组件的原生代码&#x2F;Java 类动态加载被阻止</li><li><strong>用户应用可选禁用</strong>三种动态代码加载：内存加载、存储加载、WebView JIT</li><li>被阻止时<strong>显示用户通知</strong>，可按应用配置</li></ul><blockquote><p>Vanadium 浏览器中，V8 JavaScript JIT 默认禁用，改用 DrumBrake WebAssembly 解释器在无 JIT 下支持 WebAssembly。</p></blockquote><hr><h2 id="六、反持久化：让攻击者待不住"><a href="#六、反持久化：让攻击者待不住" class="headerlink" title="六、反持久化：让攻击者待不住"></a>六、反持久化：让攻击者待不住</h2><p>就算攻击者成功利用漏洞获得了执行权限，GrapheneOS 也要让他无法持久化——重启就消失。</p><h3 id="6-1-增强-Verified-Boot（验证启动）"><a href="#6-1-增强-Verified-Boot（验证启动）" class="headerlink" title="6.1 增强 Verified Boot（验证启动）"></a>6.1 增强 Verified Boot（验证启动）</h3><p>Verified Boot 确保设备启动时运行的每一层固件和软件都经过加密签名验证。GrapheneOS 在标准 Android Verified Boot 基础上增强：</p><ul><li><strong>带外更新（APK）的持续验证</strong>：每次读取 APK 都验证 fs-verity 元数据签名，防止替换和降级</li><li><strong>系统组件降级保护</strong>：基于应用的系统组件在安装和启动时强制版本检查</li><li><strong>禁用压缩 APEX 模块</strong>：减少攻击面</li><li><strong>增强硬件远程证明</strong>：提供更精确的版本信息</li></ul><h3 id="6-2-Auditor-应用"><a href="#6-2-Auditor-应用" class="headerlink" title="6.2 Auditor 应用"></a>6.2 Auditor 应用</h3><p>GrapheneOS 配套的 <a href="https://attestation.app/">Auditor 应用</a> 提供基于硬件的设备真实性和完整性验证：</p><ul><li>使用配对设备的硬件密钥验证身份</li><li>软件检查在硬件信任之上分层</li><li>可远程监控设备是否被篡改</li></ul><hr><h2 id="七、隐私保护：不给任何人你的数据"><a href="#七、隐私保护：不给任何人你的数据" class="headerlink" title="七、隐私保护：不给任何人你的数据"></a>七、隐私保护：不给任何人你的数据</h2><h3 id="7-1-默认去-Google-化"><a href="#7-1-默认去-Google-化" class="headerlink" title="7.1 默认去 Google 化"></a>7.1 默认去 Google 化</h3><p>GrapheneOS 默认<strong>不含任何 Google 应用和服务</strong>。不仅如此，它还将多个默认连接 Google 服务器的系统功能<strong>重定向到 GrapheneOS 自有服务器</strong>：</p><table><thead><tr><th>功能</th><th>标准 Android</th><th>GrapheneOS 默认</th></tr></thead><tbody><tr><td>连接检查</td><td>Google 服务器</td><td>GrapheneOS 服务器</td></tr><tr><td>证明密钥配置</td><td>Google</td><td>GrapheneOS</td></tr><tr><td>GNSS 星历下载</td><td>Google XTRA</td><td>GrapheneOS</td></tr><tr><td>SUPL（定位）</td><td>运营商服务器</td><td>GrapheneOS 代理</td></tr><tr><td>网络时间</td><td>Google</td><td>GrapheneOS（认证加密）</td></tr><tr><td>浏览器组件更新</td><td>Google</td><td>GrapheneOS</td></tr></tbody></table><p>所有切换都可手动改回 Google 或完全禁用。</p><h3 id="7-2-网络权限开关"><a href="#7-2-网络权限开关" class="headerlink" title="7.2 网络权限开关"></a>7.2 网络权限开关</h3><p>这是 GrapheneOS 独有的杀手级隐私功能——一个全局的 <strong>Network 权限开关</strong>，可以禁止任何应用直接和间接访问网络：</p><ul><li>设备本地网络（localhost）也受保护——防止应用跨配置文件通信</li><li>与防火墙不同，它阻止应用通过操作系统或其他应用 API 使用网络</li><li><strong>禁用时假装网络断开</strong>：返回”网络连接问题”错误，避免应用检测到权限被撤销</li><li>安装应用时在确认页面显示网络开关</li></ul><h3 id="7-3-传感器权限开关"><a href="#7-3-传感器权限开关" class="headerlink" title="7.3 传感器权限开关"></a>7.3 传感器权限开关</h3><p>标准 Android 的权限体系有漏洞：Camera、Microphone、Body Sensors 之外的其他传感器（加速度计、陀螺仪、指南针、气压计等）<strong>没有权限控制</strong>。这些传感器可以用于侧信道攻击和设备指纹。</p><p>GrapheneOS 补上了这个缺口——一个传感器总开关，禁用后应用收到归零数据且不接收事件。</p><h3 id="7-4-Storage-Scopes（存储范围）"><a href="#7-4-Storage-Scopes（存储范围）" class="headerlink" title="7.4 Storage Scopes（存储范围）"></a>7.4 Storage Scopes（存储范围）</h3><p>标准 Android 的存储权限是”全有或全无”——要么给应用完整存储访问，要么不给。GrapheneOS 的 Storage Scopes 是完全兼容的替代方案：</p><ul><li>应用<strong>以为</strong>自己拥有所有存储权限</li><li>实际上只能访问用户手动选定的文件&#x2F;目录</li><li>没有存储权限的应用仍可创建文件和访问自己创建的文件</li></ul><h3 id="7-5-Contact-Scopes（联系人范围）"><a href="#7-5-Contact-Scopes（联系人范围）" class="headerlink" title="7.5 Contact Scopes（联系人范围）"></a>7.5 Contact Scopes（联系人范围）</h3><p>类似的精细化控制——不授 Contacts 权限，而是选择性授予对特定联系人的访问。</p><h3 id="7-6-Wi-Fi-隐私"><a href="#7-6-Wi-Fi-隐私" class="headerlink" title="7.6 Wi-Fi 隐私"></a>7.6 Wi-Fi 隐私</h3><ul><li><strong>按连接 MAC 随机化</strong>（默认启用）：比标准 Android 的”每网络持久随机 MAC”更隐私——每次连接都换新 MAC</li><li>重新连接前刷新 DHCP 状态，避免暴露设备身份</li><li>修复 Linux 内核 IPv6 隐私地址的严重缺陷</li></ul><h3 id="7-7-私密截图"><a href="#7-7-私密截图" class="headerlink" title="7.7 私密截图"></a>7.7 私密截图</h3><ul><li>禁用 EXIF Software 标签（标准 Android 会泄露操作系统版本和设备型号）</li><li>默认禁用 EXIF 日期&#x2F;时间&#x2F;时区标签（避免通过元数据泄露时间和准位置）</li></ul><h3 id="7-8-其他隐私特性"><a href="#7-8-其他隐私特性" class="headerlink" title="7.8 其他隐私特性"></a>7.8 其他隐私特性</h3><table><thead><tr><th>特性</th><th>说明</th></tr></thead><tbody><tr><td>PIN 混淆</td><td>提高通过物理接近或侧信道发现 PIN 的难度</td></tr><tr><td>双因素指纹解锁</td><td>指纹通过后还需输入第二因素 PIN</td></tr><tr><td>更长密码</td><td>支持最长 128 字符密码（标准为 16）</td></tr><tr><td>位置访问指示器</td><td>位置权限使用时显示绿色指示灯（标准 Android 仅有摄像头和麦克风指示器）</td></tr><tr><td>权限提示延迟</td><td>权限弹窗 1 秒延迟激活，防止应用用覆盖按钮欺骗用户</td></tr><tr><td>移除不受信任根 CA</td><td>移除 Chunghwa Telecom 和 Netlock 根证书</td></tr></tbody></table><hr><h2 id="八、沙箱化-Google-Play：天才的兼容方案"><a href="#八、沙箱化-Google-Play：天才的兼容方案" class="headerlink" title="八、沙箱化 Google Play：天才的兼容方案"></a>八、沙箱化 Google Play：天才的兼容方案</h2><p>这是 GrapheneOS 最被称道的设计。很多人想要隐私，但又离不开 Google 生态——银行 App、Google Maps、依赖推送通知的应用都需要 Google Play 服务。</p><p>GrapheneOS 的解决方案不是 microG（第三方逆向实现），也不是预装 Google Play，而是：</p><blockquote><p><strong>把官方 Google Play 当作普通应用，跑在标准应用沙箱里。</strong></p></blockquote><h3 id="工作原理"><a href="#工作原理" class="headerlink" title="工作原理"></a>工作原理</h3><ul><li>Google Play <strong>不获得任何特殊访问或权限</strong>——不像在其他系统上绕过沙箱获得大量高权限</li><li>兼容层教 Google Play 如何在完整沙箱内工作</li><li>即使安装了 Google Play，它也<strong>不做操作系统服务的后端</strong></li><li>仅作为常规应用存在于特定用户&#x2F;工作配置文件中，仅同配置文件内的应用可使用</li><li>需要用户明确同意才能获得配置文件数据或标准权限</li></ul><h3 id="功能覆盖"><a href="#功能覆盖" class="headerlink" title="功能覆盖"></a>功能覆盖</h3><p>接近完全功能，包括：</p><ul><li>大多数 Play 服务功能完美运行（dynamite modules、Google Play Games 等）</li><li>Play Store 完整支持：应用内购买、Play Asset Delivery、许可证检查</li><li>应用安装、更新、卸载（需用户授权）</li><li>无人值守自动更新（仅限 Play Store 来源的应用）</li><li><strong>默认将位置请求重新路由到 GrapheneOS 自己的地理定位服务</strong>（可禁用）</li></ul><h3 id="Android-Auto"><a href="#Android-Auto" class="headerlink" title="Android Auto"></a>Android Auto</h3><p>GrapheneOS 也支持官方 Android Auto——通过沙箱化 Google Play 兼容层的扩展，以降低的权限级别运行。</p><blockquote><p>一句话总结：你可以在享受完整应用兼容性的同时，让 Google Play 拿不到你的系统级权限。要 Google 服务的时候开，不要的时候关，随时卸载。</p></blockquote><hr><h2 id="九、Vanadium：自带硬核浏览器"><a href="#九、Vanadium：自带硬核浏览器" class="headerlink" title="九、Vanadium：自带硬核浏览器"></a>九、Vanadium：自带硬核浏览器</h2><p>GrapheneOS 默认浏览器 Vanadium 是基于 Chromium 的加固版本，安全特性包括：</p><table><thead><tr><th>特性</th><th>说明</th></tr></thead><tbody><tr><td>硬件内存标记（MTE）</td><td>主分配器启用</td></tr><tr><td>类型基础 CFI</td><td>控制流完整性</td></tr><tr><td>强栈保护器</td><td>检测栈溢出</td></tr><tr><td>自动零初始化变量</td><td>防止未初始化数据泄露</td></tr><tr><td><strong>JavaScript JIT 默认禁用</strong></td><td>按站点可开启，大幅减少攻击面</td></tr><tr><td>DrumBrake WebAssembly 解释器</td><td>无 JIT 下支持 WebAssembly</td></tr><tr><td>严格站点隔离</td><td>每个站点独立进程</td></tr><tr><td>内容过滤引擎</td><td>使用 EasyList&#x2F;EasyPrivacy，按站点切换</td></tr><tr><td>第三方 Cookie 默认禁用</td><td>—</td></tr><tr><td>支付支持默认禁用</td><td>—</td></tr><tr><td>后台同步默认禁用</td><td>—</td></tr><tr><td>DRM 默认禁用</td><td>—</td></tr><tr><td>WebGPU 禁用</td><td>攻击面缩减</td></tr><tr><td>Battery API 伪造</td><td>始终显示充电中且 100%</td></tr><tr><td>用户代理缩减</td><td>不泄露设备型号和版本</td></tr><tr><td>混合后量子密码学</td><td>默认启用</td></tr><tr><td>几乎所有远程服务禁用</td><td>默认仅连接 GrapheneOS 服务器</td></tr></tbody></table><p>Vanadium 不支持浏览器扩展——这与站点隔离和反指纹策略冲突。</p><hr><h2 id="十、胁迫密码（Duress-PIN-Password）"><a href="#十、胁迫密码（Duress-PIN-Password）" class="headerlink" title="十、胁迫密码（Duress PIN&#x2F;Password）"></a>十、胁迫密码（Duress PIN&#x2F;Password）</h2><p>GrapheneOS 提供胁迫密码功能，设计用于<strong>被胁迫交出密码的场景</strong>（如被抢劫、被绑架时被迫解锁手机）。</p><h3 id="机制"><a href="#机制" class="headerlink" title="机制"></a>机制</h3><ul><li>用户设置一个胁迫 PIN 和&#x2F;或胁迫密码</li><li>一旦在<strong>任何要求设备凭据的地方</strong>输入胁迫密码，设备<strong>不可逆地擦除所有数据</strong>（包括已安装的 eSIM）</li><li>擦除在后台静默进行，不需要重启，无法中断</li><li>对方看不到任何提示</li></ul><h3 id="触发范围"><a href="#触发范围" class="headerlink" title="触发范围"></a>触发范围</h3><p>不仅是锁屏——系统内任何 PIN&#x2F;密码输入点都会触发：</p><ul><li>锁屏界面</li><li>修改密码时的”确认当前密码”</li><li>开启开发者选项时的 PIN 输入</li><li>应用内 PIN 校验弹窗</li><li>双因素指纹解锁的 PIN 输入</li></ul><p>唯一例外是 SIM 卡 PIN（基带侧，与系统凭据无关）。</p><h3 id="设置"><a href="#设置" class="headerlink" title="设置"></a>设置</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Settings → Security &amp; privacy → Device unlock → Duress Password</span><br></pre></td></tr></table></figure><ul><li>PIN 模式设胁迫 PIN（纯数字）</li><li>Password 模式设胁迫密码（字母数字）</li><li>两者都需设置才能完整启用</li><li>如果胁迫密码与真实密码相同，真实密码始终优先，不会误擦</li></ul><h3 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h3><ul><li><strong>不可逆</strong>：一旦触发，数据无法恢复</li><li><strong>eSIM 一起被擦除</strong>：需要重新去运营商激活</li><li><strong>擦除需要时间</strong>：后台擦除非瞬间完成</li><li><strong>自身风险</strong>：自己手滑输错也会擦——胁迫密码要与真密码差异足够大</li></ul><blockquote><p>这个功能保护的是”被歹徒胁迫交密码”这种非法胁迫场景。在合法执法场景下使用它销毁数据，在很多法域可能构成独立的违法行为（如妨碍公务、毁灭证据），需要自行承担法律后果。</p></blockquote><hr><h2 id="十一、自动重启：BFU-是最安全的状态"><a href="#十一、自动重启：BFU-是最安全的状态" class="headerlink" title="十一、自动重启：BFU 是最安全的状态"></a>十一、自动重启：BFU 是最安全的状态</h2><p>GrapheneOS 的自动重启功能确保设备不会长时间停留在已解锁状态：</p><ul><li>设备锁定后经过设定时间<strong>自动重启</strong></li><li>重启后进入 **BFU（Before First Unlock）**状态——加密密钥从内存中清除</li><li>默认 18 小时，可设置 10 分钟到 72 小时</li><li>在 init 进程中实现，无法通过系统进程崩溃绕过</li><li><strong>不适用于 BFU 状态</strong>——不会导致设备持续重启</li></ul><h3 id="为什么-BFU-重要"><a href="#为什么-BFU-重要" class="headerlink" title="为什么 BFU 重要"></a>为什么 BFU 重要</h3><p>设备解锁后（AFU 状态），加密密钥驻留在内存中，理论上可被物理取证提取。BFU 状态下密钥已被清除，即使设备被拿走，面对的是全盘加密——暴力破解几乎不可能。</p><p>配合内存清理功能：设备锁定时为 SystemUI 和 system_server 触发完整垃圾回收，释放不再使用的内存，内核页分配器清零所有不再引用的对象数据。</p><hr><h2 id="十二、用户配置文件系统"><a href="#十二、用户配置文件系统" class="headerlink" title="十二、用户配置文件系统"></a>十二、用户配置文件系统</h2><p>GrapheneOS 增强了 Android 的多用户能力：</p><table><thead><tr><th>特性</th><th>说明</th></tr></thead><tbody><tr><td>更多配置文件</td><td>上限从 4 个（3+访客）提升到 32 个（31+访客）</td></tr><tr><td>结束会话</td><td>不需设备管理器即可登出配置文件，清除加密密钥恢复静止状态</td></tr><tr><td>禁用应用安装</td><td>可在 Owner 配置文件装好应用后禁止辅助用户安装</td></tr><tr><td>安装可用应用</td><td>Owner 可安装其他用户已有的包，无需重新下载</td></tr><tr><td>通知转发</td><td>支持从后台用户向活跃用户转发通知</td></tr></tbody></table><p>实践中的应用：把敏感数据放在 Owner 配置文件，日常使用放在辅助配置文件。辅助配置文件登出后，其中的应用无法运行，加密密钥被清除。</p><hr><h2 id="十三、VPN-泄漏防护"><a href="#十三、VPN-泄漏防护" class="headerlink" title="十三、VPN 泄漏防护"></a>十三、VPN 泄漏防护</h2><p>GrapheneOS 大幅改进了 Android 的 VPN 泄漏保护——这是很多人忽略但极其重要的安全细节：</p><ul><li><strong>防止 DNS 查询泄漏</strong>：标准 Android 在 VPN 下线时允许 DNS 查询泄漏到网络 DNS 服务器</li><li><strong>阻止多播数据包绕过</strong>：标准 Android 允许进程通过多播数据包完全绕过 VPN</li><li><strong>VPN 锁定时禁用 mDNS</strong></li><li><strong>多播防火墙</strong>：防止进程通过其他配置文件的 VPN 隧道发送数据包</li><li><strong>关闭 eBPF 防火墙漏洞</strong>：防止通过指定接口绕过 VPN</li><li>VPN 默认启用 “Always-on VPN” 和 “Block connections without VPN”</li></ul><hr><h2 id="十四、其他实用功能"><a href="#十四、其他实用功能" class="headerlink" title="十四、其他实用功能"></a>十四、其他实用功能</h2><h3 id="14-1-加密备份"><a href="#14-1-加密备份" class="headerlink" title="14.1 加密备份"></a>14.1 加密备份</h3><p>通过集成 Seedvault 实现加密备份，支持本地备份和云存储。</p><h3 id="14-2-通话录音"><a href="#14-2-通话录音" class="headerlink" title="14.2 通话录音"></a>14.2 通话录音</h3><p>Dialer 应用内置通话录音，录音存储在 <code>Recordings/Call Recordings</code>，无地区限制。</p><h3 id="14-3-更精确的系统时钟"><a href="#14-3-更精确的系统时钟" class="headerlink" title="14.3 更精确的系统时钟"></a>14.3 更精确的系统时钟</h3><p>系统时钟时间更新阈值从 2000ms 降到 50ms，漂移警告从 2000ms 降到 250ms——对 TOTP 等基于时间的认证协议很重要。</p><h3 id="14-4-日志查看器"><a href="#14-4-日志查看器" class="headerlink" title="14.4 日志查看器"></a>14.4 日志查看器</h3><p>设置中内置日志查看器，支持按级别、缓冲区和文本搜索过滤。GrapheneOS 因隐私原因不实现自动化崩溃报告，取而代之的是用户面向的崩溃报告和日志查看。</p><h3 id="14-5-GrapheneOS-应用仓库"><a href="#14-5-GrapheneOS-应用仓库" class="headerlink" title="14.5 GrapheneOS 应用仓库"></a>14.5 GrapheneOS 应用仓库</h3><p>自有的应用仓库客户端，用于分发 GrapheneOS 自有应用和 Google Play 镜像。</p><h3 id="14-6-网络定位服务"><a href="#14-6-网络定位服务" class="headerlink" title="14.6 网络定位服务"></a>14.6 网络定位服务</h3><p>GrapheneOS 自有的基于网络的位置服务——类似 Apple&#x2F;Google 的服务，但隐私得多：</p><ul><li>仅发送每个网络的标识符，不需要发送距离估计</li><li>本地位置估计，数据在内存中缓存最多 15 分钟，可离线运行</li><li>可选择代理到 Apple 服务或直接使用 Apple 服务</li></ul><hr><h2 id="十五、安装方法"><a href="#十五、安装方法" class="headerlink" title="十五、安装方法"></a>十五、安装方法</h2><p>GrapheneOS 提供两种官方安装方式：</p><h3 id="方式一：Web-安装器（推荐）"><a href="#方式一：Web-安装器（推荐）" class="headerlink" title="方式一：Web 安装器（推荐）"></a>方式一：Web 安装器（推荐）</h3><p>基于 WebUSB 的浏览器安装器，<strong>不需要安装任何额外软件</strong>，只需一个支持 WebUSB 的浏览器（Chrome&#x2F;Chromium&#x2F;Edge）。</p><p>大致流程：</p><ol><li>在 Pixel 上启用 OEM 解锁</li><li>连接电脑，在浏览器中打开 GrapheneOS Web 安装器</li><li>解锁 bootloader</li><li>下载 GrapheneOS 镜像并刷入</li><li>锁定 bootloader（GrapheneOS 设置向导会引导）</li><li>完成</li></ol><h3 id="方式二：命令行安装"><a href="#方式二：命令行安装" class="headerlink" title="方式二：命令行安装"></a>方式二：命令行安装</h3><p>面向技术用户，需要 fastboot 和 OpenSSH 工具。需要理解安装流程，不要盲目执行命令。</p><blockquote><p>官方强烈建议使用这两种官方方法。第三方安装指南经常过时或包含错误建议。</p></blockquote><hr><h2 id="十六、与其他隐私-ROM-的横向对比"><a href="#十六、与其他隐私-ROM-的横向对比" class="headerlink" title="十六、与其他隐私 ROM 的横向对比"></a>十六、与其他隐私 ROM 的横向对比</h2><table><thead><tr><th>特性</th><th>GrapheneOS</th><th>CalyxOS</th><th>LineageOS</th><th>DivestOS</th><th>iodéOS</th></tr></thead><tbody><tr><td>安全评级</td><td>⭐⭐⭐⭐⭐</td><td>⭐⭐⭐⭐</td><td>⭐⭐⭐</td><td>⭐⭐⭐⭐</td><td>⭐⭐⭐⭐</td></tr><tr><td>隐私评级</td><td>⭐⭐⭐⭐⭐</td><td>⭐⭐⭐⭐⭐</td><td>⭐⭐⭐⭐</td><td>⭐⭐⭐⭐⭐</td><td>⭐⭐⭐⭐⭐</td></tr><tr><td>易用性</td><td>⭐⭐⭐⭐</td><td>⭐⭐⭐⭐⭐</td><td>⭐⭐⭐⭐⭐</td><td>⭐⭐⭐</td><td>⭐⭐⭐⭐⭐</td></tr><tr><td>Google Play 兼容</td><td>沙箱化 ✅</td><td>microG ✅</td><td>手动 ⚠️</td><td>无 ❌</td><td>microG ✅</td></tr><tr><td>银行 App</td><td>大多数可用 ✅</td><td>部分 ⚠️</td><td>不确定 ⚠️</td><td>不行 ❌</td><td>部分 ⚠️</td></tr><tr><td>加固内核</td><td>✅</td><td>❌</td><td>❌</td><td>部分</td><td>❌</td></tr><tr><td>设备支持</td><td>仅 Pixel</td><td>Pixel + Fairphone + Motorola</td><td>110+ 设备</td><td>旧设备</td><td>部分 Pixel&#x2F;Fairphone&#x2F;Samsung</td></tr><tr><td>内置防火墙</td><td>❌</td><td>✅ (Datura)</td><td>❌</td><td>✅</td><td>✅</td></tr><tr><td>追踪器拦截</td><td>❌</td><td>部分</td><td>❌</td><td>✅</td><td>✅</td></tr></tbody></table><h3 id="各自定位"><a href="#各自定位" class="headerlink" title="各自定位"></a>各自定位</h3><ul><li><strong>GrapheneOS</strong>：安全最强，适合记者、活动人士、安全研究人员、追求极致隐私的极客。代价是仅支持 Pixel。</li><li><strong>CalyxOS</strong>：平衡之选，开箱即用，microG 预装，适合从标准 Android 平滑过渡。</li><li><strong>LineageOS</strong>：设备支持最广，适合非 Pixel 设备的去 Google 化。</li><li><strong>DivestOS</strong>：旧设备救星，给被抛弃的手机续命。</li><li><strong>iodéOS</strong>：预装体验最好，甚至能买到预装手机。</li></ul><hr><h2 id="十七、服务基础设施：连服务器都是隐私的"><a href="#十七、服务基础设施：连服务器都是隐私的" class="headerlink" title="十七、服务基础设施：连服务器都是隐私的"></a>十七、服务基础设施：连服务器都是隐私的</h2><p>GrapheneOS 对隐私的追求延伸到了自身服务基础设施：</p><ul><li><strong>完全自托管</strong>：专用服务器和虚拟机，不使用 CDN、SaaS 平台</li><li><strong>避免不必要日志</strong>，日志自动清除（网络服务 4 天，其他 10 天）</li><li><strong>DNSSEC</strong> 全域名实现</li><li><strong>DANE TLSA 记录</strong> 固定 TLS 密钥</li><li><strong>DNS CAA 记录</strong> 仅允许 Let’s Encrypt 颁发证书</li><li><strong>SSHFP</strong> 固定 SSH 密钥</li><li>网站不含第三方内容，严格 CSP，禁用 referrer</li><li>所有服务使用认证加密 + 前向保密</li><li>无持久 cookie（登录会话除外），<code>SameSite=Strict</code> + <code>__Host</code> 前缀</li><li>scrypt 密码哈希</li></ul><hr><h2 id="十八、谁应该用-GrapheneOS"><a href="#十八、谁应该用-GrapheneOS" class="headerlink" title="十八、谁应该用 GrapheneOS"></a>十八、谁应该用 GrapheneOS</h2><h3 id="适合"><a href="#适合" class="headerlink" title="适合"></a>适合</h3><ul><li><strong>记者、活动人士</strong>：面对有针对性的监控威胁</li><li><strong>安全研究人员</strong>：需要可审计的安全设备</li><li><strong>隐私极客</strong>：不想让 Google 追踪自己的一举一动</li><li><strong>高价值目标</strong>：持有敏感数据的企业高管、律师等</li><li><strong>技术爱好者</strong>：愿意折腾、追求掌控自己设备的人</li></ul><h3 id="不太适合"><a href="#不太适合" class="headerlink" title="不太适合"></a>不太适合</h3><ul><li><strong>需要丰富售后保障的人</strong>：出问题靠社区</li><li><strong>重度依赖 Google 生态且不愿折腾的人</strong>：虽然有沙箱化 Play，但体验不如原生</li><li><strong>非 Pixel 用户</strong>：根本装不了</li><li><strong>完全不懂技术的人</strong>：虽然安装已大幅简化，但日常使用仍需一定理解</li></ul><hr><h2 id="十九、优缺点总结"><a href="#十九、优缺点总结" class="headerlink" title="十九、优缺点总结"></a>十九、优缺点总结</h2><h3 id="优点"><a href="#优点" class="headerlink" title="优点"></a>优点</h3><ul><li>安卓生态中<strong>最强的安全加固</strong>，没有之一</li><li>彻底去 Google 化，默认不泄露任何数据</li><li>沙箱化 Google Play 兼顾隐私和兼容性</li><li>功能丰富，从胁迫密码到 VPN 泄漏防护面面俱到</li><li>非营利开源项目，无商业利益冲突</li><li>更新及时，通常在 Google 发布安全补丁后数小时内跟进</li></ul><h3 id="缺点"><a href="#缺点" class="headerlink" title="缺点"></a>缺点</h3><ul><li><strong>仅支持 Pixel</strong>——这是最大的门槛</li><li>国内应用兼容性基本 OK，但部分需要 Google SafetyNet&#x2F;Play Integrity 严格认证的应用可能有问题</li><li>没有售后保障</li><li>部分高级功能（如沙箱化 Play）需要一定学习成本</li><li>不支持某些 Google 专属功能（如 Google Assistant 唤醒、Pixel 独有 AI 功能）</li></ul><hr><h2 id="二十、-笔者语"><a href="#二十、-笔者语" class="headerlink" title="二十、 笔者语"></a>二十、 笔者语</h2><blockquote><p> 这个手机的操作系统可以在手机解开Bootloader锁 之后 还可以在刷完系统之后 再将系统 的 Bootloader 锁回去<br>可以设置接口 彻底关掉接口的所有功能就算是充电都能关掉<br>也不知道这个系统能不能对抗 帽子叔叔的信息采集系统</p></blockquote><hr><h2 id="二十一、-结语"><a href="#二十一、-结语" class="headerlink" title="二十一、 结语"></a>二十一、 结语</h2><p>GrapheneOS 不是一个”换个主题”的 ROM，而是一个<strong>从底层重新思考安全和隐私的操作系统</strong>。它证明了：你不需要在”用安卓应用”和”保护自己数据”之间二选一。</p><p>它的存在提出了一个重要的问题：<strong>你的手机到底是你的工具，还是别人监控你的终端？</strong></p><p>如果你有一台 Pixel，愿意花一个下午的时间折腾，GrapheneOS 值得一试。它不会让你的生活更方便，但会让你的数据更安全——在这个数据即资产的时代，这可能是最值得的投资。</p><hr><blockquote><p><strong>参考资料</strong></p><ul><li>GrapheneOS 官网：<a href="https://grapheneos.org/">https://grapheneos.org/</a></li><li>功能列表：<a href="https://grapheneos.org/features">https://grapheneos.org/features</a></li><li>安装指南：<a href="https://grapheneos.org/install/">https://grapheneos.org/install/</a></li><li>Auditor 服务：<a href="https://attestation.app/">https://attestation.app/</a></li><li>hardened_malloc：<a href="https://github.com/GrapheneOS/hardened_malloc">https://github.com/GrapheneOS/hardened_malloc</a></li><li>GrapheneOS 讨论论坛：<a href="https://discuss.grapheneos.org/">https://discuss.grapheneos.org/</a></li></ul></blockquote><p><em>本文信息截至 2026 年 7 月，GrapheneOS 持续更新中，具体功能以官方文档为准。</em></p>]]>
    </content>
    <id>http://example.com/2026/07/09/2026070901/</id>
    <link href="http://example.com/2026/07/09/2026070901/"/>
    <published>2026-07-09T15:28:22.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>这不是一篇简单的”刷机教程”。本文从项目起源、安全架构、隐私机制、功能特性、安装方法到横向对比，事无巨细地拆解 GrapheneOS——目前公认最安全的安卓操作系统。</p>
</blockquote>
<hr>
<h2 id="一、Graphen]]>
    </summary>
    <title>GrapheneOS 完全指南：把安卓的安全与隐私做到极致</title>
    <updated>2026-07-09T15:53:31.331Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="Springboot" scheme="http://example.com/categories/Springboot/"/>
    <category term="java" scheme="http://example.com/tags/java/"/>
    <category term="Springboot" scheme="http://example.com/tags/Springboot/"/>
    <category term="Redis" scheme="http://example.com/tags/Redis/"/>
    <category term="Flyway" scheme="http://example.com/tags/Flyway/"/>
    <content>
      <![CDATA[<blockquote><p>Flyway 管理数据库版本、SpringDoc 自动生成接口文档、多环境配置、Docker 一键部署——这些不是可选项，而是全栈项目的标配。本文以社区团购系统为例，把 Spring Boot 3 工程化的完整流程从头到尾讲透。</p></blockquote><hr><h2 id="第一章：项目结构设计"><a href="#第一章：项目结构设计" class="headerlink" title="第一章：项目结构设计"></a>第一章：项目结构设计</h2><h3 id="1-1-整体架构"><a href="#1-1-整体架构" class="headerlink" title="1.1 整体架构"></a>1.1 整体架构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">community-group-buying-system/</span><br><span class="line">├── community-group-buying-system/    # 后端（Spring Boot）</span><br><span class="line">│   ├── pom.xml</span><br><span class="line">│   ├── src/main/java/com/</span><br><span class="line">│   │   ├── SpringbootSchemaApplication.java</span><br><span class="line">│   │   ├── controller/    # 15个控制器</span><br><span class="line">│   │   ├── service/       # Service接口 + impl</span><br><span class="line">│   │   ├── dao/            # MyBatis Plus Mapper</span><br><span class="line">│   │   ├── entity/        # 实体类</span><br><span class="line">│   │   ├── config/         # 7个配置类</span><br><span class="line">│   │   ├── interceptor/    # 认证拦截器</span><br><span class="line">│   │   ├── annotation/     # 自定义注解</span><br><span class="line">│   │   ├── db/migration/   # Flyway Java迁移</span><br><span class="line">│   │   └── utils/          # 工具类</span><br><span class="line">│   └── src/main/resources/</span><br><span class="line">│       ├── application.yml         # 公共配置</span><br><span class="line">│       ├── application-dev.yml     # 开发环境</span><br><span class="line">│       ├── application-test.yml    # 测试环境</span><br><span class="line">│       └── db/migration/          # Flyway SQL迁移</span><br><span class="line">│</span><br><span class="line">├── admin-vue3/          # 管理后台前端（Vue 3，端口8081）</span><br><span class="line">├── front-vue3/          # 用户端前端（Vue 3，端口8084）</span><br><span class="line">├── Dockerfile</span><br><span class="line">└── docker-compose.yml</span><br></pre></td></tr></table></figure><p><strong>双前端设计</strong>：管理后台和用户端分离，共享同一套后端 API。</p><h3 id="1-2-主启动类"><a href="#1-2-主启动类" class="headerlink" title="1.2 主启动类"></a>1.2 主启动类</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication(exclude = &#123;</span></span><br><span class="line"><span class="meta">    SecurityAutoConfiguration.class,</span></span><br><span class="line"><span class="meta">    SecurityFilterAutoConfiguration.class</span></span><br><span class="line"><span class="meta">&#125;)</span></span><br><span class="line"><span class="meta">@MapperScan(basePackages = &#123;&quot;com.dao&quot;&#125;)</span></span><br><span class="line"><span class="meta">@EnableAsync</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpringbootSchemaApplication</span> <span class="keyword">extends</span> <span class="title class_">SpringBootServletInitializer</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(SpringbootSchemaApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> SpringApplicationBuilder <span class="title function_">configure</span><span class="params">(SpringApplicationBuilder builder)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> builder.sources(SpringbootSchemaApplication.class);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>设计要点</strong>：</p><ul><li>排除 Security 自动配置——只用 <code>BCryptPasswordEncoder</code>，不需要 Security 的过滤器链</li><li><code>@EnableAsync</code> 开启异步支持——延迟双删的 <code>@Async</code> 依赖这个</li><li><code>@MapperScan(&quot;com.dao&quot;)</code> 扫描 MyBatis Plus Mapper</li><li><code>extends SpringBootServletInitializer</code> 支持 WAR 部署</li></ul><hr><h2 id="第二章：Maven-依赖管理"><a href="#第二章：Maven-依赖管理" class="headerlink" title="第二章：Maven 依赖管理"></a>第二章：Maven 依赖管理</h2><h3 id="2-1-核心-POM"><a href="#2-1-核心-POM" class="headerlink" title="2.1 核心 POM"></a>2.1 核心 POM</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">parent</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-parent<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.4.1<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">parent</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">properties</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">java.version</span>&gt;</span>17<span class="tag">&lt;/<span class="name">java.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">mybatis-plus.version</span>&gt;</span>3.5.9<span class="tag">&lt;/<span class="name">mybatis-plus.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">jjwt.version</span>&gt;</span>0.12.6<span class="tag">&lt;/<span class="name">jjwt.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">springdoc.version</span>&gt;</span>2.8.15<span class="tag">&lt;/<span class="name">springdoc.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">hutool.version</span>&gt;</span>5.8.25<span class="tag">&lt;/<span class="name">hutool.version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">properties</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="2-2-关键依赖说明"><a href="#2-2-关键依赖说明" class="headerlink" title="2.2 关键依赖说明"></a>2.2 关键依赖说明</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- MyBatis Plus（Spring Boot 3专用starter） --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.baomidou<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>mybatis-plus-spring-boot3-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;mybatis-plus.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- Druid 连接池（Spring Boot 3专用） --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>druid-spring-boot-3-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- Spring Security（仅用BCrypt，禁用自动配置） --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-security<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- Flyway + MySQL方言 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.flywaydb<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>flyway-core<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.flywaydb<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>flyway-mysql<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>注意</strong>：Spring Boot 3 用 <code>mybatis-plus-spring-boot3-starter</code>（注意 <code>boot3</code>），不是普通的 <code>mybatis-plus-boot-starter</code>。Druid 也需要 <code>druid-spring-boot-3-starter</code>。</p><hr><h2 id="第三章：多环境配置"><a href="#第三章：多环境配置" class="headerlink" title="第三章：多环境配置"></a>第三章：多环境配置</h2><h3 id="3-1-公共配置-application-yml"><a href="#3-1-公共配置-application-yml" class="headerlink" title="3.1 公共配置 application.yml"></a>3.1 公共配置 application.yml</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">profiles:</span></span><br><span class="line">    <span class="attr">active:</span> <span class="string">dev</span>   <span class="comment"># 默认开发环境</span></span><br><span class="line">  <span class="attr">flyway:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">baseline-on-migrate:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">baseline-version:</span> <span class="number">0</span></span><br><span class="line">    <span class="attr">locations:</span> <span class="string">classpath:db/migration,classpath:com/db/migration</span></span><br><span class="line"></span><br><span class="line"><span class="attr">server:</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">8080</span></span><br><span class="line">  <span class="attr">servlet:</span></span><br><span class="line">    <span class="attr">context-path:</span> <span class="string">/springboot2c1hu</span></span><br><span class="line"></span><br><span class="line"><span class="attr">springdoc:</span></span><br><span class="line">  <span class="attr">api-docs:</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">/v3/api-docs</span></span><br><span class="line">  <span class="attr">swagger-ui:</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">/swagger-ui.html</span></span><br></pre></td></tr></table></figure><h3 id="3-2-开发环境-application-dev-yml"><a href="#3-2-开发环境-application-dev-yml" class="headerlink" title="3.2 开发环境 application-dev.yml"></a>3.2 开发环境 application-dev.yml</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">datasource:</span></span><br><span class="line">    <span class="attr">type:</span> <span class="string">com.alibaba.druid.pool.DruidDataSource</span></span><br><span class="line">    <span class="attr">driver-class-name:</span> <span class="string">com.mysql.cj.jdbc.Driver</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">jdbc:mysql://127.0.0.1:3306/springboot2c1hu?useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false&amp;serverTimezone=GMT%2B8</span></span><br><span class="line">    <span class="attr">username:</span> <span class="string">root</span></span><br><span class="line">    <span class="attr">password:</span> <span class="string">root</span></span><br><span class="line">    <span class="attr">druid:</span></span><br><span class="line">      <span class="attr">initial-size:</span> <span class="number">5</span></span><br><span class="line">      <span class="attr">max-active:</span> <span class="number">20</span></span><br><span class="line">      <span class="attr">min-idle:</span> <span class="number">5</span></span><br><span class="line">  <span class="attr">data:</span></span><br><span class="line">    <span class="attr">redis:</span></span><br><span class="line">      <span class="attr">host:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span></span><br><span class="line">      <span class="attr">port:</span> <span class="number">6379</span></span><br><span class="line">      <span class="attr">lettuce:</span></span><br><span class="line">        <span class="attr">pool:</span></span><br><span class="line">          <span class="attr">max-active:</span> <span class="number">16</span></span><br><span class="line">          <span class="attr">max-idle:</span> <span class="number">8</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jwt:</span></span><br><span class="line">  <span class="attr">secret:</span> <span class="string">CommunityGroupBuying2024SecretKeyForJWTTokenGenerationMustBeLongEnough</span></span><br><span class="line">  <span class="attr">expiration:</span> <span class="number">3600000</span></span><br><span class="line"></span><br><span class="line"><span class="attr">token:</span></span><br><span class="line">  <span class="attr">redis-prefix:</span> <span class="string">&quot;token:&quot;</span></span><br><span class="line">  <span class="attr">expire-seconds:</span> <span class="number">3600</span></span><br><span class="line"></span><br><span class="line"><span class="attr">springdoc:</span></span><br><span class="line">  <span class="attr">api-docs:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">swagger-ui:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><h3 id="3-3-生产环境-application-prod-yml"><a href="#3-3-生产环境-application-prod-yml" class="headerlink" title="3.3 生产环境 application-prod.yml"></a>3.3 生产环境 application-prod.yml</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">datasource:</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">jdbc:mysql://$&#123;DB_HOST&#125;:3306/$&#123;DB_NAME&#125;?useSSL=true</span></span><br><span class="line">    <span class="attr">username:</span> <span class="string">$&#123;DB_USER&#125;</span></span><br><span class="line">    <span class="attr">password:</span> <span class="string">$&#123;DB_PASSWORD&#125;</span></span><br><span class="line">  <span class="attr">data:</span></span><br><span class="line">    <span class="attr">redis:</span></span><br><span class="line">      <span class="attr">host:</span> <span class="string">$&#123;REDIS_HOST&#125;</span></span><br><span class="line">      <span class="attr">port:</span> <span class="string">$&#123;REDIS_PORT&#125;</span></span><br><span class="line">      <span class="attr">password:</span> <span class="string">$&#123;REDIS_PASSWORD&#125;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jwt:</span></span><br><span class="line">  <span class="attr">secret:</span> <span class="string">$&#123;JWT_SECRET&#125;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">springdoc:</span></span><br><span class="line">  <span class="attr">api-docs:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">false</span>    <span class="comment"># ★ 生产环境关闭文档</span></span><br><span class="line">  <span class="attr">swagger-ui:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure><p><strong>环境变量注入</strong>：生产配置用 <code>${ENV_VAR}</code> 引用环境变量，避免在代码中硬编码密码。</p><hr><h2 id="第四章：Flyway-数据库迁移"><a href="#第四章：Flyway-数据库迁移" class="headerlink" title="第四章：Flyway 数据库迁移"></a>第四章：Flyway 数据库迁移</h2><h3 id="4-1-Flyway-配置"><a href="#4-1-Flyway-配置" class="headerlink" title="4.1 Flyway 配置"></a>4.1 Flyway 配置</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">flyway:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">baseline-on-migrate:</span> <span class="literal">true</span>    <span class="comment"># 已有数据的库也能执行迁移</span></span><br><span class="line">    <span class="attr">baseline-version:</span> <span class="number">0</span>           <span class="comment"># 基线版本</span></span><br><span class="line">    <span class="attr">locations:</span> <span class="string">classpath:db/migration,classpath:com/db/migration</span></span><br><span class="line">    <span class="attr">table:</span> <span class="string">flyway_schema_history</span>  <span class="comment"># 迁移历史表名</span></span><br></pre></td></tr></table></figure><p><strong>两个 location 的含义</strong>：</p><ul><li><code>classpath:db/migration</code> → 放 SQL 迁移文件（<code>.sql</code>）</li><li><code>classpath:com/db/migration</code> → 放 Java 迁移文件（<code>.java</code>，继承 <code>BaseJavaMigration</code>）</li></ul><h3 id="4-2-版本命名规则"><a href="#4-2-版本命名规则" class="headerlink" title="4.2 版本命名规则"></a>4.2 版本命名规则</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">V1__init_schema.sql        ← SQL迁移，版本号1</span><br><span class="line">V2__encrypt_passwords.java  ← Java迁移，版本号2</span><br><span class="line">V3__drop_token_table.sql    ← SQL迁移，版本号3</span><br></pre></td></tr></table></figure><table><thead><tr><th>格式</th><th>含义</th></tr></thead><tbody><tr><td><code>V</code></td><td>Versioned migration（版本化迁移，只执行一次）</td></tr><tr><td><code>R</code></td><td>Repeatable migration（可重复迁移，每次checksum变化都执行）</td></tr><tr><td><code>__</code></td><td>双下划线分隔版本号和描述</td></tr></tbody></table><h3 id="4-3-V1：初始化-Schema（SQL-迁移）"><a href="#4-3-V1：初始化-Schema（SQL-迁移）" class="headerlink" title="4.3 V1：初始化 Schema（SQL 迁移）"></a>4.3 V1：初始化 Schema（SQL 迁移）</h3><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- V1__init_schema.sql</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> `users` (</span><br><span class="line">    `id` <span class="type">bigint</span> <span class="keyword">NOT NULL</span> AUTO_INCREMENT,</span><br><span class="line">    `username` <span class="type">varchar</span>(<span class="number">100</span>) <span class="keyword">NOT NULL</span>,</span><br><span class="line">    `password` <span class="type">varchar</span>(<span class="number">100</span>) <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;密码(明文，V2会加密)&#x27;</span>,</span><br><span class="line">    `role` <span class="type">varchar</span>(<span class="number">100</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;管理员&#x27;</span>,</span><br><span class="line">    <span class="keyword">PRIMARY KEY</span> (`id`),</span><br><span class="line">    <span class="keyword">UNIQUE</span> INDEX `username_index`(`username`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8mb4;</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE TABLE</span> `yonghu` (</span><br><span class="line">    `id` <span class="type">bigint</span> <span class="keyword">NOT NULL</span> AUTO_INCREMENT,</span><br><span class="line">    `zhanghao` <span class="type">varchar</span>(<span class="number">200</span>) <span class="keyword">NOT NULL</span>,</span><br><span class="line">    `mima` <span class="type">varchar</span>(<span class="number">200</span>) <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;密码(明文，V2会加密)&#x27;</span>,</span><br><span class="line">    <span class="keyword">PRIMARY KEY</span> (`id`),</span><br><span class="line">    <span class="keyword">UNIQUE</span> INDEX `zhanghao_index`(`zhanghao`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8mb4;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 初始数据（明文密码）</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> `users` <span class="keyword">VALUES</span> (<span class="number">1</span>, <span class="string">&#x27;abo&#x27;</span>, <span class="string">&#x27;abo&#x27;</span>, <span class="string">&#x27;超级管理员&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT INTO</span> `yonghu` <span class="keyword">VALUES</span> (<span class="number">1</span>, <span class="string">&#x27;用户1&#x27;</span>, <span class="string">&#x27;123456&#x27;</span>);</span><br></pre></td></tr></table></figure><p><strong>注意</strong>：V1 中密码是明文——这是一个”技术债”，V2 会用 Java 迁移加密。</p><h3 id="4-4-V2：Java-迁移——BCrypt-加密存量密码"><a href="#4-4-V2：Java-迁移——BCrypt-加密存量密码" class="headerlink" title="4.4 V2：Java 迁移——BCrypt 加密存量密码"></a>4.4 V2：Java 迁移——BCrypt 加密存量密码</h3><p>SQL 无法做到”逐行读取、条件判断、加密更新”这种复杂逻辑。Flyway 的 Java 迁移可以：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// V2__encrypt_passwords.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">V2__encrypt_passwords</span> <span class="keyword">extends</span> <span class="title class_">BaseJavaMigration</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">migrate</span><span class="params">(Context context)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        migrateUsersTable(context);     <span class="comment">// 加密 users.password</span></span><br><span class="line">        migrateYonghuTable(context);    <span class="comment">// 加密 yonghu.mima</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">migrateUsersTable</span><span class="params">(Context context)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="keyword">try</span> (<span class="type">Statement</span> <span class="variable">select</span> <span class="operator">=</span> context.getConnection().createStatement()) &#123;</span><br><span class="line">            <span class="type">ResultSet</span> <span class="variable">rs</span> <span class="operator">=</span> select.executeQuery(<span class="string">&quot;SELECT id, password FROM users&quot;</span>);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">while</span> (rs.next()) &#123;</span><br><span class="line">                <span class="type">Long</span> <span class="variable">id</span> <span class="operator">=</span> rs.getLong(<span class="string">&quot;id&quot;</span>);</span><br><span class="line">                <span class="type">String</span> <span class="variable">plainPassword</span> <span class="operator">=</span> rs.getString(<span class="string">&quot;password&quot;</span>);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// ★ 幂等性：跳过已加密的密码</span></span><br><span class="line">                <span class="comment">// BCrypt哈希以 $2a$ 或 $2b$ 开头</span></span><br><span class="line">                <span class="keyword">if</span> (plainPassword != <span class="literal">null</span> </span><br><span class="line">                        &amp;&amp; !plainPassword.startsWith(<span class="string">&quot;$2a$&quot;</span>) </span><br><span class="line">                        &amp;&amp; !plainPassword.startsWith(<span class="string">&quot;$2b$&quot;</span>)) &#123;</span><br><span class="line">                    </span><br><span class="line">                    <span class="type">String</span> <span class="variable">hashed</span> <span class="operator">=</span> BCrypt.hashpw(plainPassword, BCrypt.gensalt());</span><br><span class="line">                    <span class="comment">// SQL注入防护：单引号转义</span></span><br><span class="line">                    <span class="type">String</span> <span class="variable">escapedHash</span> <span class="operator">=</span> hashed.replace(<span class="string">&quot;&#x27;&quot;</span>, <span class="string">&quot;&#x27;&#x27;&quot;</span>);</span><br><span class="line">                    <span class="keyword">try</span> (<span class="type">Statement</span> <span class="variable">update</span> <span class="operator">=</span> context.getConnection().createStatement()) &#123;</span><br><span class="line">                        update.executeUpdate(</span><br><span class="line">                            <span class="string">&quot;UPDATE users SET password = &#x27;&quot;</span> + escapedHash + <span class="string">&quot;&#x27; WHERE id = &quot;</span> + id);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>幂等性设计</strong>：通过 <code>$2a$</code> 前缀判断是否已加密，迁移可重复执行。如果迁移失败，修复数据后重新执行即可。</p><p><strong>两种 BCrypt 实现</strong>：</p><ul><li>V2迁移用 Hutool 的 <code>BCrypt.hashpw</code>（<code>cn.hutool.crypto.digest.BCrypt</code>）</li><li>应用层用 Spring Security 的 <code>BCryptPasswordEncoder</code></li></ul><p>两者都遵循标准 BCrypt 算法，哈希互相兼容——Hutool 加密的密码可以用 Spring Security 的 <code>matches()</code> 验证。</p><h3 id="4-5-V3：删除废弃表"><a href="#4-5-V3：删除废弃表" class="headerlink" title="4.5 V3：删除废弃表"></a>4.5 V3：删除废弃表</h3><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- V3__drop_token_table.sql</span></span><br><span class="line"><span class="comment">-- Token 已迁移到 Redis 存储，不再需要数据库 token 表</span></span><br><span class="line"><span class="keyword">DROP</span> <span class="keyword">TABLE</span> IF <span class="keyword">EXISTS</span> `token`;</span><br></pre></td></tr></table></figure><p><strong>演进故事</strong>：V1 建了 <code>token</code> 表 → 架构升级为 JWT + Redis → V3 删除 token 表。每个迁移文件都是架构演进的快照。</p><h3 id="4-6-Flyway-执行流程"><a href="#4-6-Flyway-执行流程" class="headerlink" title="4.6 Flyway 执行流程"></a>4.6 Flyway 执行流程</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">应用启动</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Flyway 检查 flyway_schema_history 表</span><br><span class="line">    │</span><br><span class="line">    ├── 表不存在 → 创建表，记录 baseline (version=0)</span><br><span class="line">    │</span><br><span class="line">    ├── 表存在 → 读取已执行的迁移记录</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">扫描 classpath:db/migration 和 classpath:com/db/migration</span><br><span class="line">    │</span><br><span class="line">    ├── 找到 V1 → 未执行则执行 V1__init_schema.sql</span><br><span class="line">    ├── 找到 V2 → 未执行则执行 V2__encrypt_passwords.java</span><br><span class="line">    ├── 找到 V3 → 未执行则执行 V3__drop_token_table.sql</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">更新 flyway_schema_history 表（版本号、描述、类型、checksum、执行时间）</span><br></pre></td></tr></table></figure><h3 id="4-7-FULLTEXT-ngram-中文全文索引"><a href="#4-7-FULLTEXT-ngram-中文全文索引" class="headerlink" title="4.7 FULLTEXT + ngram 中文全文索引"></a>4.7 FULLTEXT + ngram 中文全文索引</h3><p>钢铁工厂项目中，Flyway 迁移还包含中文全文索引：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- steel-factory-fullstack 的 Flyway 迁移</span></span><br><span class="line"><span class="keyword">ALTER TABLE</span> `obtain_information` </span><br><span class="line"><span class="keyword">ADD</span> FULLTEXT INDEX `ft_title_content` (`title`, `content`) </span><br><span class="line"><span class="keyword">WITH</span> PARSER ngram;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- ngram 是 MySQL 5.7.6+ 的中文分词器</span></span><br><span class="line"><span class="comment">-- 将中文按 n-gram（默认2字）切分</span></span><br><span class="line"><span class="comment">-- &quot;钢管价格&quot; → &quot;钢管&quot;, &quot;管价&quot;, &quot;价格&quot;</span></span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 搜索时使用 MATCH ... AGAINST</span></span><br><span class="line">List&lt;News&gt; <span class="title function_">search</span><span class="params">(String keyword)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> newsMapper.search(</span><br><span class="line">        <span class="string">&quot;MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE)&quot;</span>, keyword);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-8-常见问题"><a href="#4-8-常见问题" class="headerlink" title="4.8 常见问题"></a>4.8 常见问题</h3><p><strong>迁移失败后启动报错</strong>：手动修复数据库到上一个版本的状态，删除 <code>flyway_schema_history</code> 中失败的记录，重启应用。</p><p><strong>checksum 不匹配</strong>：修改了已执行的迁移文件。<strong>解法</strong>：不要修改已执行的文件，只能新增新的 V* 文件。</p><p><strong>Java 迁移中使用 Spring Bean</strong>：Java 迁移中<strong>不能注入 Spring Bean</strong>——Flyway 在 Spring 完全初始化前执行。<code>Context</code> 对象只提供 JDBC <code>Connection</code>。</p><hr><h2 id="第五章：SpringDoc-OpenAPI-接口文档"><a href="#第五章：SpringDoc-OpenAPI-接口文档" class="headerlink" title="第五章：SpringDoc OpenAPI 接口文档"></a>第五章：SpringDoc OpenAPI 接口文档</h2><h3 id="5-1-从-Swagger-2-到-OpenAPI-3"><a href="#5-1-从-Swagger-2-到-OpenAPI-3" class="headerlink" title="5.1 从 Swagger 2 到 OpenAPI 3"></a>5.1 从 Swagger 2 到 OpenAPI 3</h3><table><thead><tr><th>特性</th><th>Springfox (Swagger 2)</th><th>SpringDoc (OpenAPI 3)</th></tr></thead><tbody><tr><td>Spring Boot 3</td><td>不兼容</td><td>完全兼容</td></tr><tr><td>依赖</td><td><code>springfox-swagger2</code></td><td><code>springdoc-openapi-starter-webmvc-ui</code></td></tr><tr><td>注解</td><td><code>@Api</code> &#x2F; <code>@ApiOperation</code></td><td><code>@Tag</code> &#x2F; <code>@Operation</code></td></tr><tr><td>配置</td><td>Docket Bean</td><td>OpenAPI Bean</td></tr></tbody></table><h3 id="5-2-配置类"><a href="#5-2-配置类" class="headerlink" title="5.2 配置类"></a>5.2 配置类</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SwaggerConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> OpenAPI <span class="title function_">openAPI</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">OpenAPI</span>()</span><br><span class="line">                .info(<span class="keyword">new</span> <span class="title class_">Info</span>()</span><br><span class="line">                        .title(<span class="string">&quot;社区团购系统 API&quot;</span>)</span><br><span class="line">                        .description(<span class="string">&quot;社区团购系统后端接口文档&quot;</span>)</span><br><span class="line">                        .version(<span class="string">&quot;v1.0.0&quot;</span>)</span><br><span class="line">                        .contact(<span class="keyword">new</span> <span class="title class_">Contact</span>()</span><br><span class="line">                                .name(<span class="string">&quot;Community Group Buying&quot;</span>)))</span><br><span class="line">                <span class="comment">// ★ 配置JWT全局认证</span></span><br><span class="line">                .addSecurityItem(<span class="keyword">new</span> <span class="title class_">SecurityRequirement</span>().addList(<span class="string">&quot;Token&quot;</span>))</span><br><span class="line">                .components(<span class="keyword">new</span> <span class="title class_">Components</span>()</span><br><span class="line">                        .addSecuritySchemes(<span class="string">&quot;Token&quot;</span>,</span><br><span class="line">                                <span class="keyword">new</span> <span class="title class_">SecurityScheme</span>()</span><br><span class="line">                                        .name(<span class="string">&quot;Token&quot;</span>)</span><br><span class="line">                                        .type(SecurityScheme.Type.APIKEY)</span><br><span class="line">                                        .in(SecurityScheme.In.HEADER)</span><br><span class="line">                                        .description(<span class="string">&quot;JWT Token&quot;</span>)));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>APIKEY vs Bearer 的区别</strong>：本项目 Token 放在自定义 Header <code>Token</code> 中（不是标准的 <code>Authorization: Bearer xxx</code>），所以用 <code>APIKEY</code> 类型。</p><h3 id="5-3-拦截器排除-Swagger-路径"><a href="#5-3-拦截器排除-Swagger-路径" class="headerlink" title="5.3 拦截器排除 Swagger 路径"></a>5.3 拦截器排除 Swagger 路径</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">InterceptorConfig</span> <span class="keyword">extends</span> <span class="title class_">WebMvcConfigurationSupport</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> &#123;</span><br><span class="line">        registry.addInterceptor(getAuthorizationInterceptor())</span><br><span class="line">                .addPathPatterns(<span class="string">&quot;/**&quot;</span>)</span><br><span class="line">                .excludePathPatterns(<span class="string">&quot;/static/**&quot;</span>)</span><br><span class="line">                <span class="comment">// ★ 排除Swagger相关路径</span></span><br><span class="line">                .excludePathPatterns(</span><br><span class="line">                    <span class="string">&quot;/swagger-ui/**&quot;</span>, <span class="string">&quot;/swagger-ui.html&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;/v3/api-docs/**&quot;</span>, <span class="string">&quot;/swagger-resources/**&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;/webjars/**&quot;</span></span><br><span class="line">                )</span><br><span class="line">                .excludePathPatterns(<span class="string">&quot;/actuator/**&quot;</span>);</span><br><span class="line">        <span class="built_in">super</span>.addInterceptors(registry);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-4-接口注解"><a href="#5-4-接口注解" class="headerlink" title="5.4 接口注解"></a>5.4 接口注解</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/orders&quot;)</span></span><br><span class="line"><span class="meta">@Tag(name = &quot;订单管理&quot;, description = &quot;订单的增删改查&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrdersController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Operation(summary = &quot;创建订单&quot;, description = &quot;用户下单创建新订单&quot;)</span></span><br><span class="line">    <span class="meta">@PostMapping(&quot;/create&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> R <span class="title function_">create</span><span class="params">(</span></span><br><span class="line"><span class="params">        <span class="meta">@Parameter(description = &quot;商品ID&quot;, required = true)</span> <span class="meta">@RequestParam</span> Long productId,</span></span><br><span class="line"><span class="params">        <span class="meta">@Parameter(description = &quot;购买数量&quot;)</span> <span class="meta">@RequestParam</span> Integer quantity</span></span><br><span class="line"><span class="params">    )</span> &#123; ... &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-5-多环境控制"><a href="#5-5-多环境控制" class="headerlink" title="5.5 多环境控制"></a>5.5 多环境控制</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># application-prod.yml</span></span><br><span class="line"><span class="attr">springdoc:</span></span><br><span class="line">  <span class="attr">api-docs:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">false</span>    <span class="comment"># 生产环境关闭文档</span></span><br><span class="line">  <span class="attr">swagger-ui:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure><hr><h2 id="第六章：跨域配置"><a href="#第六章：跨域配置" class="headerlink" title="第六章：跨域配置"></a>第六章：跨域配置</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CorsConfig</span> <span class="keyword">implements</span> <span class="title class_">WebMvcConfigurer</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addCorsMappings</span><span class="params">(CorsRegistry registry)</span> &#123;</span><br><span class="line">        registry.addMapping(<span class="string">&quot;/**&quot;</span>)</span><br><span class="line">                .allowedOriginPatterns(<span class="string">&quot;*&quot;</span>)</span><br><span class="line">                .allowedMethods(<span class="string">&quot;GET&quot;</span>, <span class="string">&quot;POST&quot;</span>, <span class="string">&quot;PUT&quot;</span>, <span class="string">&quot;DELETE&quot;</span>, <span class="string">&quot;OPTIONS&quot;</span>)</span><br><span class="line">                .allowedHeaders(<span class="string">&quot;*&quot;</span>)</span><br><span class="line">                .allowCredentials(<span class="literal">true</span>)</span><br><span class="line">                .maxAge(<span class="number">3600</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>注意</strong>：<code>allowed-origins: &quot;*&quot;</code> 和 <code>allow-credentials: true</code> 在浏览器规范中是互斥的。用 <code>allowedOriginPatterns(&quot;*&quot;)</code> 可以绕过这个限制。</p><hr><h2 id="第七章：Docker-部署"><a href="#第七章：Docker-部署" class="headerlink" title="第七章：Docker 部署"></a>第七章：Docker 部署</h2><h3 id="7-1-后端-Dockerfile"><a href="#7-1-后端-Dockerfile" class="headerlink" title="7.1 后端 Dockerfile"></a>7.1 后端 Dockerfile</h3><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> eclipse-temurin:<span class="number">17</span>-jre-alpine</span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> target/community-group-buying-system.jar app.jar</span></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">8080</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> [<span class="string">&quot;java&quot;</span>, <span class="string">&quot;-jar&quot;</span>, <span class="string">&quot;-Dspring.profiles.active=prod&quot;</span>, <span class="string">&quot;app.jar&quot;</span>]</span></span><br></pre></td></tr></table></figure><h3 id="7-2-前端-Dockerfile"><a href="#7-2-前端-Dockerfile" class="headerlink" title="7.2 前端 Dockerfile"></a>7.2 前端 Dockerfile</h3><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># admin-vue3/Dockerfile</span></span><br><span class="line"><span class="keyword">FROM</span> node:<span class="number">22</span>-alpine AS build</span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> package*.json ./</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> npm install</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> . .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> npm run build</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">FROM</span> nginx:alpine</span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --from=build /app/dist /usr/share/nginx/html</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> nginx.conf /etc/nginx/conf.d/default.conf</span></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">80</span></span><br></pre></td></tr></table></figure><h3 id="7-3-docker-compose-yml"><a href="#7-3-docker-compose-yml" class="headerlink" title="7.3 docker-compose.yml"></a>7.3 docker-compose.yml</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&#x27;3.8&#x27;</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">mysql:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">mysql:8.0</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">MYSQL_ROOT_PASSWORD:</span> <span class="string">root</span></span><br><span class="line">      <span class="attr">MYSQL_DATABASE:</span> <span class="string">springboot2c1hu</span></span><br><span class="line">    <span class="attr">ports:</span> [<span class="string">&quot;3306:3306&quot;</span>]</span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">mysql_data:/var/lib/mysql</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">redis:7-alpine</span></span><br><span class="line">    <span class="attr">ports:</span> [<span class="string">&quot;6379:6379&quot;</span>]</span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">redis_data:/data</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">backend:</span></span><br><span class="line">    <span class="attr">build:</span> <span class="string">.</span></span><br><span class="line">    <span class="attr">ports:</span> [<span class="string">&quot;8080:8080&quot;</span>]</span><br><span class="line">    <span class="attr">depends_on:</span> [<span class="string">mysql</span>, <span class="string">redis</span>]</span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">DB_HOST=mysql</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">DB_NAME=springboot2c1hu</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">DB_USER=root</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">DB_PASSWORD=root</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">REDIS_HOST=redis</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">REDIS_PORT=6379</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SPRING_PROFILES_ACTIVE=prod</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">admin-frontend:</span></span><br><span class="line">    <span class="attr">build:</span> <span class="string">./admin-vue3</span></span><br><span class="line">    <span class="attr">ports:</span> [<span class="string">&quot;8081:80&quot;</span>]</span><br><span class="line"></span><br><span class="line">  <span class="attr">front-frontend:</span></span><br><span class="line">    <span class="attr">build:</span> <span class="string">./front-vue3</span></span><br><span class="line">    <span class="attr">ports:</span> [<span class="string">&quot;8084:80&quot;</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">mysql_data:</span></span><br><span class="line">  <span class="attr">redis_data:</span></span><br></pre></td></tr></table></figure><p><strong>一键启动</strong>：<code>docker-compose up -d</code> 启动全部服务。</p><hr><h2 id="第八章：前后端联调"><a href="#第八章：前后端联调" class="headerlink" title="第八章：前后端联调"></a>第八章：前后端联调</h2><h3 id="8-1-Vue-3-请求封装"><a href="#8-1-Vue-3-请求封装" class="headerlink" title="8.1 Vue 3 请求封装"></a>8.1 Vue 3 请求封装</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">&#x27;axios&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> service = axios.<span class="title function_">create</span>(&#123;</span><br><span class="line">  <span class="attr">baseURL</span>: <span class="string">&#x27;/springboot2c1hu&#x27;</span>,  <span class="comment">// 后端context-path</span></span><br><span class="line">  <span class="attr">timeout</span>: <span class="number">10000</span></span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 请求拦截器：自动携带Token</span></span><br><span class="line">service.<span class="property">interceptors</span>.<span class="property">request</span>.<span class="title function_">use</span>(<span class="function"><span class="params">config</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> token = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="string">&#x27;Token&#x27;</span>)</span><br><span class="line">  <span class="keyword">if</span> (token) &#123;</span><br><span class="line">    config.<span class="property">headers</span>[<span class="string">&#x27;Token&#x27;</span>] = token  <span class="comment">// ★ 自定义Header名</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> config</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 响应拦截器：统一处理错误</span></span><br><span class="line">service.<span class="property">interceptors</span>.<span class="property">response</span>.<span class="title function_">use</span>(</span><br><span class="line">  <span class="function"><span class="params">response</span> =&gt;</span> response.<span class="property">data</span>,</span><br><span class="line">  <span class="function"><span class="params">error</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (error.<span class="property">response</span>.<span class="property">status</span> === <span class="number">401</span>) &#123;</span><br><span class="line">      <span class="variable language_">localStorage</span>.<span class="title function_">removeItem</span>(<span class="string">&#x27;Token&#x27;</span>)</span><br><span class="line">      router.<span class="title function_">push</span>(<span class="string">&#x27;/login&#x27;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">reject</span>(error)</span><br><span class="line">  &#125;</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> service</span><br></pre></td></tr></table></figure><h3 id="8-2-Vite-代理配置"><a href="#8-2-Vite-代理配置" class="headerlink" title="8.2 Vite 代理配置"></a>8.2 Vite 代理配置</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vite.config.js</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(&#123;</span><br><span class="line">  <span class="attr">server</span>: &#123;</span><br><span class="line">    <span class="attr">port</span>: <span class="number">8081</span>,</span><br><span class="line">    <span class="attr">proxy</span>: &#123;</span><br><span class="line">      <span class="string">&#x27;/springboot2c1hu&#x27;</span>: &#123;</span><br><span class="line">        <span class="attr">target</span>: <span class="string">&#x27;http://localhost:8080&#x27;</span>,</span><br><span class="line">        <span class="attr">changeOrigin</span>: <span class="literal">true</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><hr><h2 id="第九章：Druid-连接池监控"><a href="#第九章：Druid-连接池监控" class="headerlink" title="第九章：Druid 连接池监控"></a>第九章：Druid 连接池监控</h2><h3 id="9-1-配置"><a href="#9-1-配置" class="headerlink" title="9.1 配置"></a>9.1 配置</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">datasource:</span></span><br><span class="line">    <span class="attr">druid:</span></span><br><span class="line">      <span class="attr">initial-size:</span> <span class="number">5</span></span><br><span class="line">      <span class="attr">max-active:</span> <span class="number">20</span></span><br><span class="line">      <span class="attr">min-idle:</span> <span class="number">5</span></span><br><span class="line">      <span class="attr">max-wait:</span> <span class="number">60000</span></span><br><span class="line">      <span class="comment"># 监控配置</span></span><br><span class="line">      <span class="attr">stat-view-servlet:</span></span><br><span class="line">        <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">        <span class="attr">login-username:</span> <span class="string">admin</span></span><br><span class="line">        <span class="attr">login-password:</span> <span class="string">admin</span></span><br><span class="line">        <span class="attr">url-pattern:</span> <span class="string">/druid/*</span></span><br><span class="line">      <span class="attr">web-stat-filter:</span></span><br><span class="line">        <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">        <span class="attr">url-pattern:</span> <span class="string">/*</span></span><br></pre></td></tr></table></figure><h3 id="9-2-访问-Druid-监控"><a href="#9-2-访问-Druid-监控" class="headerlink" title="9.2 访问 Druid 监控"></a>9.2 访问 Druid 监控</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:8080/springboot2c1hu/druid/</span><br></pre></td></tr></table></figure><p>可以查看 SQL 执行统计、慢查询、连接池状态等。生产环境建议关闭或加 IP 白名单。</p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><table><thead><tr><th>配置项</th><th>值</th><th>说明</th></tr></thead><tbody><tr><td>Spring Boot</td><td>3.4.1</td><td>Java 17</td></tr><tr><td>前端</td><td>Vue 3.5.34</td><td>Vite 构建</td></tr><tr><td>数据库</td><td>MySQL 8.0 + Druid</td><td>连接池</td></tr><tr><td>缓存</td><td>Redis 7 + Lettuce</td><td>异步客户端</td></tr><tr><td>ORM</td><td>MyBatis Plus 3.5.9</td><td>代码生成</td></tr><tr><td>安全</td><td>JWT + BCrypt</td><td>自定义拦截器</td></tr><tr><td>迁移</td><td>Flyway</td><td>版本化SQL + Java迁移</td></tr><tr><td>文档</td><td>SpringDoc 2.8.15</td><td>OpenAPI 3</td></tr><tr><td>部署</td><td>Docker Compose</td><td>一键启动</td></tr></tbody></table><p><strong>工程化要点</strong>：多环境配置（dev&#x2F;test&#x2F;prod）、环境变量注入、Flyway 版本化管理、Docker 编排、前端代理联调——这些是全栈项目的标配，不是可选项。</p>]]>
    </content>
    <id>http://example.com/2026/07/07/2026070703/</id>
    <link href="http://example.com/2026/07/07/2026070703/"/>
    <published>2026-07-07T04:12:51.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>Flyway 管理数据库版本、SpringDoc 自动生成接口文档、多环境配置、Docker 一键部署——这些不是可选项，而是全栈项目的标配。本文以社区团购系统为例，把 Spring Boot 3 工程化的完整流程从头到尾讲透。</p>
</blo]]>
    </summary>
    <title>Spring Boot 3 工程化实战：从数据库迁移到 Docker 部署</title>
    <updated>2026-07-07T10:00:06.746Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="Springboot" scheme="http://example.com/categories/Springboot/"/>
    <category term="java" scheme="http://example.com/tags/java/"/>
    <category term="Springboot" scheme="http://example.com/tags/Springboot/"/>
    <category term="Redis" scheme="http://example.com/tags/Redis/"/>
    <category term="RBAC" scheme="http://example.com/tags/RBAC/"/>
    <content>
      <![CDATA[<p> 大部分 RBAC 教程只讲”用户-角色-权限”三张表的设计，但真正的痛点是：不同角色的登录逻辑、密码修改、注册流程都不一样，用 if-else 写完就是一场灾难。本文基于 IntensifiedMyCode 和 springboot-core-arch 两个项目，把 RBAC 权限系统和 AOP 审计日志的完整实现从头到尾讲透。</p><hr><h2 id="第一章：if-else-地狱长什么样"><a href="#第一章：if-else-地狱长什么样" class="headerlink" title="第一章：if-else 地狱长什么样"></a>第一章：if-else 地狱长什么样</h2><p>假设有4种角色：超级管理员、部门管理员、社团负责人、普通用户。登录接口要区分角色走不同逻辑：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ 反面教材</span></span><br><span class="line"><span class="meta">@PostMapping(&quot;/login&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">login</span><span class="params">(<span class="meta">@RequestBody</span> Account account)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">role</span> <span class="operator">=</span> account.getRole();</span><br><span class="line">    <span class="keyword">if</span> (<span class="string">&quot;SUPER_ADMIN&quot;</span>.equals(role)) &#123;</span><br><span class="line">        <span class="keyword">return</span> R.success(adminService.login(account));</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">&quot;DEPT_ADMIN&quot;</span>.equals(role)) &#123;</span><br><span class="line">        <span class="keyword">return</span> R.success(deptAdminService.login(account));</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">&quot;CLUB_LEADER&quot;</span>.equals(role)) &#123;</span><br><span class="line">        <span class="keyword">return</span> R.success(clubLeaderService.login(account));</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">&quot;USER&quot;</span>.equals(role)) &#123;</span><br><span class="line">        <span class="keyword">return</span> R.success(userService.login(account));</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> R.error(<span class="string">&quot;未知角色&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@PostMapping(&quot;/register&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">register</span><span class="params">(<span class="meta">@RequestBody</span> Account account)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">role</span> <span class="operator">=</span> account.getRole();</span><br><span class="line">    <span class="keyword">if</span> (<span class="string">&quot;SUPER_ADMIN&quot;</span>.equals(role)) &#123;</span><br><span class="line">        <span class="keyword">return</span> R.error(<span class="string">&quot;管理员请联系超级管理员添加&quot;</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">&quot;USER&quot;</span>.equals(role)) &#123;</span><br><span class="line">        userService.register(account);</span><br><span class="line">        <span class="keyword">return</span> R.ok();</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@PostMapping(&quot;/updatePassword&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">updatePassword</span><span class="params">(<span class="meta">@RequestBody</span> Account account)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">role</span> <span class="operator">=</span> account.getRole();</span><br><span class="line">    <span class="keyword">if</span> (<span class="string">&quot;SUPER_ADMIN&quot;</span>.equals(role)) &#123;</span><br><span class="line">        adminService.updatePassword(account);</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>问题</strong>：</p><ol><li>每新增一个角色，所有相关方法都要加 if-else 分支</li><li>分支逻辑散落在各个 Controller 中，难以维护</li><li>违反开闭原则——修改角色逻辑要改已有代码</li><li>测试困难——每个分支都要单独覆盖</li></ol><hr><h2 id="第二章：角色枚举设计"><a href="#第二章：角色枚举设计" class="headerlink" title="第二章：角色枚举设计"></a>第二章：角色枚举设计</h2><h3 id="2-1-RoleEnum"><a href="#2-1-RoleEnum" class="headerlink" title="2.1 RoleEnum"></a>2.1 RoleEnum</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Getter</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">enum</span> <span class="title class_">RoleEnum</span> &#123;</span><br><span class="line">    SUPER_ADMIN(<span class="string">&quot;SUPER_ADMIN&quot;</span>, <span class="string">&quot;超级管理员&quot;</span>),</span><br><span class="line">    DEPT_ADMIN(<span class="string">&quot;DEPT_ADMIN&quot;</span>, <span class="string">&quot;部门管理员&quot;</span>),</span><br><span class="line">    CLUB_LEADER(<span class="string">&quot;CLUB_LEADER&quot;</span>, <span class="string">&quot;社团负责人&quot;</span>),</span><br><span class="line">    USER(<span class="string">&quot;USER&quot;</span>, <span class="string">&quot;普通用户&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String code;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String label;</span><br><span class="line"></span><br><span class="line">    RoleEnum(String code, String label) &#123;</span><br><span class="line">        <span class="built_in">this</span>.code = code;</span><br><span class="line">        <span class="built_in">this</span>.label = label;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 强校验：无效角色码直接抛异常</span></span><br><span class="line"><span class="comment">     * ★ 不要返回null，直接抛异常——快速失败</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> RoleEnum <span class="title function_">fromCode</span><span class="params">(String code)</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> (RoleEnum value : values()) &#123;</span><br><span class="line">            <span class="keyword">if</span> (value.getCode().equalsIgnoreCase(code)) &#123;</span><br><span class="line">                <span class="keyword">return</span> value;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">CustomerException</span>(<span class="string">&quot;无效角色标识: &quot;</span> + code);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 管理员角色判断</span></span><br><span class="line"><span class="comment">     * 三种管理员角色共用一套管理逻辑</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">isAdminRole</span><span class="params">(String code)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> SUPER_ADMIN.code.equals(code)</span><br><span class="line">            || DEPT_ADMIN.code.equals(code)</span><br><span class="line">            || CLUB_LEADER.code.equals(code);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">isUserRole</span><span class="params">(String code)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> USER.code.equals(code);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>枚举的好处</strong>：角色码集中管理，不会出现拼写错误。<code>fromCode</code> 做强校验——传一个不存在的角色码会直接抛异常，而不是静默走错分支。</p><hr><h2 id="第三章：策略模式实现"><a href="#第三章：策略模式实现" class="headerlink" title="第三章：策略模式实现"></a>第三章：策略模式实现</h2><h3 id="3-1-策略接口"><a href="#3-1-策略接口" class="headerlink" title="3.1 策略接口"></a>3.1 策略接口</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">RoleStrategy</span> &#123;</span><br><span class="line">    String <span class="title function_">getRole</span><span class="params">()</span>;                          <span class="comment">// 角色标识</span></span><br><span class="line">    Account <span class="title function_">login</span><span class="params">(Account account)</span>;            <span class="comment">// 登录</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">updatePassword</span><span class="params">(Account account)</span>;       <span class="comment">// 改密码</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">register</span><span class="params">(Account account)</span>;             <span class="comment">// 注册</span></span><br><span class="line">    Account <span class="title function_">selectById</span><span class="params">(String userId)</span>;          <span class="comment">// 查用户</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-2-策略实现"><a href="#3-2-策略实现" class="headerlink" title="3.2 策略实现"></a>3.2 策略实现</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 管理员策略</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AdminStrategy</span> <span class="keyword">implements</span> <span class="title class_">RoleStrategy</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> AdminService adminService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">getRole</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> RoleEnum.SUPER_ADMIN.getCode();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Account <span class="title function_">login</span><span class="params">(Account account)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> adminService.login(account);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">updatePassword</span><span class="params">(Account account)</span> &#123;</span><br><span class="line">        adminService.updatePassword(account);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">register</span><span class="params">(Account account)</span> &#123;</span><br><span class="line">        <span class="comment">// 管理员不允许自助注册</span></span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnsupportedOperationException</span>(<span class="string">&quot;管理员账号不支持自助注册&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Account <span class="title function_">selectById</span><span class="params">(String userId)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> adminService.selectById(userId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 普通用户策略</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserStrategy</span> <span class="keyword">implements</span> <span class="title class_">RoleStrategy</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">getRole</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> RoleEnum.USER.getCode();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Account <span class="title function_">login</span><span class="params">(Account account)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> userService.login(account);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">updatePassword</span><span class="params">(Account account)</span> &#123;</span><br><span class="line">        userService.updatePassword(account);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">register</span><span class="params">(Account account)</span> &#123;</span><br><span class="line">        userService.register(account);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Account <span class="title function_">selectById</span><span class="params">(String userId)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> userService.selectById(userId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-3-策略上下文：Spring-自动装配的魔法"><a href="#3-3-策略上下文：Spring-自动装配的魔法" class="headerlink" title="3.3 策略上下文：Spring 自动装配的魔法"></a>3.3 策略上下文：Spring 自动装配的魔法</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RoleStrategyContext</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Map&lt;String, RoleStrategy&gt; strategyMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ★ Spring 构造器注入：自动收集所有 RoleStrategy 实现</span></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">RoleStrategyContext</span><span class="params">(List&lt;RoleStrategy&gt; strategies)</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> (RoleStrategy strategy : strategies) &#123;</span><br><span class="line">            strategyMap.put(strategy.getRole().toUpperCase(), strategy);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 根据角色码获取策略</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> RoleStrategy <span class="title function_">getStrategy</span><span class="params">(String roleCode)</span> &#123;</span><br><span class="line">        RoleEnum.fromCode(roleCode); <span class="comment">// 强校验角色合法性</span></span><br><span class="line">        <span class="type">RoleStrategy</span> <span class="variable">strategy</span> <span class="operator">=</span> strategyMap.get(roleCode.toUpperCase());</span><br><span class="line">        <span class="keyword">if</span> (strategy == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">CustomerException</span>(<span class="string">&quot;暂未支持的角色类型: &quot;</span> + roleCode);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> strategy;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Token 刷新（策略路由）</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">refreshToken</span><span class="params">(String oldToken)</span> &#123;</span><br><span class="line">        <span class="type">DecodedJWT</span> <span class="variable">decodedJWT</span> <span class="operator">=</span> JwtUtil.verifyToken(oldToken);</span><br><span class="line">        <span class="type">String</span> <span class="variable">userId</span> <span class="operator">=</span> decodedJWT.getSubject();</span><br><span class="line">        List&lt;String&gt; roles = decodedJWT.getClaim(<span class="string">&quot;roles&quot;</span>).asList(String.class);</span><br><span class="line">        <span class="type">String</span> <span class="variable">role</span> <span class="operator">=</span> roles.get(<span class="number">0</span>).toUpperCase();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 根据角色找到对应的策略，查用户，生成新Token</span></span><br><span class="line">        <span class="type">RoleStrategy</span> <span class="variable">strategy</span> <span class="operator">=</span> getStrategy(role);</span><br><span class="line">        <span class="type">Account</span> <span class="variable">account</span> <span class="operator">=</span> strategy.selectById(userId);</span><br><span class="line">        <span class="keyword">return</span> JwtUtil.generateAccessToken(</span><br><span class="line">                String.valueOf(account.getId()),</span><br><span class="line">                List.of(account.getRole())</span><br><span class="line">        );</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-4-自动装配原理"><a href="#3-4-自动装配原理" class="headerlink" title="3.4 自动装配原理"></a>3.4 自动装配原理</h3><p>Spring 在初始化 <code>RoleStrategyContext</code> 时，发现构造器参数是 <code>List&lt;RoleStrategy&gt;</code>，会自动从容器中收集所有 <code>RoleStrategy</code> 类型的 Bean：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">Spring 容器扫描 @Component</span><br><span class="line">    │</span><br><span class="line">    ├── AdminStrategy (getRole() = &quot;SUPER_ADMIN&quot;)</span><br><span class="line">    └── UserStrategy  (getRole() = &quot;USER&quot;)</span><br><span class="line">    │</span><br><span class="line">    ▼ 注入到 List&lt;RoleStrategy&gt;</span><br><span class="line">    │</span><br><span class="line">RoleStrategyContext 构造</span><br><span class="line">    │</span><br><span class="line">    ▼ 遍历 List，按 getRole() 建 Map</span><br><span class="line">    │</span><br><span class="line">    strategyMap = &#123;</span><br><span class="line">        &quot;SUPER_ADMIN&quot; → AdminStrategy,</span><br><span class="line">        &quot;USER&quot;        → UserStrategy</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p><strong>好处</strong>：新增角色时，只需写一个新的 <code>@Component</code> 策略类，不需要修改 <code>RoleStrategyContext</code> 和任何已有代码——完全符合开闭原则。</p><h3 id="3-5-Controller-调用"><a href="#3-5-Controller-调用" class="headerlink" title="3.5 Controller 调用"></a>3.5 Controller 调用</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WebController</span> &#123;</span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> RoleStrategyContext roleStrategyContext;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostMapping(&quot;/login&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> R <span class="title function_">login</span><span class="params">(<span class="meta">@RequestBody</span> Account account)</span> &#123;</span><br><span class="line">        <span class="comment">// ★ 策略模式：一行代码搞定，不需要 if-else</span></span><br><span class="line">        <span class="type">LoginResult</span> <span class="variable">result</span> <span class="operator">=</span> roleStrategyContext</span><br><span class="line">            .getStrategy(account.getRole())</span><br><span class="line">            .login(account);</span><br><span class="line">        <span class="keyword">return</span> R.success(result);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostMapping(&quot;/updatePassword&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> R <span class="title function_">updatePassword</span><span class="params">(<span class="meta">@RequestBody</span> Account account)</span> &#123;</span><br><span class="line">        roleStrategyContext.getStrategy(account.getRole()).updatePassword(account);</span><br><span class="line">        <span class="keyword">return</span> R.ok();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostMapping(&quot;/register&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> R <span class="title function_">register</span><span class="params">(<span class="meta">@RequestBody</span> Account account)</span> &#123;</span><br><span class="line">        <span class="comment">// 安全控制：只允许注册普通用户</span></span><br><span class="line">        account.setRole(RoleEnum.USER.getCode());</span><br><span class="line">        roleStrategyContext.getStrategy(account.getRole()).register(account);</span><br><span class="line">        <span class="keyword">return</span> R.ok();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>对比 if-else 版本</strong>：Controller 代码量减少 80%，且新增角色时无需改动 Controller。</p><hr><h2 id="第四章：RBAC-数据库设计"><a href="#第四章：RBAC-数据库设计" class="headerlink" title="第四章：RBAC 数据库设计"></a>第四章：RBAC 数据库设计</h2><h3 id="4-1-经典三表结构"><a href="#4-1-经典三表结构" class="headerlink" title="4.1 经典三表结构"></a>4.1 经典三表结构</h3><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 角色表</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> `roles` (</span><br><span class="line">    `id` <span class="type">int</span>(<span class="number">11</span>) <span class="keyword">NOT NULL</span> AUTO_INCREMENT,</span><br><span class="line">    `code` <span class="type">varchar</span>(<span class="number">30</span>) <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;角色编码&#x27;</span>,</span><br><span class="line">    `name` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">NOT NULL</span>,</span><br><span class="line">    `description` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">    <span class="keyword">PRIMARY KEY</span> (`id`),</span><br><span class="line">    <span class="keyword">UNIQUE</span> INDEX `role_code_unique`(`code`)</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 预置4个角色</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> `roles` <span class="keyword">VALUES</span></span><br><span class="line">    (<span class="number">1</span>, <span class="string">&#x27;SUPER_ADMIN&#x27;</span>, <span class="string">&#x27;超级管理员&#x27;</span>, <span class="string">&#x27;拥有全部管理权限&#x27;</span>),</span><br><span class="line">    (<span class="number">2</span>, <span class="string">&#x27;DEPT_ADMIN&#x27;</span>, <span class="string">&#x27;部门管理员&#x27;</span>, <span class="string">&#x27;管理部门内部事务&#x27;</span>),</span><br><span class="line">    (<span class="number">3</span>, <span class="string">&#x27;CLUB_LEADER&#x27;</span>, <span class="string">&#x27;社团负责人&#x27;</span>, <span class="string">&#x27;管理社团相关事务&#x27;</span>),</span><br><span class="line">    (<span class="number">4</span>, <span class="string">&#x27;USER&#x27;</span>, <span class="string">&#x27;普通用户&#x27;</span>, <span class="string">&#x27;仅可访问首页&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 用户表（统一 users 表，所有角色共用）</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> `users` (</span><br><span class="line">    `id` <span class="type">int</span>(<span class="number">11</span>) <span class="keyword">NOT NULL</span> AUTO_INCREMENT,</span><br><span class="line">    `username` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">NOT NULL</span>,</span><br><span class="line">    `password` <span class="type">varchar</span>(<span class="number">100</span>) <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;密码(BCrypt)&#x27;</span>,</span><br><span class="line">    `name` <span class="type">varchar</span>(<span class="number">20</span>) <span class="keyword">NOT NULL</span>,</span><br><span class="line">    <span class="keyword">PRIMARY KEY</span> (`id`),</span><br><span class="line">    <span class="keyword">UNIQUE</span> INDEX `username_index`(`username`)</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 用户-角色关联表（多对多）</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> `user_roles` (</span><br><span class="line">    `user_id` <span class="type">int</span>(<span class="number">11</span>) <span class="keyword">NOT NULL</span>,</span><br><span class="line">    `role_id` <span class="type">int</span>(<span class="number">11</span>) <span class="keyword">NOT NULL</span>,</span><br><span class="line">    <span class="keyword">PRIMARY KEY</span> (`user_id`, `role_id`)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h3 id="4-2-SQL-层的角色过滤"><a href="#4-2-SQL-层的角色过滤" class="headerlink" title="4.2 SQL 层的角色过滤"></a>4.2 SQL 层的角色过滤</h3><p>管理员和普通用户共用 <code>users</code> 表，通过 SQL JOIN 区分：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="operator">&lt;</span><span class="operator">!</span><span class="comment">-- 查询管理员（3种管理员角色） --&gt;</span></span><br><span class="line"><span class="keyword">SELECT</span> u.<span class="operator">*</span>, GROUP_CONCAT(r.code) <span class="keyword">AS</span> role</span><br><span class="line"><span class="keyword">FROM</span> users u</span><br><span class="line"><span class="keyword">INNER</span> <span class="keyword">JOIN</span> user_roles ur <span class="keyword">ON</span> u.id <span class="operator">=</span> ur.user_id</span><br><span class="line"><span class="keyword">INNER</span> <span class="keyword">JOIN</span> roles r <span class="keyword">ON</span> ur.role_id <span class="operator">=</span> r.id</span><br><span class="line"><span class="keyword">WHERE</span> r.code <span class="keyword">IN</span> (<span class="string">&#x27;SUPER_ADMIN&#x27;</span>, <span class="string">&#x27;DEPT_ADMIN&#x27;</span>, <span class="string">&#x27;CLUB_LEADER&#x27;</span>)</span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> u.id</span><br><span class="line"></span><br><span class="line"><span class="operator">&lt;</span><span class="operator">!</span><span class="comment">-- 查询普通用户（只查USER角色） --&gt;</span></span><br><span class="line"><span class="keyword">SELECT</span> u.<span class="operator">*</span>, GROUP_CONCAT(r.code) <span class="keyword">AS</span> role</span><br><span class="line"><span class="keyword">FROM</span> users u</span><br><span class="line"><span class="keyword">INNER</span> <span class="keyword">JOIN</span> user_roles ur <span class="keyword">ON</span> u.id <span class="operator">=</span> ur.user_id</span><br><span class="line"><span class="keyword">INNER</span> <span class="keyword">JOIN</span> roles r <span class="keyword">ON</span> ur.role_id <span class="operator">=</span> r.id</span><br><span class="line"><span class="keyword">WHERE</span> r.code <span class="operator">=</span> <span class="string">&#x27;USER&#x27;</span></span><br><span class="line"><span class="keyword">GROUP</span> <span class="keyword">BY</span> u.id</span><br></pre></td></tr></table></figure><p><strong>统一 users 表 vs 分表</strong>：统一表设计简单（不需要跨表 JOIN），分表隔离性好（不同角色字段不同）。本项目用统一表 + 角色码区分。</p><hr><h2 id="第五章：方法级权限注解"><a href="#第五章：方法级权限注解" class="headerlink" title="第五章：方法级权限注解"></a>第五章：方法级权限注解</h2><h3 id="5-1-启用注解权限"><a href="#5-1-启用注解权限" class="headerlink" title="5.1 启用注解权限"></a>5.1 启用注解权限</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableWebSecurity</span></span><br><span class="line"><span class="meta">@EnableMethodSecurity</span>  <span class="comment">// ★ 启用 @PreAuthorize / @PostAuthorize</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SecurityConfig</span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><h3 id="5-2-使用示例"><a href="#5-2-使用示例" class="headerlink" title="5.2 使用示例"></a>5.2 使用示例</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostAuthorize(&quot;hasRole(&#x27;SUPER_ADMIN&#x27;)&quot;)</span></span><br><span class="line"><span class="meta">@GetMapping(&quot;/getAll&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">getAllMenu</span><span class="params">()</span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><h3 id="5-3-⚠️-常见陷阱"><a href="#5-3-⚠️-常见陷阱" class="headerlink" title="5.3 ⚠️ 常见陷阱"></a>5.3 ⚠️ 常见陷阱</h3><p><code>hasRole(&#39;SUPER_ADMIN&#39;)</code> 需要 SecurityContext 中有 <code>ROLE_SUPER_ADMIN</code> 的 <code>GrantedAuthority</code>。但很多项目的 JWT 过滤器在设置 Authentication 时传了 <code>Collections.emptyList()</code>（空权限列表），导致 <code>@PostAuthorize</code> 永远拒绝访问。</p><p><strong>正确做法</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// JWT过滤器中设置权限</span></span><br><span class="line">List&lt;GrantedAuthority&gt; authorities = Arrays.asList(</span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">SimpleGrantedAuthority</span>(<span class="string">&quot;ROLE_&quot;</span> + role.toUpperCase())</span><br><span class="line">);</span><br><span class="line"><span class="type">UsernamePasswordAuthenticationToken</span> <span class="variable">authToken</span> <span class="operator">=</span></span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">UsernamePasswordAuthenticationToken</span>(</span><br><span class="line">        account.getUsername(), <span class="literal">null</span>, authorities);  <span class="comment">// ★ 不要传空列表</span></span><br><span class="line">SecurityContextHolder.getContext().setAuthentication(authToken);</span><br></pre></td></tr></table></figure><hr><h2 id="第六章：声明式审计日志"><a href="#第六章：声明式审计日志" class="headerlink" title="第六章：声明式审计日志"></a>第六章：声明式审计日志</h2><h3 id="6-1-目标效果"><a href="#6-1-目标效果" class="headerlink" title="6.1 目标效果"></a>6.1 目标效果</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@AuditLogRecord(action = &quot;登录&quot;, resource = &quot;用户&quot;)</span></span><br><span class="line"><span class="meta">@PostMapping(&quot;/login&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">login</span><span class="params">(<span class="meta">@RequestBody</span> Account account)</span> &#123; ... &#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@AuditLogRecord(action = &quot;添加管理员&quot;, resource = &quot;管理员&quot;)</span></span><br><span class="line"><span class="meta">@PostMapping(&quot;/add&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">add</span><span class="params">(<span class="meta">@RequestBody</span> Admin admin)</span> &#123; ... &#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@AuditLogRecord(action = &quot;修改密码&quot;, resource = &quot;用户&quot;)</span></span><br><span class="line"><span class="meta">@PostMapping(&quot;/updatePassword&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">updatePassword</span><span class="params">(<span class="meta">@RequestBody</span> Account account)</span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><p>一个注解搞定——不需要在方法体内写任何日志代码。</p><h3 id="6-2-审计日志注解"><a href="#6-2-审计日志注解" class="headerlink" title="6.2 审计日志注解"></a>6.2 审计日志注解</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Retention(RetentionPolicy.RUNTIME)</span></span><br><span class="line"><span class="meta">@Target(ElementType.METHOD)</span></span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> AuditLogRecord &#123;</span><br><span class="line">    String <span class="title function_">action</span><span class="params">()</span>;              <span class="comment">// 操作类型：&quot;登录&quot;、&quot;删除&quot;、&quot;修改密码&quot;</span></span><br><span class="line">    String <span class="title function_">resource</span><span class="params">()</span> <span class="keyword">default</span> <span class="string">&quot;&quot;</span>;<span class="comment">// 资源名称：&quot;用户&quot;、&quot;管理员&quot;、&quot;订单&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-3-审计日志实体"><a href="#6-3-审计日志实体" class="headerlink" title="6.3 审计日志实体"></a>6.3 审计日志实体</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span> <span class="meta">@Builder</span> <span class="meta">@NoArgsConstructor</span> <span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AuditLog</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> String username;      <span class="comment">// 操作人</span></span><br><span class="line">    <span class="keyword">private</span> String action;        <span class="comment">// 操作类型</span></span><br><span class="line">    <span class="keyword">private</span> String resource;      <span class="comment">// 受影响资源</span></span><br><span class="line">    <span class="keyword">private</span> String ipAddress;     <span class="comment">// 操作来源IP</span></span><br><span class="line">    <span class="keyword">private</span> String details;       <span class="comment">// 详情（已脱敏）</span></span><br><span class="line">    <span class="keyword">private</span> Timestamp createdAt;  <span class="comment">// 操作时间</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-4-审计日志表（Flyway初始化）"><a href="#6-4-审计日志表（Flyway初始化）" class="headerlink" title="6.4 审计日志表（Flyway初始化）"></a>6.4 审计日志表（Flyway初始化）</h3><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> `audit_log` (</span><br><span class="line">    `id` <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span> AUTO_INCREMENT,</span><br><span class="line">    `username` <span class="type">VARCHAR</span>(<span class="number">50</span>) <span class="keyword">NOT NULL</span>,</span><br><span class="line">    `action` <span class="type">VARCHAR</span>(<span class="number">100</span>) <span class="keyword">NOT NULL</span>,</span><br><span class="line">    `resource` <span class="type">VARCHAR</span>(<span class="number">50</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">    `ip_address` <span class="type">VARCHAR</span>(<span class="number">50</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">    `details` TEXT,</span><br><span class="line">    `created_at` <span class="type">TIMESTAMP</span> <span class="keyword">NOT NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span>,</span><br><span class="line">    <span class="keyword">PRIMARY KEY</span> (`id`),</span><br><span class="line">    INDEX `idx_username` (`username`),</span><br><span class="line">    INDEX `idx_created_at` (`created_at`)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><hr><h2 id="第七章：AOP-切面实现"><a href="#第七章：AOP-切面实现" class="headerlink" title="第七章：AOP 切面实现"></a>第七章：AOP 切面实现</h2><h3 id="7-1-核心代码"><a href="#7-1-核心代码" class="headerlink" title="7.1 核心代码"></a>7.1 核心代码</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Aspect</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AuditLogAspect</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> AuditLogService auditLogService;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * ★ <span class="doctag">@Around</span>(&quot;<span class="doctag">@annotation</span>(auditLogRecord)&quot;)</span></span><br><span class="line"><span class="comment">     * 匹配所有标注了 <span class="doctag">@AuditLogRecord</span> 注解的方法</span></span><br><span class="line"><span class="comment">     * auditLogRecord 参数名与注解变量名一致，Spring自动绑定</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Around(&quot;@annotation(auditLogRecord)&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Object <span class="title function_">handleAudit</span><span class="params">(ProceedingJoinPoint joinPoint, </span></span><br><span class="line"><span class="params">                               AuditLogRecord auditLogRecord)</span> <span class="keyword">throws</span> Throwable &#123;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 1. 获取当前用户</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> getCurrentUsername();</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 2. 获取客户端IP</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">ip</span> <span class="operator">=</span> getClientIp();</span><br><span class="line">        </span><br><span class="line">        <span class="type">long</span> <span class="variable">startTime</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 3. 执行目标方法</span></span><br><span class="line">            <span class="type">Object</span> <span class="variable">result</span> <span class="operator">=</span> joinPoint.proceed();</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// 4. 记录成功日志（脱敏处理）</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">safeDetails</span> <span class="operator">=</span> sanitizeArgs(joinPoint.getArgs());</span><br><span class="line">            </span><br><span class="line">            <span class="type">AuditLog</span> <span class="variable">successLog</span> <span class="operator">=</span> AuditLog.builder()</span><br><span class="line">                    .username(username)</span><br><span class="line">                    .action(auditLogRecord.action())</span><br><span class="line">                    .resource(auditLogRecord.resource())</span><br><span class="line">                    .ipAddress(ip)</span><br><span class="line">                    .details(safeDetails)</span><br><span class="line">                    .build();</span><br><span class="line">            auditLogService.saveLog(successLog);</span><br><span class="line">            </span><br><span class="line">            <span class="keyword">return</span> result;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Throwable e) &#123;</span><br><span class="line">            <span class="comment">// 5. 记录失败日志（脱敏 + 异常信息）</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">safeDetails</span> <span class="operator">=</span> sanitizeArgs(joinPoint.getArgs());</span><br><span class="line">            </span><br><span class="line">            <span class="type">AuditLog</span> <span class="variable">errorLog</span> <span class="operator">=</span> AuditLog.builder()</span><br><span class="line">                    .username(username)</span><br><span class="line">                    .action(auditLogRecord.action())</span><br><span class="line">                    .resource(auditLogRecord.resource())</span><br><span class="line">                    .ipAddress(ip)</span><br><span class="line">                    .details(safeDetails + <span class="string">&quot; | 异常: &quot;</span> + e.getMessage())</span><br><span class="line">                    .build();</span><br><span class="line">            auditLogService.saveLog(errorLog);</span><br><span class="line">            </span><br><span class="line">            <span class="keyword">throw</span> e; <span class="comment">// 继续抛出，交给全局异常处理器</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-2-切点表达式解析"><a href="#7-2-切点表达式解析" class="headerlink" title="7.2 切点表达式解析"></a>7.2 切点表达式解析</h3><p><code>@Around(&quot;@annotation(auditLogRecord)&quot;)</code> 的含义：</p><ul><li>匹配所有标注了 <code>@AuditLogRecord</code> 注解的方法</li><li><code>auditLogRecord</code> 参数名与注解变量名一致，Spring 会自动绑定注解实例</li></ul><p><strong>等价写法</strong>（手动获取注解）：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Around(&quot;@annotation(com.example.annotation.AuditLogRecord)&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Object <span class="title function_">handleAudit</span><span class="params">(ProceedingJoinPoint joinPoint)</span> <span class="keyword">throws</span> Throwable &#123;</span><br><span class="line">    <span class="type">MethodSignature</span> <span class="variable">signature</span> <span class="operator">=</span> (MethodSignature) joinPoint.getSignature();</span><br><span class="line">    <span class="type">AuditLogRecord</span> <span class="variable">auditLogRecord</span> <span class="operator">=</span> signature.getMethod()</span><br><span class="line">            .getAnnotation(AuditLogRecord.class);</span><br><span class="line">    <span class="comment">// ... 后续逻辑</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>前者更简洁，后者更灵活（可以在方法体内决定是否记录日志）。</p><hr><h2 id="第八章：密码脱敏"><a href="#第八章：密码脱敏" class="headerlink" title="第八章：密码脱敏"></a>第八章：密码脱敏</h2><h3 id="8-1-问题"><a href="#8-1-问题" class="headerlink" title="8.1 问题"></a>8.1 问题</h3><p>如果直接 <code>Arrays.toString(args)</code> 记录方法参数，密码会以明文出现在审计日志中：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">details: [Account(username=admin, password=123456, role=SUPER_ADMIN)]</span><br></pre></td></tr></table></figure><p>审计日志通常有 DBA、运维等多方查看权限，密码明文是严重的安全隐患。</p><h3 id="8-2-正则脱敏实现"><a href="#8-2-正则脱敏实现" class="headerlink" title="8.2 正则脱敏实现"></a>8.2 正则脱敏实现</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 脱敏方法参数</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> String <span class="title function_">sanitizeArgs</span><span class="params">(Object[] args)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (args == <span class="literal">null</span> || args.length == <span class="number">0</span>) <span class="keyword">return</span> <span class="string">&quot;[]&quot;</span>;</span><br><span class="line">    <span class="type">String</span> <span class="variable">sanitized</span> <span class="operator">=</span> Arrays.stream(args)</span><br><span class="line">            .map(<span class="built_in">this</span>::sanitizeArg)</span><br><span class="line">            .collect(Collectors.joining(<span class="string">&quot;, &quot;</span>));</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;[&quot;</span> + sanitized + <span class="string">&quot;]&quot;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 脱敏单个参数</span></span><br><span class="line"><span class="comment"> * 检测 password/pwd 字段并替换为 ***</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> String <span class="title function_">sanitizeArg</span><span class="params">(Object arg)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (arg == <span class="literal">null</span>) <span class="keyword">return</span> <span class="string">&quot;null&quot;</span>;</span><br><span class="line">    <span class="type">String</span> <span class="variable">argStr</span> <span class="operator">=</span> arg.toString();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 正则替换：password=xxx, newPassword=xxx 等</span></span><br><span class="line">    <span class="keyword">if</span> (argStr.toLowerCase().contains(<span class="string">&quot;password&quot;</span>) </span><br><span class="line">            || argStr.toLowerCase().contains(<span class="string">&quot;pwd&quot;</span>)) &#123;</span><br><span class="line">        argStr = argStr.replaceAll(</span><br><span class="line">            <span class="string">&quot;(?i)(password|pwd|newPassword|oldPassword)\\s*=\\s*[^,&#125;\\]]+&quot;</span>,</span><br><span class="line">            <span class="string">&quot;$1=***&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> argStr;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-3-正则解析"><a href="#8-3-正则解析" class="headerlink" title="8.3 正则解析"></a>8.3 正则解析</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(?i)(password|pwd|newPassword|oldPassword)\s*=\s*[^,&#125;\]]+</span><br></pre></td></tr></table></figure><table><thead><tr><th>部分</th><th>含义</th></tr></thead><tbody><tr><td><code>(?i)</code></td><td>大小写不敏感</td></tr><tr><td><code>(password|pwd|...)</code></td><td>匹配密码相关字段名</td></tr><tr><td><code>\s*=\s*</code></td><td>匹配等号（允许空格）</td></tr><tr><td><code>[^,}\]]+</code></td><td>匹配值（到逗号、右花括号或右方括号为止）</td></tr></tbody></table><p><strong>替换效果</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">输入: Account(username=admin, password=123456, role=SUPER_ADMIN)</span><br><span class="line">输出: Account(username=admin, password=***, role=SUPER_ADMIN)</span><br></pre></td></tr></table></figure><h3 id="8-4-脱敏的局限性"><a href="#8-4-脱敏的局限性" class="headerlink" title="8.4 脱敏的局限性"></a>8.4 脱敏的局限性</h3><p>这个正则方案依赖于对象的 <code>toString()</code> 输出格式为 <code>key=value</code>。对于 Lombok <code>@Data</code> 生成的 toString，格式是 <code>ClassName(field1=value1, field2=value2)</code>，能正常工作。</p><p>但如果参数是 JSON 字符串（<code>{&quot;password&quot;:&quot;123456&quot;}</code>），正则无法匹配。更健壮的方案：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 方案1：Jackson自定义序列化器（精确脱敏）</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PasswordSerializer</span> <span class="keyword">extends</span> <span class="title class_">JsonSerializer</span>&lt;String&gt; &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">serialize</span><span class="params">(String value, JsonGenerator gen, SerializerProvider provider)</span> &#123;</span><br><span class="line">        gen.writeString(<span class="string">&quot;***&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在实体字段上标注</span></span><br><span class="line"><span class="meta">@JsonSerialize(using = PasswordSerializer.class)</span></span><br><span class="line"><span class="keyword">private</span> String password;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方案2：自定义脱敏注解</span></span><br><span class="line"><span class="meta">@Target(ElementType.FIELD)</span></span><br><span class="line"><span class="meta">@Retention(RetentionPolicy.RUNTIME)</span></span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> Sensitive &#123;</span><br><span class="line">    String <span class="title function_">strategy</span><span class="params">()</span> <span class="keyword">default</span> <span class="string">&quot;PASSWORD&quot;</span>; <span class="comment">// PASSWORD, PHONE, EMAIL, ID_CARD</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="第九章：获取当前用户和IP"><a href="#第九章：获取当前用户和IP" class="headerlink" title="第九章：获取当前用户和IP"></a>第九章：获取当前用户和IP</h2><h3 id="9-1-获取当前用户"><a href="#9-1-获取当前用户" class="headerlink" title="9.1 获取当前用户"></a>9.1 获取当前用户</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> String <span class="title function_">getCurrentUsername</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="type">var</span> <span class="variable">authentication</span> <span class="operator">=</span> SecurityContextHolder.getContext().getAuthentication();</span><br><span class="line">        <span class="keyword">if</span> (authentication != <span class="literal">null</span> &amp;&amp; authentication.isAuthenticated()) &#123;</span><br><span class="line">            <span class="keyword">return</span> authentication.getName();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        log.debug(<span class="string">&quot;无法获取当前用户: &quot;</span> + e.getMessage());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;anonymous&quot;</span>; <span class="comment">// 登录接口可能还没有Authentication</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>为什么登录接口会是 anonymous</strong>：<code>@AuditLogRecord(action = &quot;登录&quot;)</code> 标注在登录方法上，但登录成功前 SecurityContext 中还没有 Authentication，所以用户名为 <code>anonymous</code>。解决方案：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 方案1：从方法参数中提取用户名</span></span><br><span class="line"><span class="meta">@Around(&quot;@annotation(auditLogRecord)&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Object <span class="title function_">handleAudit</span><span class="params">(ProceedingJoinPoint joinPoint, AuditLogRecord record)</span> <span class="keyword">throws</span> Throwable &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> getCurrentUsername();</span><br><span class="line">    <span class="keyword">if</span> (<span class="string">&quot;anonymous&quot;</span>.equals(username)) &#123;</span><br><span class="line">        <span class="comment">// 尝试从方法参数中提取</span></span><br><span class="line">        <span class="keyword">for</span> (Object arg : joinPoint.getArgs()) &#123;</span><br><span class="line">            <span class="keyword">if</span> (arg <span class="keyword">instanceof</span> Account) &#123;</span><br><span class="line">                username = ((Account) arg).getUsername();</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="9-2-获取客户端IP"><a href="#9-2-获取客户端IP" class="headerlink" title="9.2 获取客户端IP"></a>9.2 获取客户端IP</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> String <span class="title function_">getClientIp</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="type">ServletRequestAttributes</span> <span class="variable">attrs</span> <span class="operator">=</span> (ServletRequestAttributes) </span><br><span class="line">            RequestContextHolder.currentRequestAttributes();</span><br><span class="line">        <span class="type">HttpServletRequest</span> <span class="variable">request</span> <span class="operator">=</span> attrs.getRequest();</span><br><span class="line">        </span><br><span class="line">        <span class="type">String</span> <span class="variable">ip</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;X-Forwarded-For&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (ip == <span class="literal">null</span> || ip.isEmpty() || <span class="string">&quot;unknown&quot;</span>.equalsIgnoreCase(ip)) &#123;</span><br><span class="line">            ip = request.getHeader(<span class="string">&quot;X-Real-IP&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (ip == <span class="literal">null</span> || ip.isEmpty() || <span class="string">&quot;unknown&quot;</span>.equalsIgnoreCase(ip)) &#123;</span><br><span class="line">            ip = request.getRemoteAddr();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 多级代理时取第一个非unknown的IP</span></span><br><span class="line">        <span class="keyword">if</span> (ip != <span class="literal">null</span> &amp;&amp; ip.contains(<span class="string">&quot;,&quot;</span>)) &#123;</span><br><span class="line">            ip = ip.split(<span class="string">&quot;,&quot;</span>)[<span class="number">0</span>].trim();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> ip;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;unknown&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="第十章：分布式锁防重复注册"><a href="#第十章：分布式锁防重复注册" class="headerlink" title="第十章：分布式锁防重复注册"></a>第十章：分布式锁防重复注册</h2><h3 id="10-1-分布式锁工具"><a href="#10-1-分布式锁工具" class="headerlink" title="10.1 分布式锁工具"></a>10.1 分布式锁工具</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DistributedLockUtils</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 加锁：SETNX + 过期时间</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(String lockKey, String requestId, <span class="type">int</span> expireTime)</span> &#123;</span><br><span class="line">        <span class="type">Boolean</span> <span class="variable">result</span> <span class="operator">=</span> redisTemplate.opsForValue()</span><br><span class="line">                .setIfAbsent(<span class="string">&quot;lock:&quot;</span> + lockKey, requestId, expireTime, TimeUnit.SECONDS);</span><br><span class="line">        <span class="keyword">return</span> result != <span class="literal">null</span> &amp;&amp; result;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 释放锁：Lua脚本保证原子性（先比较再删除）</span></span><br><span class="line">    <span class="comment">// ★ 必须用Lua脚本，否则get和delete之间可能有其他线程介入</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">releaseLock</span><span class="params">(String lockKey, String requestId)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">luaScript</span> <span class="operator">=</span></span><br><span class="line">            <span class="string">&quot;if redis.call(&#x27;get&#x27;, KEYS[1]) == ARGV[1] then &quot;</span> +</span><br><span class="line">            <span class="string">&quot;   return redis.call(&#x27;del&#x27;, KEYS[1]) &quot;</span> +</span><br><span class="line">            <span class="string">&quot;else return 0 end&quot;</span>;</span><br><span class="line">        DefaultRedisScript&lt;Long&gt; redisScript = <span class="keyword">new</span> <span class="title class_">DefaultRedisScript</span>&lt;&gt;(luaScript, Long.class);</span><br><span class="line">        <span class="type">Long</span> <span class="variable">result</span> <span class="operator">=</span> redisTemplate.execute(redisScript, </span><br><span class="line">            Collections.singletonList(<span class="string">&quot;lock:&quot;</span> + lockKey), requestId);</span><br><span class="line">        <span class="keyword">return</span> result != <span class="literal">null</span> &amp;&amp; result &gt; <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-2-使用场景"><a href="#10-2-使用场景" class="headerlink" title="10.2 使用场景"></a>10.2 使用场景</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">register</span><span class="params">(User user)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> <span class="string">&quot;user:register:&quot;</span> + user.getUsername();</span><br><span class="line">    <span class="type">String</span> <span class="variable">requestId</span> <span class="operator">=</span> String.valueOf(System.currentTimeMillis());</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (distributedLockUtils.tryLock(lockKey, requestId, <span class="number">10</span>)) &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 检查用户名是否已存在</span></span><br><span class="line">            <span class="keyword">if</span> (userMapper.findByUsername(user.getUsername()) != <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">CustomerException</span>(<span class="string">&quot;用户名已存在&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// BCrypt加密密码</span></span><br><span class="line">            user.setPassword(PasswordEncoder.encode(user.getPassword()));</span><br><span class="line">            userMapper.insert(user);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            distributedLockUtils.releaseLock(lockKey, requestId);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">CustomerException</span>(<span class="string">&quot;系统繁忙，请稍后再试&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>为什么需要分布式锁</strong>：用户快速点击两次注册按钮，两个请求同时检查用户名（都显示不存在），然后都执行 INSERT——第二个会因唯一约束报错，但第一个已经创建了用户。分布式锁确保同一用户名的注册操作串行执行。</p><hr><h2 id="第十一章：审计日志查询"><a href="#第十一章：审计日志查询" class="headerlink" title="第十一章：审计日志查询"></a>第十一章：审计日志查询</h2><h3 id="11-1-分页查询"><a href="#11-1-分页查询" class="headerlink" title="11.1 分页查询"></a>11.1 分页查询</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/auditLog/page&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">page</span><span class="params">(<span class="meta">@RequestParam(defaultValue = &quot;1&quot;)</span> <span class="type">int</span> pageNum,</span></span><br><span class="line"><span class="params">              <span class="meta">@RequestParam(defaultValue = &quot;10&quot;)</span> <span class="type">int</span> pageSize,</span></span><br><span class="line"><span class="params">              <span class="meta">@RequestParam(required = false)</span> String username,</span></span><br><span class="line"><span class="params">              <span class="meta">@RequestParam(required = false)</span> String action)</span> &#123;</span><br><span class="line">    Map&lt;String, Object&gt; params = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">    params.put(<span class="string">&quot;username&quot;</span>, username);</span><br><span class="line">    params.put(<span class="string">&quot;action&quot;</span>, action);</span><br><span class="line">    params.put(<span class="string">&quot;startIndex&quot;</span>, (pageNum - <span class="number">1</span>) * pageSize);</span><br><span class="line">    params.put(<span class="string">&quot;pageSize&quot;</span>, pageSize);</span><br><span class="line">    </span><br><span class="line">    List&lt;AuditLog&gt; list = auditLogService.selectPage(params);</span><br><span class="line">    <span class="type">int</span> <span class="variable">total</span> <span class="operator">=</span> auditLogService.selectCount(params);</span><br><span class="line">    <span class="keyword">return</span> R.success(<span class="keyword">new</span> <span class="title class_">PageResult</span>(list, total, pageNum, pageSize));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="11-2-MyBatis-XML"><a href="#11-2-MyBatis-XML" class="headerlink" title="11.2 MyBatis XML"></a>11.2 MyBatis XML</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">select</span> <span class="attr">id</span>=<span class="string">&quot;selectPage&quot;</span> <span class="attr">resultType</span>=<span class="string">&quot;AuditLog&quot;</span>&gt;</span></span><br><span class="line">    SELECT * FROM audit_log</span><br><span class="line">    WHERE 1=1</span><br><span class="line">    <span class="tag">&lt;<span class="name">if</span> <span class="attr">test</span>=<span class="string">&quot;username != null and username != &#x27;&#x27;&quot;</span>&gt;</span></span><br><span class="line">        AND username LIKE CONCAT(&#x27;%&#x27;, #&#123;username&#125;, &#x27;%&#x27;)</span><br><span class="line">    <span class="tag">&lt;/<span class="name">if</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">if</span> <span class="attr">test</span>=<span class="string">&quot;action != null and action != &#x27;&#x27;&quot;</span>&gt;</span></span><br><span class="line">        AND action = #&#123;action&#125;</span><br><span class="line">    <span class="tag">&lt;/<span class="name">if</span>&gt;</span></span><br><span class="line">    ORDER BY created_at DESC</span><br><span class="line">    LIMIT #&#123;startIndex&#125;, #&#123;pageSize&#125;</span><br><span class="line"><span class="tag">&lt;/<span class="name">select</span>&gt;</span></span><br></pre></td></tr></table></figure><hr><h2 id="第十二章：性能考虑"><a href="#第十二章：性能考虑" class="headerlink" title="第十二章：性能考虑"></a>第十二章：性能考虑</h2><h3 id="12-1-同步-vs-异步记录"><a href="#12-1-同步-vs-异步记录" class="headerlink" title="12.1 同步 vs 异步记录"></a>12.1 同步 vs 异步记录</h3><p>本文实现是同步记录（<code>auditLogService.saveLog()</code> 在请求线程中执行）。对于高并发场景，建议改为异步：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 方案1：@Async异步记录</span></span><br><span class="line"><span class="meta">@Async</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">saveLog</span><span class="params">(AuditLog log)</span> &#123;</span><br><span class="line">    auditLogMapper.insert(log);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方案2：消息队列</span></span><br><span class="line"><span class="meta">@Around(&quot;@annotation(auditLogRecord)&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Object <span class="title function_">handleAudit</span><span class="params">(ProceedingJoinPoint joinPoint, AuditLogRecord record)</span> <span class="keyword">throws</span> Throwable &#123;</span><br><span class="line">    <span class="comment">// ... 收集日志信息</span></span><br><span class="line">    rabbitTemplate.convertAndSend(<span class="string">&quot;audit.log.queue&quot;</span>, auditLog); <span class="comment">// 发到MQ</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方案3：批量写入（Buffer + 定时刷盘）</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> List&lt;AuditLog&gt; buffer = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title function_">saveLog</span><span class="params">(AuditLog log)</span> &#123;</span><br><span class="line">    buffer.add(log);</span><br><span class="line">    <span class="keyword">if</span> (buffer.size() &gt;= <span class="number">100</span>) &#123;</span><br><span class="line">        auditLogMapper.batchInsert(buffer);</span><br><span class="line">        buffer.clear();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="12-2-AOP-性能开销"><a href="#12-2-AOP-性能开销" class="headerlink" title="12.2 AOP 性能开销"></a>12.2 AOP 性能开销</h3><table><thead><tr><th>操作</th><th>耗时</th></tr></thead><tbody><tr><td>AOP 代理创建</td><td>应用启动时一次性开销</td></tr><tr><td>切点匹配</td><td>每次方法调用 ~0.01ms</td></tr><tr><td>参数序列化+脱敏</td><td>~0.1-1ms（取决于参数复杂度）</td></tr><tr><td>数据库写入</td><td>~1-5ms</td></tr></tbody></table><p>对于后台管理系统，这个开销可接受。对于超高并发的接口，建议只记录关键操作。</p><hr><h2 id="第十三章：策略模式扩展指南"><a href="#第十三章：策略模式扩展指南" class="headerlink" title="第十三章：策略模式扩展指南"></a>第十三章：策略模式扩展指南</h2><p>新增 <code>TEACHER</code> 角色只需 3 步：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 1. 枚举加一项</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">enum</span> <span class="title class_">RoleEnum</span> &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    TEACHER(<span class="string">&quot;TEACHER&quot;</span>, <span class="string">&quot;教师&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 写策略实现</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TeacherStrategy</span> <span class="keyword">implements</span> <span class="title class_">RoleStrategy</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">getRole</span><span class="params">()</span> &#123; <span class="keyword">return</span> <span class="string">&quot;TEACHER&quot;</span>; &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Account <span class="title function_">login</span><span class="params">(Account account)</span> &#123; ... &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">updatePassword</span><span class="params">(Account account)</span> &#123; ... &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">register</span><span class="params">(Account account)</span> &#123; ... &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Account <span class="title function_">selectById</span><span class="params">(String userId)</span> &#123; ... &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3. 数据库插入角色</span></span><br><span class="line">INSERT INTO <span class="title function_">roles</span> <span class="params">(code, name)</span> VALUES (<span class="string">&#x27;TEACHER&#x27;</span>, <span class="string">&#x27;教师&#x27;</span>);</span><br></pre></td></tr></table></figure><p><strong>不需要改</strong>：Controller、RoleStrategyContext、任何已有策略类。这就是策略模式 + Spring 自动装配的威力。</p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><table><thead><tr><th>技术点</th><th>实现</th><th>核心文件</th></tr></thead><tbody><tr><td>策略模式</td><td><code>RoleStrategy</code> 接口 + <code>RoleStrategyContext</code> + <code>List&lt;RoleStrategy&gt;</code> 自动注入</td><td>RoleStrategy.java</td></tr><tr><td>角色管理</td><td><code>RoleEnum</code> 枚举 + <code>fromCode</code> 强校验</td><td>RoleEnum.java</td></tr><tr><td>RBAC 三表</td><td>roles + users + user_roles（多对多）</td><td>SQL</td></tr><tr><td>JWT 双Token</td><td>AccessToken(30min) + RefreshToken(7天)</td><td>TokenUtils.java</td></tr><tr><td>声明式审计</td><td><code>@AuditLogRecord</code> 注解 + <code>@Around</code> AOP 切面</td><td>AuditLogAspect.java</td></tr><tr><td>密码脱敏</td><td>正则 <code>(?i)(password|pwd)\s*=\s*[^,}\]]+</code> 替换为 <code>***</code></td><td>AuditLogAspect.java</td></tr><tr><td>分布式锁</td><td>Redis SETNX + Lua释放</td><td>DistributedLockUtils.java</td></tr><tr><td>方法级权限</td><td><code>@EnableMethodSecurity</code> + <code>@PostAuthorize</code></td><td>SecurityConfig.java</td></tr><tr><td>密码加密</td><td>BCrypt + 自动加盐 + cost factor</td><td>PasswordEncoder.java</td></tr></tbody></table><p><strong>核心思想</strong>：用策略模式把”不同角色的不同行为”抽象成接口，用 Spring 的 <code>List&lt;T&gt;</code> 自动注入收集所有实现，用 Map 路由。用 AOP 把审计日志这个横切关注点从业务代码中剥离出来，让 Controller 保持干净。一行注解，背后是代理模式 + 环绕通知 + 反射 + 正则脱敏的完整体系。</p>]]>
    </content>
    <id>http://example.com/2026/07/07/2026070702/</id>
    <link href="http://example.com/2026/07/07/2026070702/"/>
    <published>2026-07-07T03:12:51.000Z</published>
    <summary>
      <![CDATA[<p> 大部分 RBAC 教程只讲”用户-角色-权限”三张表的设计，但真正的痛点是：不同角色的登录逻辑、密码修改、注册流程都不一样，用 if-else 写完就是一场灾难。本文基于 IntensifiedMyCode 和 springboot-core-arch 两个项目，把 RB]]>
    </summary>
    <title>RBAC 权限系统 + AOP 审计日志实战：从策略模式到密码脱敏</title>
    <updated>2026-07-07T10:00:06.667Z</updated>
  </entry>
  <entry>
    <author>
      <name>ZapcoMan</name>
    </author>
    <category term="Springboot" scheme="http://example.com/categories/Springboot/"/>
    <category term="java" scheme="http://example.com/tags/java/"/>
    <category term="Springboot" scheme="http://example.com/tags/Springboot/"/>
    <category term="Redis" scheme="http://example.com/tags/Redis/"/>
    <category term="JWT" scheme="http://example.com/tags/JWT/"/>
    <content>
      <![CDATA[<blockquote><p>JWT 是无状态的——签发后无法主动失效。但业务需求是：用户改密码后旧 Token 必须失效、退出登录后 Token 立即作废、Token 被盗用后不能在其他设备使用。怎么解决这些矛盾？本文用 4 个真实项目的代码，把 JWT 安全认证的完整知识体系从头到尾讲透。</p></blockquote><hr><h2 id="第一章：认证体系全景"><a href="#第一章：认证体系全景" class="headerlink" title="第一章：认证体系全景"></a>第一章：认证体系全景</h2><h3 id="1-1-纯-JWT-的局限性"><a href="#1-1-纯-JWT-的局限性" class="headerlink" title="1.1 纯 JWT 的局限性"></a>1.1 纯 JWT 的局限性</h3><p>JWT（JSON Web Token）的设计哲学是<strong>无状态</strong>——服务端不存储 Token，验证时只检查签名和过期时间。这带来了高性能（不需要查 Redis&#x2F;DB），但也带来了致命问题：</p><table><thead><tr><th>问题</th><th>描述</th><th>纯JWT能解决？</th></tr></thead><tbody><tr><td>主动失效</td><td>用户退出登录后 Token 立即作废</td><td>❌</td></tr><tr><td>改密失效</td><td>用户修改密码后旧 Token 失效</td><td>❌</td></tr><tr><td>Token 盗用</td><td>Token 被窃取后在其他设备使用</td><td>❌</td></tr><tr><td>强制踢出</td><td>管理员强制用户下线</td><td>❌</td></tr></tbody></table><h3 id="1-2-解决方案演进"><a href="#1-2-解决方案演进" class="headerlink" title="1.2 解决方案演进"></a>1.2 解决方案演进</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">纯JWT（无状态）</span><br><span class="line">    │ 不够安全</span><br><span class="line">    ▼</span><br><span class="line">JWT + Redis黑名单（Token废弃后加入黑名单）</span><br><span class="line">    │ 黑名单无限增长</span><br><span class="line">    ▼</span><br><span class="line">JWT + Redis白名单（只存当前有效Token）</span><br><span class="line">    │ Token被盗仍可使用</span><br><span class="line">    ▼</span><br><span class="line">JWT + IP绑定 + Redis白名单（Token绑定IP）</span><br><span class="line">    │ 移动端IP变化导致掉线</span><br><span class="line">    ▼</span><br><span class="line">JWT + 密码做密钥 + 自动续期（改密自动失效）</span><br><span class="line">    │ ← 当前最优方案</span><br></pre></td></tr></table></figure><h3 id="1-3-三重校验架构"><a href="#1-3-三重校验架构" class="headerlink" title="1.3 三重校验架构"></a>1.3 三重校验架构</h3><p>本文的核心方案采用<strong>三重校验</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">┌──────────┐     ①登录请求      ┌──────────────┐</span><br><span class="line">│  客户端   │ ──────────────────&gt; │  Controller  │</span><br><span class="line">│          │                     │  (login)     │</span><br><span class="line">│          │ &lt;────────────────── │              │</span><br><span class="line">│          │   ②返回JWT Token    └──────┬───────┘</span><br><span class="line">│          │                          │ ③生成Token</span><br><span class="line">│          │                          ▼ (写入IP)</span><br><span class="line">│          │                    ┌──────────────┐</span><br><span class="line">│          │     ④携带Token     │   JwtUtils    │</span><br><span class="line">│          │ ──────────────────&gt; │              │</span><br><span class="line">│          │                    └──────┬───────┘</span><br><span class="line">│  ┌───────┤                          │ ⑤验签名+IP</span><br><span class="line">│  │ 拦截器  │ &lt;─── ⑥Redis校验 ─── ┌──────────────────┐</span><br><span class="line">│  │        │ ──── ⑦放行/拒绝 ──&gt; │ RedisTokenService  │</span><br><span class="line">│  └───────┘                     │ (Token白名单)      │</span><br><span class="line">│                                 └──────────────────┘</span><br></pre></td></tr></table></figure><p><strong>三重校验</strong>：</p><ol><li>JWT 签名验证（防篡改）</li><li>IP 绑定校验（防盗用）</li><li>Redis 存在性校验（主动失效）</li></ol><hr><h2 id="第二章：Token-生成——把-IP-写入-Claims"><a href="#第二章：Token-生成——把-IP-写入-Claims" class="headerlink" title="第二章：Token 生成——把 IP 写入 Claims"></a>第二章：Token 生成——把 IP 写入 Claims</h2><h3 id="2-1-JwtUtils-完整实现"><a href="#2-1-JwtUtils-完整实现" class="headerlink" title="2.1 JwtUtils 完整实现"></a>2.1 JwtUtils 完整实现</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtUtils</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;jwt.secret&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String secret;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;jwt.expiration&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">long</span> expiration; <span class="comment">// 1小时（毫秒）</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 生成 JWT Token</span></span><br><span class="line"><span class="comment">     * ★ 将客户端IP写入Claims实现IP绑定</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> userId    用户ID</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> username  用户名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> tableName 用户类型表名（区分管理员users和普通用户yonghu）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> role      角色</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> ip        客户端IP（★ IP绑定的核心）</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">generateToken</span><span class="params">(Long userId, String username, </span></span><br><span class="line"><span class="params">                                 String tableName, String role, String ip)</span> &#123;</span><br><span class="line">        Map&lt;String, Object&gt; claims = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        claims.put(<span class="string">&quot;userId&quot;</span>, userId);</span><br><span class="line">        claims.put(<span class="string">&quot;username&quot;</span>, username);</span><br><span class="line">        claims.put(<span class="string">&quot;tableName&quot;</span>, tableName);  <span class="comment">// 区分用户类型: users/yonghu</span></span><br><span class="line">        claims.put(<span class="string">&quot;role&quot;</span>, role);</span><br><span class="line">        claims.put(<span class="string">&quot;ip&quot;</span>, ip);                 <span class="comment">// ★ IP绑定核心</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> Jwts.builder()</span><br><span class="line">                .claims(claims)</span><br><span class="line">                .subject(username)</span><br><span class="line">                .issuedAt(<span class="keyword">new</span> <span class="title class_">Date</span>())</span><br><span class="line">                .expiration(<span class="keyword">new</span> <span class="title class_">Date</span>(System.currentTimeMillis() + expiration))</span><br><span class="line">                .signWith(getSigningKey())   <span class="comment">// HMAC-SHA256</span></span><br><span class="line">                .compact();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 验证 Token —— 签名 + 过期 + IP绑定 三重校验</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">validateToken</span><span class="params">(String token, String requestIp)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">Claims</span> <span class="variable">claims</span> <span class="operator">=</span> parseToken(token);</span><br><span class="line">            <span class="type">String</span> <span class="variable">tokenIp</span> <span class="operator">=</span> claims.get(<span class="string">&quot;ip&quot;</span>, String.class);</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// ★ IP 绑定验证：Token中记录的IP必须与当前请求IP匹配</span></span><br><span class="line">            <span class="comment">// 如果生成时没写入IP（tokenIp == null），跳过IP校验</span></span><br><span class="line">            <span class="comment">// 这是兼容移动端的策略——移动端可以不绑IP</span></span><br><span class="line">            <span class="keyword">if</span> (tokenIp != <span class="literal">null</span> &amp;&amp; !tokenIp.isEmpty() &amp;&amp; !tokenIp.equals(requestIp)) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">false</span>;  <span class="comment">// IP不匹配 → Token被盗用</span></span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (JwtException | IllegalArgumentException e) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> SecretKey <span class="title function_">getSigningKey</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>设计要点</strong>：<code>tableName</code> 字段区分管理员（<code>users</code> 表）和普通用户（<code>yonghu</code> 表），同一系统两套登录入口共用一套 JWT 体系。<code>ip</code> 字段的 null 检查是兼容移动端的关键——Web 端绑 IP，移动端不绑。</p><h3 id="2-2-客户端真实-IP-获取"><a href="#2-2-客户端真实-IP-获取" class="headerlink" title="2.2 客户端真实 IP 获取"></a>2.2 客户端真实 IP 获取</h3><p>Nginx 反向代理时 <code>request.getRemoteAddr()</code> 返回的是 Nginx 的 IP（127.0.0.1），需要从 HTTP 头获取真实 IP：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">getClientIp</span><span class="params">(HttpServletRequest request)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">ip</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;X-Forwarded-For&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (ip == <span class="literal">null</span> || ip.isEmpty() || <span class="string">&quot;unknown&quot;</span>.equalsIgnoreCase(ip)) &#123;</span><br><span class="line">        ip = request.getHeader(<span class="string">&quot;Proxy-Client-IP&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (ip == <span class="literal">null</span> || ip.isEmpty() || <span class="string">&quot;unknown&quot;</span>.equalsIgnoreCase(ip)) &#123;</span><br><span class="line">        ip = request.getHeader(<span class="string">&quot;WL-Proxy-Client-IP&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (ip == <span class="literal">null</span> || ip.isEmpty() || <span class="string">&quot;unknown&quot;</span>.equalsIgnoreCase(ip)) &#123;</span><br><span class="line">        ip = request.getHeader(<span class="string">&quot;X-Real-IP&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (ip == <span class="literal">null</span> || ip.isEmpty() || <span class="string">&quot;unknown&quot;</span>.equalsIgnoreCase(ip)) &#123;</span><br><span class="line">        ip = request.getRemoteAddr();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 多级代理时取第一个非unknown的IP</span></span><br><span class="line">    <span class="comment">// X-Forwarded-For 格式: client, proxy1, proxy2</span></span><br><span class="line">    <span class="keyword">if</span> (ip != <span class="literal">null</span> &amp;&amp; ip.contains(<span class="string">&quot;,&quot;</span>)) &#123;</span><br><span class="line">        ip = ip.split(<span class="string">&quot;,&quot;</span>)[<span class="number">0</span>].trim();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> ip;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Header 优先级</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">X-Forwarded-For → Proxy-Client-IP → WL-Proxy-Client-IP → X-Real-IP → RemoteAddr</span><br></pre></td></tr></table></figure><p><strong>Nginx 配置</strong>（必须设置这些 Header）：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> / &#123;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://backend;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>$proxy_add_x_forwarded_for</code> 会自动追加当前请求的 <code>$remote_addr</code> 到 <code>X-Forwarded-For</code> 链。多级代理时，第一个值是真实客户端 IP。</p><hr><h2 id="第三章：Redis-白名单——让-JWT-可主动失效"><a href="#第三章：Redis-白名单——让-JWT-可主动失效" class="headerlink" title="第三章：Redis 白名单——让 JWT 可主动失效"></a>第三章：Redis 白名单——让 JWT 可主动失效</h2><h3 id="3-1-RedisTokenService-实现"><a href="#3-1-RedisTokenService-实现" class="headerlink" title="3.1 RedisTokenService 实现"></a>3.1 RedisTokenService 实现</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisTokenService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;token.redis-prefix&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String redisPrefix;     <span class="comment">// &quot;token:&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;token.expire-seconds&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">long</span> expireSeconds;     <span class="comment">// 3600秒</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 存储 Token 会话到 Redis（Hash结构）</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * 为什么用 Hash 而非 String？</span></span><br><span class="line"><span class="comment">     * Hash 可以存储多个字段（userId/username/tableName/role）</span></span><br><span class="line"><span class="comment">     * 验证时可以取出单个字段，不需要反序列化整个对象</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">saveToken</span><span class="params">(String token, Long userId, String username, </span></span><br><span class="line"><span class="params">                           String tableName, String role)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> redisPrefix + token;</span><br><span class="line">        Map&lt;String, Object&gt; session = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        session.put(<span class="string">&quot;userId&quot;</span>, userId);</span><br><span class="line">        session.put(<span class="string">&quot;username&quot;</span>, username);</span><br><span class="line">        session.put(<span class="string">&quot;tableName&quot;</span>, tableName);</span><br><span class="line">        session.put(<span class="string">&quot;role&quot;</span>, role);</span><br><span class="line"></span><br><span class="line">        redisTemplate.opsForHash().putAll(key, session);</span><br><span class="line">        redisTemplate.expire(key, expireSeconds, TimeUnit.SECONDS);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获取 Token 会话 —— ★ 滑动续期</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * 每次有效请求都刷新TTL，活跃用户不会被迫重新登录</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> Map&lt;Object, Object&gt; <span class="title function_">getTokenSession</span><span class="params">(String token)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> redisPrefix + token;</span><br><span class="line">        Map&lt;Object, Object&gt; session = redisTemplate.opsForHash().entries(key);</span><br><span class="line">        <span class="keyword">if</span> (session.isEmpty()) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;  <span class="comment">// Token不存在或已过期</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// ★ 续期：每次有效请求刷新TTL</span></span><br><span class="line">        redisTemplate.expire(key, expireSeconds, TimeUnit.SECONDS);</span><br><span class="line">        <span class="keyword">return</span> session;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 删除 Token（退出登录时调用）</span></span><br><span class="line"><span class="comment">     * ★ 主动失效：即使JWT本身没过期，Redis中删除后也会被拦截器拒绝</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">removeToken</span><span class="params">(String token)</span> &#123;</span><br><span class="line">        redisTemplate.delete(redisPrefix + token);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-2-滑动过期-vs-固定过期"><a href="#3-2-滑动过期-vs-固定过期" class="headerlink" title="3.2 滑动过期 vs 固定过期"></a>3.2 滑动过期 vs 固定过期</h3><table><thead><tr><th>策略</th><th>Token 有效期</th><th>用户体验</th><th>Redis 内存</th></tr></thead><tbody><tr><td>固定过期</td><td>签发时设定，不续期</td><td>活跃用户也会被迫重新登录</td><td>可预测</td></tr><tr><td>滑动过期</td><td>每次有效请求刷新TTL</td><td>活跃用户持续操作不会掉线</td><td>可能偏高</td></tr></tbody></table><p>本项目用滑动过期——每次有效请求都刷新 1 小时 TTL，活跃用户在持续操作期间不会被迫重新登录。代价是 Redis 内存可能偏高（活跃用户的 Token 不会过期），但每个 Token 只占一个 Hash（约 200 字节），影响可忽略。</p><hr><h2 id="第四章：拦截器——三重校验"><a href="#第四章：拦截器——三重校验" class="headerlink" title="第四章：拦截器——三重校验"></a>第四章：拦截器——三重校验</h2><h3 id="4-1-AuthorizationInterceptor-完整实现"><a href="#4-1-AuthorizationInterceptor-完整实现" class="headerlink" title="4.1 AuthorizationInterceptor 完整实现"></a>4.1 AuthorizationInterceptor 完整实现</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AuthorizationInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">LOGIN_TOKEN_KEY</span> <span class="operator">=</span> <span class="string">&quot;Token&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> JwtUtils jwtUtils;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RedisTokenService redisTokenService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, </span></span><br><span class="line"><span class="params">                             Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 1. 跨域预检请求放行</span></span><br><span class="line">        <span class="keyword">if</span> (request.getMethod().equals(RequestMethod.OPTIONS.name())) &#123;</span><br><span class="line">            response.setStatus(HttpStatus.OK.value());</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2. @IgnoreAuth 注解的方法直接放行</span></span><br><span class="line">        IgnoreAuth annotation;</span><br><span class="line">        <span class="keyword">if</span> (handler <span class="keyword">instanceof</span> HandlerMethod) &#123;</span><br><span class="line">            annotation = ((HandlerMethod) handler).getMethodAnnotation(IgnoreAuth.class);</span><br><span class="line">            <span class="keyword">if</span>(annotation != <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3. 从请求头获取 Token</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(LOGIN_TOKEN_KEY);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ★ 第一重：JWT 签名 + IP 绑定校验</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">clientIp</span> <span class="operator">=</span> JwtUtils.getClientIp(request);</span><br><span class="line">        <span class="keyword">if</span> (StringUtils.isNotBlank(token) &amp;&amp; jwtUtils.validateToken(token, clientIp)) &#123;</span><br><span class="line">            </span><br><span class="line">            <span class="comment">// ★ 第二重：Redis 白名单校验</span></span><br><span class="line">            Map&lt;Object, Object&gt; session = redisTokenService.getTokenSession(token);</span><br><span class="line">            <span class="keyword">if</span> (session != <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="comment">// 将用户信息注入到请求上下文，后续Controller可以直接用</span></span><br><span class="line">                request.getSession().setAttribute(<span class="string">&quot;userId&quot;</span>, </span><br><span class="line">                    Long.valueOf(session.get(<span class="string">&quot;userId&quot;</span>).toString()));</span><br><span class="line">                request.getSession().setAttribute(<span class="string">&quot;role&quot;</span>, session.get(<span class="string">&quot;role&quot;</span>).toString());</span><br><span class="line">                request.getSession().setAttribute(<span class="string">&quot;tableName&quot;</span>, session.get(<span class="string">&quot;tableName&quot;</span>).toString());</span><br><span class="line">                request.getSession().setAttribute(<span class="string">&quot;username&quot;</span>, session.get(<span class="string">&quot;username&quot;</span>).toString());</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 校验失败 → 401</span></span><br><span class="line">        response.setCharacterEncoding(<span class="string">&quot;UTF-8&quot;</span>);</span><br><span class="line">        response.setContentType(<span class="string">&quot;application/json; charset=utf-8&quot;</span>);</span><br><span class="line">        response.getWriter().print(JSONObject.toJSONString(R.error(<span class="number">401</span>, <span class="string">&quot;请先登录&quot;</span>)));</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-2-为什么需要双重校验？"><a href="#4-2-为什么需要双重校验？" class="headerlink" title="4.2 为什么需要双重校验？"></a>4.2 为什么需要双重校验？</h3><p>JWT 本身是无状态的，签发后无法主动失效。配合 Redis 后：</p><ul><li><strong>第一重（JWT验证）</strong>：检查签名是否被篡改、Token 是否过期、IP 是否匹配</li><li><strong>第二重（Redis校验）</strong>：检查 Token 是否在 Redis 白名单中</li></ul><p>退出登录时删除 Redis 中的 Token → 即使 JWT 本身没过期，第二重校验也会失败 → Token 立即作废。</p><h3 id="4-3-拦截器注册"><a href="#4-3-拦截器注册" class="headerlink" title="4.3 拦截器注册"></a>4.3 拦截器注册</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">InterceptorConfig</span> <span class="keyword">extends</span> <span class="title class_">WebMvcConfigurationSupport</span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> AuthorizationInterceptor <span class="title function_">getAuthorizationInterceptor</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">AuthorizationInterceptor</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> &#123;</span><br><span class="line">        registry.addInterceptor(getAuthorizationInterceptor())</span><br><span class="line">                .addPathPatterns(<span class="string">&quot;/**&quot;</span>)  <span class="comment">// 拦截所有路径</span></span><br><span class="line">                .excludePathPatterns(<span class="string">&quot;/static/**&quot;</span>)</span><br><span class="line">                <span class="comment">// ★ 排除Swagger文档路径，否则文档页面也要登录</span></span><br><span class="line">                .excludePathPatterns(</span><br><span class="line">                    <span class="string">&quot;/swagger-ui/**&quot;</span>, <span class="string">&quot;/swagger-ui.html&quot;</span>, </span><br><span class="line">                    <span class="string">&quot;/v3/api-docs/**&quot;</span>, <span class="string">&quot;/swagger-resources/**&quot;</span>, </span><br><span class="line">                    <span class="string">&quot;/webjars/**&quot;</span></span><br><span class="line">                )</span><br><span class="line">                .excludePathPatterns(<span class="string">&quot;/actuator/**&quot;</span>);  <span class="comment">// 监控端点放行</span></span><br><span class="line">        <span class="built_in">super</span>.addInterceptors(registry);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="第五章：登录与退出完整流程"><a href="#第五章：登录与退出完整流程" class="headerlink" title="第五章：登录与退出完整流程"></a>第五章：登录与退出完整流程</h2><h3 id="5-1-登录流程"><a href="#5-1-登录流程" class="headerlink" title="5.1 登录流程"></a>5.1 登录流程</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@IgnoreAuth</span>  <span class="comment">// 登录接口免鉴权</span></span><br><span class="line"><span class="meta">@RateLimit(key = &quot;login:users&quot;, count = 5, period = 60, </span></span><br><span class="line"><span class="meta">           message = &quot;登录请求过于频繁，请1分钟后再试&quot;)</span></span><br><span class="line"><span class="meta">@PostMapping(value = &quot;/login&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">login</span><span class="params">(<span class="meta">@RequestBody</span> Map&lt;String, String&gt; params, HttpServletRequest request)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> params.get(<span class="string">&quot;username&quot;</span>);</span><br><span class="line">    <span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> params.get(<span class="string">&quot;password&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1. 查用户</span></span><br><span class="line">    <span class="type">UserEntity</span> <span class="variable">user</span> <span class="operator">=</span> userService.getOne(</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">QueryWrapper</span>&lt;UserEntity&gt;().eq(<span class="string">&quot;username&quot;</span>, username));</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. BCrypt 密码验证</span></span><br><span class="line">    <span class="keyword">if</span>(user == <span class="literal">null</span> || !PasswordEncoder.matches(password, user.getPassword())) &#123;</span><br><span class="line">        <span class="keyword">return</span> R.error(<span class="string">&quot;账号或密码不正确&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3. 获取客户端IP</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">clientIp</span> <span class="operator">=</span> JwtUtils.getClientIp(request);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 4. 生成 Token（写入IP）</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> jwtUtils.generateToken(</span><br><span class="line">        user.getId(), username, <span class="string">&quot;users&quot;</span>, user.getRole(), clientIp);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 5. 存入 Redis 白名单</span></span><br><span class="line">    redisTokenService.saveToken(token, user.getId(), username, <span class="string">&quot;users&quot;</span>, user.getRole());</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> R.ok().put(<span class="string">&quot;token&quot;</span>, token);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-2-退出登录——主动失效"><a href="#5-2-退出登录——主动失效" class="headerlink" title="5.2 退出登录——主动失效"></a>5.2 退出登录——主动失效</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping(&quot;/logout&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">logout</span><span class="params">(HttpServletRequest request)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;Token&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (token != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="comment">// ★ 从 Redis 删除 Token → 立即失效</span></span><br><span class="line">        <span class="comment">// 即使 JWT 本身还没过期（1小时TTL），拦截器第二重校验也会拒绝</span></span><br><span class="line">        redisTokenService.removeToken(token);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> R.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="第六章：Token-自动续期——Renew-Token-方案"><a href="#第六章：Token-自动续期——Renew-Token-方案" class="headerlink" title="第六章：Token 自动续期——Renew-Token 方案"></a>第六章：Token 自动续期——Renew-Token 方案</h2><h3 id="6-1-方案原理"><a href="#6-1-方案原理" class="headerlink" title="6.1 方案原理"></a>6.1 方案原理</h3><p>在 springboot-core-arch 项目中，Token 续期用了另一种方案——通过响应头自动返回新 Token：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">请求到达</span><br><span class="line">    │</span><br><span class="line">    ▼ JwtAuthenticationFilter</span><br><span class="line">    │ 1. 验证Token签名</span><br><span class="line">    │ 2. 解析过期时间</span><br><span class="line">    │ 3. 判断：剩余有效期 &lt; 30分钟？</span><br><span class="line">    │    ├── 否 → 正常放行</span><br><span class="line">    │    └── 是 → 生成新Token（24小时）</span><br><span class="line">    │           放入响应头 Renew-Token</span><br><span class="line">    │</span><br><span class="line">    ▼ 响应返回</span><br><span class="line">    │</span><br><span class="line">客户端</span><br><span class="line">    │ 检查响应头有没有 Renew-Token</span><br><span class="line">    │ ├── 有 → 替换本地Token</span><br><span class="line">    │ └── 无 → 继续用旧Token</span><br></pre></td></tr></table></figure><h3 id="6-2-过滤器实现"><a href="#6-2-过滤器实现" class="headerlink" title="6.2 过滤器实现"></a>6.2 过滤器实现</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtAuthenticationFilter</span> <span class="keyword">extends</span> <span class="title class_">OncePerRequestFilter</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doFilterInternal</span><span class="params">(HttpServletRequest request,</span></span><br><span class="line"><span class="params">                                    HttpServletResponse response,</span></span><br><span class="line"><span class="params">                                    FilterChain filterChain)</span> <span class="keyword">throws</span> ServletException, IOException &#123;</span><br><span class="line">        <span class="comment">// 白名单放行</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">uri</span> <span class="operator">=</span> request.getRequestURI();</span><br><span class="line">        <span class="keyword">if</span> (isWhiteListed(uri)) &#123;</span><br><span class="line">            filterChain.doFilter(request, response);</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 提取 Token</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;token&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(token)) &#123;</span><br><span class="line">            token = request.getParameter(<span class="string">&quot;token&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(token)) &#123;</span><br><span class="line">            filterChain.doFilter(request, response);</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 解析 Token：audience = &quot;userId-role&quot;</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">audience</span> <span class="operator">=</span> JWT.decode(token).getAudience().get(<span class="number">0</span>);</span><br><span class="line">        String[] split = audience.split(<span class="string">&quot;-&quot;</span>);</span><br><span class="line">        <span class="type">String</span> <span class="variable">userId</span> <span class="operator">=</span> split[<span class="number">0</span>];</span><br><span class="line">        <span class="type">String</span> <span class="variable">role</span> <span class="operator">=</span> split[<span class="number">1</span>];</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ★ 密码做密钥：用用户密码验证JWT签名</span></span><br><span class="line">        <span class="type">AccountService</span> <span class="variable">service</span> <span class="operator">=</span> serviceMap.get(role);</span><br><span class="line">        <span class="type">Account</span> <span class="variable">account</span> <span class="operator">=</span> service.selectById(userId);</span><br><span class="line">        <span class="type">JWTVerifier</span> <span class="variable">verifier</span> <span class="operator">=</span> JWT.require(Algorithm.HMAC256(account.getPassword())).build();</span><br><span class="line">        verifier.verify(token);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置 Security 上下文</span></span><br><span class="line">        <span class="type">UsernamePasswordAuthenticationToken</span> <span class="variable">authToken</span> <span class="operator">=</span></span><br><span class="line">                <span class="keyword">new</span> <span class="title class_">UsernamePasswordAuthenticationToken</span>(</span><br><span class="line">                    account.getUsername(), <span class="literal">null</span>, Collections.emptyList());</span><br><span class="line">        SecurityContextHolder.getContext().setAuthentication(authToken);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// ★ 自动续期逻辑</span></span><br><span class="line">        <span class="comment">// 如果token剩余有效期少于30分钟，则自动续签24小时</span></span><br><span class="line">        <span class="keyword">if</span> (shouldRenewToken(token, <span class="number">30</span> * <span class="number">60</span> * <span class="number">1000</span>)) &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">newToken</span> <span class="operator">=</span> createToken(</span><br><span class="line">                userId + <span class="string">&quot;-&quot;</span> + role, </span><br><span class="line">                account.getPassword(), </span><br><span class="line">                <span class="number">24</span>  <span class="comment">// 新Token有效期24小时</span></span><br><span class="line">            );</span><br><span class="line">            response.setHeader(<span class="string">&quot;Renew-Token&quot;</span>, newToken);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        filterChain.doFilter(request, response);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 判断Token是否即将过期</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> threshold 续签阈值（毫秒），30分钟 = 30 * 60 * 1000</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">shouldRenewToken</span><span class="params">(String token, <span class="type">long</span> threshold)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">Date</span> <span class="variable">expiresAt</span> <span class="operator">=</span> JWT.decode(token).getExpiresAt();</span><br><span class="line">            <span class="keyword">return</span> expiresAt != <span class="literal">null</span> </span><br><span class="line">                &amp;&amp; (expiresAt.getTime() - System.currentTimeMillis() &lt; threshold);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">createToken</span><span class="params">(String audience, String secret, <span class="type">int</span> hours)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> JWT.create()</span><br><span class="line">                .withAudience(audience)</span><br><span class="line">                .withExpiresAt(DateUtil.offsetHour(<span class="keyword">new</span> <span class="title class_">Date</span>(), hours))</span><br><span class="line">                .sign(Algorithm.HMAC256(secret));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-3-前端处理"><a href="#6-3-前端处理" class="headerlink" title="6.3 前端处理"></a>6.3 前端处理</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">&#x27;axios&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> service = axios.<span class="title function_">create</span>(&#123;</span><br><span class="line">  <span class="attr">baseURL</span>: <span class="string">&#x27;/api&#x27;</span>,</span><br><span class="line">  <span class="attr">timeout</span>: <span class="number">10000</span></span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// ★ 响应拦截器：检查 Renew-Token（只需5行代码）</span></span><br><span class="line">service.<span class="property">interceptors</span>.<span class="property">response</span>.<span class="title function_">use</span>(</span><br><span class="line">  <span class="function"><span class="params">response</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 检查是否有续期Token</span></span><br><span class="line">    <span class="keyword">const</span> renewToken = response.<span class="property">headers</span>[<span class="string">&#x27;renew-token&#x27;</span>]</span><br><span class="line">    <span class="keyword">if</span> (renewToken) &#123;</span><br><span class="line">      <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(<span class="string">&#x27;token&#x27;</span>, renewToken)</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Token已自动续期&#x27;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> response.<span class="property">data</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="function"><span class="params">error</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (error.<span class="property">response</span> &amp;&amp; error.<span class="property">response</span>.<span class="property">status</span> === <span class="number">401</span>) &#123;</span><br><span class="line">      <span class="variable language_">localStorage</span>.<span class="title function_">removeItem</span>(<span class="string">&#x27;token&#x27;</span>)</span><br><span class="line">      <span class="variable language_">window</span>.<span class="property">location</span>.<span class="property">href</span> = <span class="string">&#x27;/login&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">reject</span>(error)</span><br><span class="line">  &#125;</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> service</span><br></pre></td></tr></table></figure><p><strong>前端只需 5 行代码</strong>：检查响应头 → 替换本地 Token。不需要写刷新接口、不需要处理并发刷新、不需要管理 RefreshToken。</p><h3 id="6-4-续期时机分析"><a href="#6-4-续期时机分析" class="headerlink" title="6.4 续期时机分析"></a>6.4 续期时机分析</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Token有效期: 24小时</span><br><span class="line">续期阈值: 30分钟（剩余 &lt; 30分钟时触发）</span><br><span class="line"></span><br><span class="line">时间轴：</span><br><span class="line">0h        23h        23.5h           24h</span><br><span class="line">│----------│-----------│--------------│</span><br><span class="line">│  正常     │ 触发续期   │ 新Token 24h   │ 旧Token过期</span><br><span class="line">│  使用     │           │              │ (已被替换)</span><br></pre></td></tr></table></figure><ul><li>0-23h：Token 有效期充足，不续期</li><li>23-24h：剩余 &lt; 30 分钟，每次请求都返回新 Token</li><li>新 Token 有效期从当前时刻开始算 24 小时</li></ul><p><strong>效果</strong>：用户持续操作期间永远不会掉线。只有连续 24 小时不操作才会过期。</p><hr><h2 id="第七章：密码做密钥——改密自动失效"><a href="#第七章：密码做密钥——改密自动失效" class="headerlink" title="第七章：密码做密钥——改密自动失效"></a>第七章：密码做密钥——改密自动失效</h2><h3 id="7-1-设计原理"><a href="#7-1-设计原理" class="headerlink" title="7.1 设计原理"></a>7.1 设计原理</h3><p>在 springboot-core-arch 和钢铁工厂微服务中，JWT 的签名密钥不是固定的全局密钥，而是<strong>用户密码的 BCrypt 哈希值</strong>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 生成Token时，用用户密码作为HMAC256密钥</span></span><br><span class="line"><span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> TokenUtils.createToken(</span><br><span class="line">    userId + <span class="string">&quot;-&quot;</span> + role,    <span class="comment">// audience</span></span><br><span class="line">    dbAdmin.getPassword(),   <span class="comment">// ★ 密码作为签名密钥</span></span><br><span class="line">    clientIp</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p><strong>验证时</strong>：从数据库查出用户密码，用密码验证 Token 签名：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 从Redis获取密码（用于验证JWT签名）</span></span><br><span class="line"><span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> (String) redisTemplate.opsForValue()</span><br><span class="line">    .get(<span class="string">&quot;token:password:&quot;</span> + audience);</span><br><span class="line"></span><br><span class="line"><span class="comment">// ★ 用密码验证签名</span></span><br><span class="line"><span class="type">JWTVerifier</span> <span class="variable">verifier</span> <span class="operator">=</span> JWT.require(Algorithm.HMAC256(password)).build();</span><br><span class="line">verifier.verify(token);</span><br></pre></td></tr></table></figure><h3 id="7-2-改密自动失效原理"><a href="#7-2-改密自动失效原理" class="headerlink" title="7.2 改密自动失效原理"></a>7.2 改密自动失效原理</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">用户改密码：</span><br><span class="line">    旧密码: $2a$10$abc...  ← 旧Token的签名密钥</span><br><span class="line">    新密码: $2a$10$xyz...  ← 数据库中的密码已更新</span><br><span class="line">    </span><br><span class="line">旧Token到达 → 查数据库获取密码 → 用新密码验证旧Token签名 → ✗ 验证失败！</span><br><span class="line">→ 旧Token自动失效，无需操作Redis</span><br></pre></td></tr></table></figure><p><strong>优势</strong>：改密码后旧 Token 自动失效，不需要额外操作 Redis。</p><h3 id="7-3-网关层的密码验证"><a href="#7-3-网关层的密码验证" class="headerlink" title="7.3 网关层的密码验证"></a>7.3 网关层的密码验证</h3><p>在钢铁工厂微服务中，网关需要验证 Token 但无法直接查用户数据库——密码存储在 Redis 中：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 用户服务登录时，把密码存入Redis</span></span><br><span class="line"><span class="type">String</span> <span class="variable">passwordKey</span> <span class="operator">=</span> <span class="string">&quot;token:password:&quot;</span> + data;  <span class="comment">// data = &quot;userId-role&quot;</span></span><br><span class="line">staticRedisTemplate.opsForValue().set(passwordKey, sign, <span class="number">2</span>, TimeUnit.HOURS);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 网关验证时，从Redis获取密码</span></span><br><span class="line"><span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> (String) redisTemplate.opsForValue()</span><br><span class="line">    .get(<span class="string">&quot;token:password:&quot;</span> + audience);</span><br><span class="line"><span class="keyword">if</span> (password == <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> unauthorized(exchange);  <span class="comment">// Token已过期或用户已改密</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用密码验证JWT签名</span></span><br><span class="line"><span class="type">JWTVerifier</span> <span class="variable">verifier</span> <span class="operator">=</span> JWT.require(Algorithm.HMAC256(password)).build();</span><br><span class="line">verifier.verify(token);</span><br></pre></td></tr></table></figure><hr><h2 id="第八章：双-Token-机制"><a href="#第八章：双-Token-机制" class="headerlink" title="第八章：双 Token 机制"></a>第八章：双 Token 机制</h2><h3 id="8-1-AccessToken-RefreshToken"><a href="#8-1-AccessToken-RefreshToken" class="headerlink" title="8.1 AccessToken + RefreshToken"></a>8.1 AccessToken + RefreshToken</h3><p>在 IntensifiedMyCode 项目中，认证用了双 Token 方案：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TokenUtils</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 生成 AccessToken（短期，30分钟）</span></span><br><span class="line"><span class="comment">     * 携带在每次请求中，用于鉴权</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">createAccessToken</span><span class="params">(String userId, String role)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">audience</span> <span class="operator">=</span> userId + <span class="string">&quot;-&quot;</span> + role + <span class="string">&quot;-access&quot;</span>;</span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> JWT.create()</span><br><span class="line">                .withAudience(audience)</span><br><span class="line">                .withExpiresAt(DateUtil.offsetMinute(<span class="keyword">new</span> <span class="title class_">Date</span>(), <span class="number">30</span>))</span><br><span class="line">                .sign(Algorithm.HMAC256(jwtSecret));</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 存入Redis白名单</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">tokenKey</span> <span class="operator">=</span> <span class="string">&quot;token:access:&quot;</span> + userId + <span class="string">&quot;:&quot;</span> + role;</span><br><span class="line">        redisUtils.set(tokenKey, token, <span class="number">30</span>, TimeUnit.MINUTES);</span><br><span class="line">        <span class="keyword">return</span> token;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 生成 RefreshToken（长期，7天）</span></span><br><span class="line"><span class="comment">     * AccessToken过期后用它换新的</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">createRefreshToken</span><span class="params">(String userId, String role)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">audience</span> <span class="operator">=</span> userId + <span class="string">&quot;-&quot;</span> + role + <span class="string">&quot;-refresh&quot;</span>;</span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> JWT.create()</span><br><span class="line">                .withAudience(audience)</span><br><span class="line">                .withExpiresAt(DateUtil.offsetDay(<span class="keyword">new</span> <span class="title class_">Date</span>(), <span class="number">7</span>))</span><br><span class="line">                .sign(Algorithm.HMAC256(jwtSecret));</span><br><span class="line">        </span><br><span class="line">        <span class="type">String</span> <span class="variable">tokenKey</span> <span class="operator">=</span> <span class="string">&quot;token:refresh:&quot;</span> + userId + <span class="string">&quot;:&quot;</span> + role;</span><br><span class="line">        redisUtils.set(tokenKey, token, <span class="number">7</span>, TimeUnit.DAYS);</span><br><span class="line">        <span class="keyword">return</span> token;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-2-Token-刷新流程"><a href="#8-2-Token-刷新流程" class="headerlink" title="8.2 Token 刷新流程"></a>8.2 Token 刷新流程</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping(&quot;/refresh&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">refresh</span><span class="params">(<span class="meta">@RequestBody</span> Account account)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">userId</span> <span class="operator">=</span> account.getId().toString();</span><br><span class="line">    <span class="type">String</span> <span class="variable">role</span> <span class="operator">=</span> account.getRole();</span><br><span class="line">    <span class="type">String</span> <span class="variable">oldRefreshToken</span> <span class="operator">=</span> account.getRefreshToken();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 验证RefreshToken有效性</span></span><br><span class="line">    <span class="keyword">if</span> (!tokenUtils.isRefreshTokenValid(userId, role, oldRefreshToken)) &#123;</span><br><span class="line">        <span class="keyword">return</span> R.error(<span class="string">&quot;RefreshToken已失效，请重新登录&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 删除旧Token对</span></span><br><span class="line">    tokenUtils.removeTokens(userId, role);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 生成新Token对</span></span><br><span class="line">    Map&lt;String, String&gt; newTokens = tokenUtils.createTokens(userId, role);</span><br><span class="line">    <span class="keyword">return</span> R.success(newTokens);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-3-双-Token-的优势"><a href="#8-3-双-Token-的优势" class="headerlink" title="8.3 双 Token 的优势"></a>8.3 双 Token 的优势</h3><table><thead><tr><th>Token</th><th>有效期</th><th>用途</th><th>存储</th></tr></thead><tbody><tr><td>AccessToken</td><td>30分钟</td><td>携带在每次请求中，用于鉴权</td><td>Redis白名单</td></tr><tr><td>RefreshToken</td><td>7天</td><td>AccessToken过期后用它换新</td><td>Redis白名单</td></tr></tbody></table><p>AccessToken 短期有效 → 被盗风险窗口小；RefreshToken 长期有效 → 用户不用频繁登录。</p><hr><h2 id="第九章：网关统一鉴权-用户信息透传"><a href="#第九章：网关统一鉴权-用户信息透传" class="headerlink" title="第九章：网关统一鉴权 + 用户信息透传"></a>第九章：网关统一鉴权 + 用户信息透传</h2><h3 id="9-1-微服务网关鉴权"><a href="#9-1-微服务网关鉴权" class="headerlink" title="9.1 微服务网关鉴权"></a>9.1 微服务网关鉴权</h3><p>在微服务架构中，每个服务都做鉴权太重复——网关统一鉴权后，通过 Header 传递用户信息给下游：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AuthGlobalFilter</span> <span class="keyword">implements</span> <span class="title class_">GlobalFilter</span>, Ordered &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Void&gt; <span class="title function_">filter</span><span class="params">(ServerWebExchange exchange, GatewayFilterChain chain)</span> &#123;</span><br><span class="line">        <span class="type">ServerHttpRequest</span> <span class="variable">request</span> <span class="operator">=</span> exchange.getRequest();</span><br><span class="line">        <span class="type">String</span> <span class="variable">path</span> <span class="operator">=</span> request.getURI().getPath();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 1. 白名单检查</span></span><br><span class="line">        <span class="keyword">if</span> (isWhiteListed(path)) &#123;</span><br><span class="line">            <span class="keyword">return</span> chain.filter(exchange);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2. 提取Token（Header或URL参数）</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeaders().getFirst(<span class="string">&quot;token&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(token)) &#123;</span><br><span class="line">            token = request.getQueryParams().getFirst(<span class="string">&quot;token&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(token)) &#123;</span><br><span class="line">            <span class="keyword">return</span> unauthorized(exchange);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3. 解析Token</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">audience</span> <span class="operator">=</span> JWT.decode(token).getAudience().get(<span class="number">0</span>);</span><br><span class="line">        String[] split = audience.split(<span class="string">&quot;-&quot;</span>);</span><br><span class="line">        <span class="type">String</span> <span class="variable">userId</span> <span class="operator">=</span> split[<span class="number">0</span>];</span><br><span class="line">        <span class="type">String</span> <span class="variable">role</span> <span class="operator">=</span> split[<span class="number">1</span>];</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 4. 从Redis获取密码验证签名</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> (String) redisTemplate.opsForValue()</span><br><span class="line">                .get(<span class="string">&quot;token:password:&quot;</span> + audience);</span><br><span class="line">        <span class="keyword">if</span> (password == <span class="literal">null</span>) <span class="keyword">return</span> unauthorized(exchange);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 5. 验证JWT签名</span></span><br><span class="line">        <span class="type">JWTVerifier</span> <span class="variable">verifier</span> <span class="operator">=</span> JWT.require(Algorithm.HMAC256(password)).build();</span><br><span class="line">        verifier.verify(token);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 6. IP绑定验证</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">clientIp</span> <span class="operator">=</span> getClientIp(request);</span><br><span class="line">        <span class="type">String</span> <span class="variable">storedIp</span> <span class="operator">=</span> (String) redisTemplate.opsForValue().get(<span class="string">&quot;token:&quot;</span> + audience);</span><br><span class="line">        <span class="keyword">if</span> (storedIp == <span class="literal">null</span> || !clientIp.equals(storedIp)) &#123;</span><br><span class="line">            <span class="keyword">return</span> unauthorized(exchange);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 7. ★ 用户信息透传给下游微服务</span></span><br><span class="line">        <span class="type">ServerHttpRequest</span> <span class="variable">mutatedRequest</span> <span class="operator">=</span> request.mutate()</span><br><span class="line">                .header(<span class="string">&quot;X-User-Id&quot;</span>, userId)</span><br><span class="line">                .header(<span class="string">&quot;X-User-Role&quot;</span>, role)</span><br><span class="line">                .build();</span><br><span class="line">        <span class="keyword">return</span> chain.filter(exchange.mutate().request(mutatedRequest).build());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">getOrder</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> -<span class="number">1</span>;  <span class="comment">// 高优先级</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="9-2-用户信息透传原理"><a href="#9-2-用户信息透传原理" class="headerlink" title="9.2 用户信息透传原理"></a>9.2 用户信息透传原理</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">客户端                    网关                        下游服务</span><br><span class="line">  │                        │                             │</span><br><span class="line">  │── 请求(Token) ────────&gt;│                             │</span><br><span class="line">  │                        │ 解析Token获取userId/role     │</span><br><span class="line">  │                        │ 添加 X-User-Id Header ──────&gt;│</span><br><span class="line">  │                        │ 添加 X-User-Role Header ─────&gt;│</span><br><span class="line">  │                        │                             │ 下游服务通过request.getHeader(&quot;X-User-Id&quot;)获取</span><br><span class="line">  │&lt;── 响应 ──────────────│&lt;─────────────────────────────│</span><br></pre></td></tr></table></figure><p>下游服务不需要再次解析 JWT——网关统一鉴权后，通过自定义 Header 传递用户信息。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 下游服务获取用户ID</span></span><br><span class="line"><span class="meta">@GetMapping(&quot;/profile&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">getProfile</span><span class="params">(HttpServletRequest request)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">userId</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;X-User-Id&quot;</span>);  <span class="comment">// 从网关注入的Header获取</span></span><br><span class="line">    <span class="keyword">return</span> R.success(userService.getById(userId));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="第十章：4-种方案横向对比"><a href="#第十章：4-种方案横向对比" class="headerlink" title="第十章：4 种方案横向对比"></a>第十章：4 种方案横向对比</h2><h3 id="10-1-方案概览"><a href="#10-1-方案概览" class="headerlink" title="10.1 方案概览"></a>10.1 方案概览</h3><table><thead><tr><th>方案</th><th>项目</th><th>核心特点</th><th>Token密钥</th><th>主动失效</th></tr></thead><tbody><tr><td>A</td><td>community-group-buying-system</td><td>JWT + IP绑定 + Redis白名单</td><td>固定密钥</td><td>Redis删除Token</td></tr><tr><td>B</td><td>IntensifiedMyCode</td><td>双Token + Redis白名单 + 策略模式</td><td>固定密钥</td><td>Redis删除Token</td></tr><tr><td>C</td><td>springboot-core-arch</td><td>密码做密钥 + Renew-Token续期</td><td>用户密码</td><td>改密自动失效</td></tr><tr><td>D</td><td>steel-pipe-factory-microservices</td><td>密码做密钥 + IP绑定 + 网关鉴权</td><td>用户密码</td><td>Redis删除Token</td></tr></tbody></table><h3 id="10-2-对比表"><a href="#10-2-对比表" class="headerlink" title="10.2 对比表"></a>10.2 对比表</h3><table><thead><tr><th>维度</th><th>方案A</th><th>方案B</th><th>方案C</th><th>方案D</th></tr></thead><tbody><tr><td>密钥来源</td><td>固定</td><td>固定</td><td>用户密码</td><td>用户密码</td></tr><tr><td>IP绑定</td><td>✅</td><td>❌</td><td>❌</td><td>✅</td></tr><tr><td>Token数</td><td>1</td><td>2</td><td>1</td><td>1</td></tr><tr><td>续期方式</td><td>Redis滑动</td><td>RefreshToken</td><td>Renew-Token</td><td>无</td></tr><tr><td>改密失效</td><td>需删Redis</td><td>需删Redis</td><td>自动</td><td>自动</td></tr><tr><td>多设备</td><td>共享Token</td><td>共享Token</td><td>独立Token</td><td>独立Token</td></tr><tr><td>Redis依赖</td><td>强</td><td>强</td><td>弱</td><td>强</td></tr><tr><td>前端改动</td><td>无</td><td>大（刷新逻辑）</td><td>小（拦截响应头）</td><td>无</td></tr><tr><td>适合架构</td><td>单体</td><td>单体</td><td>单体&#x2F;微服务</td><td>微服务</td></tr><tr><td>安全性</td><td>中高</td><td>中高</td><td>高</td><td>高</td></tr></tbody></table><h3 id="10-3-选择建议"><a href="#10-3-选择建议" class="headerlink" title="10.3 选择建议"></a>10.3 选择建议</h3><table><thead><tr><th>场景</th><th>推荐方案</th><th>原因</th></tr></thead><tbody><tr><td>单体应用，Web端为主</td><td>方案A</td><td>IP绑定防盗用，Redis白名单支持主动失效</td></tr><tr><td>需要最高安全级别</td><td>方案D</td><td>三重保护（密码密钥+IP+Redis）</td></tr><tr><td>移动端为主，IP频繁变化</td><td>方案B</td><td>不绑IP，双Token兼顾安全和体验</td></tr><tr><td>微服务架构</td><td>方案C + 网关</td><td>Renew-Token透明续期，网关统一鉴权</td></tr></tbody></table><hr><h2 id="第十一章：IP-绑定的局限性与解决方案"><a href="#第十一章：IP-绑定的局限性与解决方案" class="headerlink" title="第十一章：IP 绑定的局限性与解决方案"></a>第十一章：IP 绑定的局限性与解决方案</h2><h3 id="11-1-移动端-IP-频繁变化"><a href="#11-1-移动端-IP-频繁变化" class="headerlink" title="11.1 移动端 IP 频繁变化"></a>11.1 移动端 IP 频繁变化</h3><p>手机从 WiFi 切到 4G&#x2F;5G，IP 会变化，导致 Token 失效：</p><table><thead><tr><th>方案</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>IP 绑定（严格）</td><td>安全性最高</td><td>移动端体验差</td></tr><tr><td>IP 段绑定（C 段）</td><td>兼容IP微调</td><td>安全性降低</td></tr><tr><td>设备指纹绑定</td><td>不受IP变化影响</td><td>实现复杂</td></tr><tr><td>仅Redis白名单</td><td>不受IP影响</td><td>Token被盗可继续使用</td></tr></tbody></table><h3 id="11-2-推荐方案：按场景选择"><a href="#11-2-推荐方案：按场景选择" class="headerlink" title="11.2 推荐方案：按场景选择"></a>11.2 推荐方案：按场景选择</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Web端：严格IP绑定</span></span><br><span class="line"><span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> jwtUtils.generateToken(userId, username, <span class="string">&quot;users&quot;</span>, role, clientIp);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 移动端：仅Redis白名单，不绑IP</span></span><br><span class="line"><span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> jwtUtils.generateToken(userId, username, <span class="string">&quot;yonghu&quot;</span>, role, <span class="literal">null</span>);</span><br><span class="line"><span class="comment">// validateToken 中 ip == null 时跳过IP校验</span></span><br></pre></td></tr></table></figure><p><code>validateToken</code> 中的判断 <code>tokenIp != null &amp;&amp; !tokenIp.isEmpty()</code> 已经做了兼容——如果生成时没写入 IP，验证时自动跳过 IP 校验。</p><hr><h2 id="第十二章：登出与黑名单"><a href="#第十二章：登出与黑名单" class="headerlink" title="第十二章：登出与黑名单"></a>第十二章：登出与黑名单</h2><h3 id="12-1-方案A-B的登出（Redis白名单删除）"><a href="#12-1-方案A-B的登出（Redis白名单删除）" class="headerlink" title="12.1 方案A&#x2F;B的登出（Redis白名单删除）"></a>12.1 方案A&#x2F;B的登出（Redis白名单删除）</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping(&quot;/logout&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R <span class="title function_">logout</span><span class="params">(HttpServletRequest request)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;Token&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (token != <span class="literal">null</span>) &#123;</span><br><span class="line">        redisTokenService.removeToken(token);  <span class="comment">// 删除Redis中的Token</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> R.ok();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>退出登录后，即使 JWT 本身还没过期（1小时TTL），Redis 中已不存在该 Token，下次请求会被拦截器第二重校验拒绝。</p><h3 id="12-2-方案C的登出（Redis黑名单）"><a href="#12-2-方案C的登出（Redis黑名单）" class="headerlink" title="12.2 方案C的登出（Redis黑名单）"></a>12.2 方案C的登出（Redis黑名单）</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping(&quot;/logout&quot;)</span></span><br><span class="line"><span class="keyword">public</span> R&lt;?&gt; logout(HttpServletRequest request) &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> JwtUtil.getTokenFromRequest(request);</span><br><span class="line">    <span class="keyword">if</span> (token != <span class="literal">null</span> &amp;&amp; JwtUtil.isTokenValid(token)) &#123;</span><br><span class="line">        <span class="comment">// 将Token加入Redis黑名单（剩余有效期内）</span></span><br><span class="line">        <span class="type">long</span> <span class="variable">expire</span> <span class="operator">=</span> JwtUtil.getRemainingTime(token);</span><br><span class="line">        redisUtil.set(<span class="string">&quot;blacklist:&quot;</span> + token, <span class="string">&quot;true&quot;</span>, expire, TimeUnit.MILLISECONDS);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> R.ok(<span class="string">&quot;登出成功&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>黑名单 vs 白名单</strong>：</p><ul><li>黑名单：存废弃的 Token（过期自动清除）</li><li>白名单：存有效的 Token（删除即失效）</li></ul><p>Renew-Token 方案配合黑名单更合适——登出时把旧 Token 加入黑名单，但 Renew 出来的新 Token 不受影响。</p><hr><h2 id="第十三章：BCrypt-密码加密"><a href="#第十三章：BCrypt-密码加密" class="headerlink" title="第十三章：BCrypt 密码加密"></a>第十三章：BCrypt 密码加密</h2><h3 id="13-1-PasswordEncoder-封装"><a href="#13-1-PasswordEncoder-封装" class="headerlink" title="13.1 PasswordEncoder 封装"></a>13.1 PasswordEncoder 封装</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PasswordEncoder</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">BCryptPasswordEncoder</span> <span class="variable">ENCODER</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BCryptPasswordEncoder</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">encode</span><span class="params">(String rawPassword)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> ENCODER.encode(rawPassword);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">matches</span><span class="params">(String rawPassword, String encodedPassword)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (rawPassword == <span class="literal">null</span> || encodedPassword == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> ENCODER.matches(rawPassword, encodedPassword);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="13-2-BCrypt-为什么安全"><a href="#13-2-BCrypt-为什么安全" class="headerlink" title="13.2 BCrypt 为什么安全"></a>13.2 BCrypt 为什么安全</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">明文: &quot;123456&quot;</span><br><span class="line">加密1: $2a$10$N9qo8uLOickgx2ZMRZoMy...  ← 不同盐值</span><br><span class="line">加密2: $2a$10$X3P5vQ7rS1tW8yK0zBmN...  ← 不同盐值</span><br></pre></td></tr></table></figure><table><thead><tr><th>特性</th><th>说明</th></tr></thead><tbody><tr><td>自动加盐</td><td>每次加密生成随机盐值，相同明文加密结果不同</td></tr><tr><td>防彩虹表</td><td>盐值嵌入在哈希结果中，预计算的彩虹表无法使用</td></tr><tr><td>可调复杂度</td><td><code>$2a$10$</code> 中的 10 是 cost factor，可调大增加计算时间</td></tr><tr><td>防暴力破解</td><td>单次验证需要 ~100ms，暴力枚举成本极高</td></tr></tbody></table><h3 id="13-3-Spring-Security-的取舍"><a href="#13-3-Spring-Security-的取舍" class="headerlink" title="13.3 Spring Security 的取舍"></a>13.3 Spring Security 的取舍</h3><p>社区团购项目有一个特殊设计——引入了 <code>spring-boot-starter-security</code> 依赖，但排除了 Security 的自动配置：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication(exclude = &#123;</span></span><br><span class="line"><span class="meta">    SecurityAutoConfiguration.class,</span></span><br><span class="line"><span class="meta">    SecurityFilterAutoConfiguration.class</span></span><br><span class="line"><span class="meta">&#125;)</span></span><br></pre></td></tr></table></figure><p><strong>为什么这么做</strong>：只需要 <code>BCryptPasswordEncoder</code> 这个工具类，不需要 Security 的过滤器链、默认登录页、CSRF 保护等。排除自动配置后，Security 不会干扰自定义拦截器。</p><hr><h2 id="第十四章：JWT-配置"><a href="#第十四章：JWT-配置" class="headerlink" title="第十四章：JWT 配置"></a>第十四章：JWT 配置</h2><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># application-dev.yml</span></span><br><span class="line"><span class="attr">jwt:</span></span><br><span class="line">  <span class="attr">secret:</span> <span class="string">CommunityGroupBuying2024SecretKeyForJWTTokenGenerationMustBeLongEnough</span></span><br><span class="line">  <span class="attr">expiration:</span> <span class="number">3600000</span>              <span class="comment"># 1小时（毫秒）</span></span><br><span class="line">  <span class="attr">refresh-expiration:</span> <span class="number">86400000</span>     <span class="comment"># 24小时</span></span><br><span class="line"></span><br><span class="line"><span class="attr">token:</span></span><br><span class="line">  <span class="attr">redis-prefix:</span> <span class="string">&quot;token:&quot;</span></span><br><span class="line">  <span class="attr">expire-seconds:</span> <span class="number">3600</span>             <span class="comment"># Redis中Token的TTL（秒）</span></span><br></pre></td></tr></table></figure><p><strong>密钥长度</strong>：HMAC-SHA256 要求密钥至少 256 位（32 字节）。上面的密钥 72 字节，满足要求。</p><p><strong>多环境</strong>：生产环境密钥应该从环境变量注入，不要硬编码：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># application-prod.yml</span></span><br><span class="line"><span class="attr">jwt:</span></span><br><span class="line">  <span class="attr">secret:</span> <span class="string">$&#123;JWT_SECRET&#125;</span></span><br></pre></td></tr></table></figure><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><table><thead><tr><th>校验层</th><th>防御目标</th><th>实现</th><th>方案A</th><th>方案B</th><th>方案C</th><th>方案D</th></tr></thead><tbody><tr><td>JWT签名</td><td>Token篡改</td><td>HMAC-SHA256</td><td>✅</td><td>✅</td><td>✅</td><td>✅</td></tr><tr><td>IP绑定</td><td>Token盗用</td><td>Claims写入IP</td><td>✅</td><td>❌</td><td>❌</td><td>✅</td></tr><tr><td>Redis白名单</td><td>主动失效</td><td>Hash存储+删除</td><td>✅</td><td>✅</td><td>❌</td><td>✅</td></tr><tr><td>密码做密钥</td><td>改密失效</td><td>HMAC256(密码)</td><td>❌</td><td>❌</td><td>✅</td><td>✅</td></tr><tr><td>Token续期</td><td>体验优化</td><td>Renew-Token</td><td>❌</td><td>RefreshToken</td><td>✅</td><td>❌</td></tr><tr><td>BCrypt</td><td>密码泄露</td><td>自适应哈希</td><td>✅</td><td>✅</td><td>✅</td><td>✅</td></tr><tr><td>限流</td><td>暴力破解</td><td>Lua&#x2F;Bucket4j</td><td>✅</td><td>✅</td><td>❌</td><td>❌</td></tr></tbody></table><p><strong>核心思想</strong>：没有最好的方案，只有最适合场景的方案。选择的关键不是”哪个更先进”，而是”你的场景需要哪个级别的安全”和”你的团队能承受多复杂度的运维”。</p>]]>
    </content>
    <id>http://example.com/2026/07/07/2026070701/</id>
    <link href="http://example.com/2026/07/07/2026070701/"/>
    <published>2026-07-07T02:12:51.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>JWT 是无状态的——签发后无法主动失效。但业务需求是：用户改密码后旧 Token 必须失效、退出登录后 Token 立即作废、Token 被盗用后不能在其他设备使用。怎么解决这些矛盾？本文用 4 个真实项目的代码，把 JWT 安全认证的完整知识体]]>
    </summary>
    <title>JWT 认证安全体系全解：从 IP 绑定到自动续期，4 种方案横评</title>
    <updated>2026-07-07T09:52:36.350Z</updated>
  </entry>
</feed>
