<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on Bruce's Blog</title><link>https://www.huangqixuan.com/post/</link><description>Recent content in Posts on Bruce's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Fri, 28 Aug 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://www.huangqixuan.com/post/index.xml" rel="self" type="application/rss+xml"/><item><title>BNBU 校园 VPN 一键连接：OpenConnect、AnyConnect 与宁盾令牌 TOTP 教程</title><link>https://www.huangqixuan.com/p/research-anyconnect-bnbu/</link><pubDate>Fri, 28 Aug 2026 00:00:00 +0800</pubDate><guid>https://www.huangqixuan.com/p/research-anyconnect-bnbu/</guid><description>&lt;img src="https://www.huangqixuan.com/img/anyconnect/anyconnect-technical-cover.png" alt="Featured image of post BNBU 校园 VPN 一键连接：OpenConnect、AnyConnect 与宁盾令牌 TOTP 教程" /&gt;&lt;p&gt;我做这个项目，没有什么宏大的理由，只是每次连学校 VPN 都觉得麻烦。&lt;/p&gt;&#10;&lt;p&gt;以前打开 Cisco AnyConnect，要输入账号、密码，再拿出手机，打开宁盾令牌看六位验证码。有时宁盾令牌自己还要先输入安全锁密码。一套流程走完并不久，但每次都重复，次数多了就很烦。&lt;/p&gt;&#10;&lt;p&gt;原版 Cisco AnyConnect 还会安装后台服务，并随 Windows 启动。这个设计对企业设备可能很正常，对我却是一直挂着一个平时用不到的服务。我实在忍受不了，才开始找替代客户端。&lt;/p&gt;&#10;&lt;p&gt;最后的效果很简单：打开 OpenConnect-GUI，选中 &lt;code&gt;BNBU VPN&lt;/code&gt;，点击 Connect。账号、静态密码和动态码都由客户端处理，不需要再拿手机。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="最终的一次点击连接界面" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://www.huangqixuan.com/img/anyconnect/openconnect-one-click.png"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;现在连接 BNBU VPN 只剩一次点击。&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;这篇先讲我为什么一路折腾到这里，再给出一套可以照着做的完整流程。教程只适用于你本人有权使用的账号和学校发给你的个人令牌。二维码、seed、Base32 secret 和完整 &lt;code&gt;otpauth://&lt;/code&gt; URI 都是登录凭据，不要发给别人，也不要上传到在线转换网站。&lt;/p&gt;&#10;&lt;h2 id="四篇文章给我的启发"&gt;&lt;a href="#%e5%9b%9b%e7%af%87%e6%96%87%e7%ab%a0%e7%bb%99%e6%88%91%e7%9a%84%e5%90%af%e5%8f%91" class="header-anchor"&gt;&lt;/a&gt;四篇文章给我的启发&#10;&lt;/h2&gt;&lt;p&gt;我不是从零猜出 OpenConnect 和宁盾令牌的用法。真正让我把思路串起来的是四篇博客：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://haotian22.top/546c6c89.html" target="_blank" rel="noopener"&#10; &gt;HaoTian：UIC AnyConnect 的校园 VPN 研究&lt;/a&gt;，写于 2024 年。它从 Clash DNS、路由表、局域网和 ZeroTier 冲突一路研究到 OpenConnect-GUI，让我第一次意识到 Cisco AnyConnect 客户端本身也可以替换。文中的 &lt;code&gt;v.uic.edu.cn&lt;/code&gt; 属于当时的 UIC 环境，当前 BNBU 配置应使用实际可用的 &lt;code&gt;v.bnbu.edu.cn&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://baka.link/zh/posts/dkey-token-to-standard-totp/" target="_blank" rel="noopener"&#10; &gt;Kirin：将 Dkey Token（宁盾令牌）转换为标准 TOTP&lt;/a&gt;，写于 2022 年。它展示了宁盾 App 激活时下发的 &lt;code&gt;seed&lt;/code&gt;、&lt;code&gt;timeStep&lt;/code&gt; 和 &lt;code&gt;passwordLength&lt;/code&gt;，并说明 Hex seed 可以转换成 Base32。&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://blog.steven53.top/p/%E5%AE%81%E7%9B%BE%E4%BB%A4%E7%89%8Cdkey-token%E8%BD%AC%E6%8D%A2%E4%B8%BA%E6%A0%87%E5%87%86totp/" target="_blank" rel="noopener"&#10; &gt;Steven：宁盾令牌（Dkey Token）转换为标准 TOTP&lt;/a&gt;，写于 2023 年。它同时记录了网页响应和二维码两条路线，还解释了 Base64 seed、Hex seed 与 Base32 secret 其实是同一串二进制密钥的不同表示。&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://www.jianyun.run/posts/convert-ningdun-token-to-standard-2fa-totp/" target="_blank" rel="noopener"&#10; &gt;唐长老：宁盾令牌转标准 2FA TOTP&lt;/a&gt;，写于 2020 年。它把二维码里的 token、Hex 到 Base32 的转换和 &lt;code&gt;otpauth://&lt;/code&gt; URI 结构连成了一条完整链路。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;第一篇解决的是“为什么换客户端”和网络协同，后面三篇解决的是“怎样让宁盾验证码自动生成”。它们的学校、年份和客户端环境不完全相同，不能逐项照抄；放在一起之后，OpenConnect 替代、TOTP 参数和 BNBU 实测才组成一条完整路线。&lt;/p&gt;&#10;&lt;h2 id="整个项目是怎样走弯路的"&gt;&lt;a href="#%e6%95%b4%e4%b8%aa%e9%a1%b9%e7%9b%ae%e6%98%af%e6%80%8e%e6%a0%b7%e8%b5%b0%e5%bc%af%e8%b7%af%e7%9a%84" class="header-anchor"&gt;&lt;/a&gt;整个项目是怎样走弯路的&#10;&lt;/h2&gt;&lt;p&gt;我先确认 OpenConnect 能不能代替 Cisco AnyConnect。OpenConnect-GUI 可以连接 BNBU，登录窗口也能正确显示 Student 认证组，但当时仍要输入密码和宁盾验证码。&lt;/p&gt;&#10;&lt;p&gt;接着我试过命令行脚本。脚本能提交账号和静态密码，也能进入：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Please enter a dynamic token&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可它提交动态码后总是 &lt;code&gt;Login failed&lt;/code&gt;，同一时间用 GUI 却能成功。这说明密码不是问题，失败发生在宁盾挑战的交互方式。继续堆脚本只会增加登录失败次数，所以我停掉了命令行路线。&lt;/p&gt;&#10;&lt;p&gt;后来查看 OpenConnect-GUI 的行为，才确认 &lt;code&gt;Batch mode&lt;/code&gt; 可以保存静态密码。这样已经能少输一项，但动态码仍要拿手机看。&lt;/p&gt;&#10;&lt;p&gt;最后卡住的是宁盾令牌。最初拿到的公共激活二维码只有服务器信息，没有个人 token。我一度准备抓宁盾 App 的激活流量。后来从 BNBU 门户重新获取个人手机令牌，才发现新二维码属于另一种结构，里面直接有个人 token，不需要抓包。&lt;/p&gt;&#10;&lt;p&gt;ChatGPT 完成了 OpenConnect-GUI 配置、认证流程排查和 OpenConnect 9.21 升级，但在个人令牌转换这里停了下来。我只把宁盾令牌这一段交给 DeepSeek harness 继续处理。它调用本机离线二维码库读取我自己的二维码，检查字段结构，再完成 Hex 到 Base32 的转换。最终验收仍然是官方宁盾 App 对码和真实 VPN 连接，不是模型说“应该可以”。&lt;/p&gt;&#10;&lt;h2 id="准备工作与风险"&gt;&lt;a href="#%e5%87%86%e5%a4%87%e5%b7%a5%e4%bd%9c%e4%b8%8e%e9%a3%8e%e9%99%a9" class="header-anchor"&gt;&lt;/a&gt;准备工作与风险&#10;&lt;/h2&gt;&lt;p&gt;开始前先确认这些条件：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;你能正常登录 &lt;code&gt;portal.bnbu.edu.cn&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;你自己的 BNBU VPN 账号当前可用。&lt;/li&gt;&#10;&lt;li&gt;手机里的宁盾令牌当前能生成正确动态码。&lt;/li&gt;&#10;&lt;li&gt;Windows 自动设置时间和时区已经开启。&lt;/li&gt;&#10;&lt;li&gt;你接受把静态密码与第二因素放在同一台电脑上的风险。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;重新获取手机令牌可能让旧令牌立即失效。先留出足够时间，一次做完重新绑定、转换、对码和 VPN 测试。不要在快要用 VPN 时临时操作。&lt;/p&gt;&#10;&lt;h2 id="第一步安装并验证-openconnect-gui"&gt;&lt;a href="#%e7%ac%ac%e4%b8%80%e6%ad%a5%e5%ae%89%e8%a3%85%e5%b9%b6%e9%aa%8c%e8%af%81-openconnect-gui" class="header-anchor"&gt;&lt;/a&gt;第一步：安装并验证 OpenConnect-GUI&#10;&lt;/h2&gt;&lt;p&gt;安装 OpenConnect-GUI 后，先不要碰 OTP。只配置服务器并完成一次普通连接，确认替代客户端本身可用。&lt;/p&gt;&#10;&lt;p&gt;基础项目如下：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;项目&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;填写内容&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Name&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;BNBU VPN&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Gateway&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;https://v.bnbu.edu.cn&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Username&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;你的 BNBU 账号&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Groupname&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;student&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;VPN protocol&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;Cisco AnyConnect or OpenConnect&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CA Certificate&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;留空，使用系统证书&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Server Certificate&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;留空&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;User Certificate / User Key&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;留空&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Disable UDP&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;默认不勾&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Use Proxy&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;不勾&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;第一次测试时，把 OTP Token 设为 None 或 Disabled。连接窗口出现认证组时选择 Student，输入账号、密码和手机上的宁盾六位码。只有这一步成功，后面才有继续自动化的意义。&lt;/p&gt;&#10;&lt;h2 id="第二步确认底层-openconnect-至少是-920"&gt;&lt;a href="#%e7%ac%ac%e4%ba%8c%e6%ad%a5%e7%a1%ae%e8%ae%a4%e5%ba%95%e5%b1%82-openconnect-%e8%87%b3%e5%b0%91%e6%98%af-920" class="header-anchor"&gt;&lt;/a&gt;第二步：确认底层 OpenConnect 至少是 9.20&#10;&lt;/h2&gt;&lt;p&gt;OpenConnect-GUI 1.6.2 当时附带的底层核心是 9.12。这个版本不能完整解析 &lt;code&gt;otpauth://&lt;/code&gt; URI，也不能从 URI 正确读取非默认的 60 秒周期。&lt;/p&gt;&#10;&lt;p&gt;&lt;a class="link" href="https://www.infradead.org/openconnect/changelog.html" target="_blank" rel="noopener"&#10; &gt;OpenConnect 更新记录&lt;/a&gt;显示，&lt;code&gt;otpauth://&lt;/code&gt; URI 和非默认 TOTP 周期支持在 9.20 加入。我实际使用的是 9.21，GnuTLS 版本为 3.8.11。&lt;/p&gt;&#10;&lt;p&gt;在 PowerShell 中检查：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;C:\Program Files\OpenConnect-GUI\openconnect.exe&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-version&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;应该看到类似：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OpenConnect version v9.21&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using GnuTLS 3.8.11&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Features present: ... HOTP software token, TOTP software token ...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果还是 9.12，不要只复制一个来路不明的 DLL。OpenConnect、GnuTLS 及依赖库要使用同一套 Windows 构建。优先使用 OpenConnect 官方发布页或可信发行包，并保留能够恢复的旧安装。升级后先启动 GUI，再重新做一次手动验证码连接。&lt;/p&gt;&#10;&lt;h2 id="第三步从-bnbu-门户进入令牌解绑"&gt;&lt;a href="#%e7%ac%ac%e4%b8%89%e6%ad%a5%e4%bb%8e-bnbu-%e9%97%a8%e6%88%b7%e8%bf%9b%e5%85%a5%e4%bb%a4%e7%89%8c%e8%a7%a3%e7%bb%91" class="header-anchor"&gt;&lt;/a&gt;第三步：从 BNBU 门户进入令牌解绑&#10;&lt;/h2&gt;&lt;p&gt;登录 &lt;a class="link" href="https://portal.bnbu.edu.cn/" target="_blank" rel="noopener"&#10; &gt;BNBU Portal&lt;/a&gt;，进入应用中心，在通用服务中找到“令牌解绑”。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="BNBU Portal 中的令牌解绑入口" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://www.huangqixuan.com/img/anyconnect/bnbu-portal-token-service.png"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;图中的“令牌解绑”就是进入宁盾自助页面的入口。&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;进入后选择“手机令牌”。页面会显示当前令牌状态，并提供“重新获取手机令牌”和“显示激活码内容”。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="宁盾手机令牌自助页面" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://www.huangqixuan.com/img/anyconnect/nington-mobile-token.png"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;点击重新获取后，以新页面生成的个人二维码为准。图中二维码区域已经留空。&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;公共激活二维码与这里重新生成的个人二维码可能不同：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;类型&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;解码后的特征&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;应该怎样处理&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;公共激活码&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;activationMethod: 2&lt;/code&gt;，没有 &lt;code&gt;token&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;不能直接转换，需要走官方 App 激活流程&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;个人手机令牌&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;activationMethod: 1&lt;/code&gt;，包含 &lt;code&gt;token&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;可以在本人电脑上离线检查和转换&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;BNBU 这次实测走的是第二种。个人二维码中的 token 带有 Hex seed、算法编号、刷新周期和验证码长度。&lt;/p&gt;&#10;&lt;h2 id="第四步完全离线地读取和转换"&gt;&lt;a href="#%e7%ac%ac%e5%9b%9b%e6%ad%a5%e5%ae%8c%e5%85%a8%e7%a6%bb%e7%ba%bf%e5%9c%b0%e8%af%bb%e5%8f%96%e5%92%8c%e8%bd%ac%e6%8d%a2" class="header-anchor"&gt;&lt;/a&gt;第四步：完全离线地读取和转换&#10;&lt;/h2&gt;&lt;p&gt;不要把个人二维码传给在线二维码网站。最方便的入口是宁盾页面的“显示激活码内容”，复制完整激活链接后，在本人电脑上处理。&lt;/p&gt;&#10;&lt;p&gt;下面脚本只使用 Python 标准库。它会读取粘贴的激活链接，解析 &lt;code&gt;#&lt;/code&gt; 后的 Base64 JSON，检查是否包含个人 token，再把 Hex seed 转为 Base32 并生成 &lt;code&gt;otpauth://&lt;/code&gt; URI。&lt;/p&gt;&#10;&lt;p&gt;新建 &lt;code&gt;convert_dkey.py&lt;/code&gt;：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;base64&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlsplit&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;粘贴个人手机令牌的完整激活链接：&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fragment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fragment&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;链接中没有 #Base64 片段&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fragment&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;=&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urlsafe_b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;token&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;这个二维码没有个人 token，不能按本教程直接转换。&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34; activationMethod=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;activationMethod&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;seed_hex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;seed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;seed_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed_hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b32encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ascii&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;passwordLength&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;timeStep&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;BNBU:DKEY&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;issuer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;BNBU&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;otpauth://totp/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?secret=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;amp;issuer=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;issuer&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;algorithm=SHA1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;amp;digits=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;period=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;activationMethod: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;activationMethod&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;seed 字节数: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;digits: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;period: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;下面一行是登录凭据，不要发给任何人：&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在脚本所在目录运行：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;python&lt;/span&gt; &lt;span class="p"&gt;.\&lt;/span&gt;&lt;span class="n"&gt;convert_dkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;py&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我这次实际读到的结构是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;activationMethod&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;token&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;crypto&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;seed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;lt;40 位 Hex，已删除&amp;gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;timeStep&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;passwordLength&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;seed&lt;/code&gt; 的 40 位 Hex 对应 20 字节，也就是 160 bit。&lt;code&gt;crypto: 1&lt;/code&gt; 在这次部署中对应 HMAC-SHA1，验证码每 60 秒更新一次，共 6 位。教程里的数字来自这次 BNBU 实测，但脚本仍以你自己的 JSON 字段为准。&lt;/p&gt;&#10;&lt;h2 id="第五步先对码不要直接登录"&gt;&lt;a href="#%e7%ac%ac%e4%ba%94%e6%ad%a5%e5%85%88%e5%af%b9%e7%a0%81%e4%b8%8d%e8%a6%81%e7%9b%b4%e6%8e%a5%e7%99%bb%e5%bd%95" class="header-anchor"&gt;&lt;/a&gt;第五步：先对码，不要直接登录&#10;&lt;/h2&gt;&lt;p&gt;先把脚本生成的 URI 导入支持自定义周期的验证器，例如 Bitwarden。不要急着填进 OpenConnect。&lt;/p&gt;&#10;&lt;p&gt;同时打开宁盾官方 App 和新的验证器，观察同一个 60 秒周期内的六位码。至少连续跨过 2 到 3 个周期，每次都完全一致，才能确认：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Hex 解码正确。&lt;/li&gt;&#10;&lt;li&gt;Base32 转换正确。&lt;/li&gt;&#10;&lt;li&gt;算法是 SHA-1。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;digits=6&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;period=60&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;电脑和手机时间同步。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;只对上一次不够。周期或时间刚好错位时，偶尔也可能造成误判。对码失败时，先检查自动时间与时区，再核对脚本读出的 &lt;code&gt;timeStep&lt;/code&gt;、&lt;code&gt;passwordLength&lt;/code&gt; 和 &lt;code&gt;crypto&lt;/code&gt;，不要连续尝试 VPN 登录。&lt;/p&gt;&#10;&lt;h2 id="第六步把-totp-与密码交给-openconnect-gui"&gt;&lt;a href="#%e7%ac%ac%e5%85%ad%e6%ad%a5%e6%8a%8a-totp-%e4%b8%8e%e5%af%86%e7%a0%81%e4%ba%a4%e7%bb%99-openconnect-gui" class="header-anchor"&gt;&lt;/a&gt;第六步：把 TOTP 与密码交给 OpenConnect-GUI&#10;&lt;/h2&gt;&lt;p&gt;对码通过后，编辑 OpenConnect-GUI 配置：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="脱敏后的 OpenConnect-GUI 完整配置" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://www.huangqixuan.com/img/anyconnect/openconnect-profile-redacted.png"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Username 与 OTP URI 已脱敏，其余项目按实测配置保留。&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;需要改动的项目：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;项目&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;设置&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;OTP Token&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;TOTP (RFC6238)&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;OTP 右侧输入框&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;粘贴完整 &lt;code&gt;otpauth://&lt;/code&gt; URI&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Batch mode&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;勾选&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Minimize on Connect&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;按需勾选&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;保存后连接。第一次可能仍要完成一次密码输入，让 GUI 保存静态密码。成功后断开，再重新打开 GUI 测试第二次连接。&lt;/p&gt;&#10;&lt;p&gt;最终验收标准不是“配置保存成功”，而是：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;点击 Connect 后不再要求输入静态密码。&lt;/li&gt;&#10;&lt;li&gt;不再弹出宁盾六位码输入框。&lt;/li&gt;&#10;&lt;li&gt;日志进入 Connected，学校内网资源可以正常访问。&lt;/li&gt;&#10;&lt;li&gt;断开后再次连接仍然成功。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="如果二维码里没有-token"&gt;&lt;a href="#%e5%a6%82%e6%9e%9c%e4%ba%8c%e7%bb%b4%e7%a0%81%e9%87%8c%e6%b2%a1%e6%9c%89-token" class="header-anchor"&gt;&lt;/a&gt;如果二维码里没有 token&#10;&lt;/h2&gt;&lt;p&gt;如果脚本提示 &lt;code&gt;activationMethod=2&lt;/code&gt; 或没有 &lt;code&gt;token&lt;/code&gt;，你拿到的是公共激活码。这种二维码只有宁盾服务器信息，不能凭空推导个人 seed。&lt;/p&gt;&#10;&lt;p&gt;宁盾转换的三篇参考文章还记录了两条备用路线：一种是在令牌绑定网页中检查本人 &lt;code&gt;refreshMobileToken&lt;/code&gt; 响应，另一种是在本人 App 激活时检查返回数据。响应里的 seed 可能是 Base64，也可能是 Hex，而且某些 &lt;code&gt;data&lt;/code&gt; 字段还是双重编码的 JSON。&lt;/p&gt;&#10;&lt;p&gt;这两条路线会接触账号、激活流量和个人二次认证密钥，环境差异也很大。BNBU 当前可以从门户重新获取包含 token 的个人二维码，因此本文不把抓包作为默认步骤。没有 token 时，优先回门户确认是否点了“重新获取手机令牌”，或者联系 ITSC 获取正式支持。&lt;/p&gt;&#10;&lt;h2 id="收尾把暴露面清干净"&gt;&lt;a href="#%e6%94%b6%e5%b0%be%e6%8a%8a%e6%9a%b4%e9%9c%b2%e9%9d%a2%e6%b8%85%e5%b9%b2%e5%87%80" class="header-anchor"&gt;&lt;/a&gt;收尾：把暴露面清干净&#10;&lt;/h2&gt;&lt;p&gt;完成后检查并删除这些内容：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;个人二维码截图。&lt;/li&gt;&#10;&lt;li&gt;保存完整二维码文本的文件。&lt;/li&gt;&#10;&lt;li&gt;解码后的 JSON。&lt;/li&gt;&#10;&lt;li&gt;包含 seed、Base32 或完整 URI 的终端历史、聊天记录和剪贴板。&lt;/li&gt;&#10;&lt;li&gt;为排查临时生成的日志与抓包文件。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;如果真实 seed 或 Base32 曾经发进聊天、上传网盘或交给在线转换网站，按已经泄露处理。回 Portal 重新获取手机令牌，让旧令牌失效，再用新的二维码重新做一次。不要只删除聊天记录后继续使用旧密钥。&lt;/p&gt;&#10;&lt;p&gt;还要接受一个现实：把静态密码和 TOTP seed 都放进同一台电脑后，便利性提高了，双因素原本的设备隔离却减弱了。Windows 登录、磁盘加密和电脑的物理安全会变得更重要。&lt;/p&gt;&#10;&lt;h2 id="回头看这次折腾"&gt;&lt;a href="#%e5%9b%9e%e5%a4%b4%e7%9c%8b%e8%bf%99%e6%ac%a1%e6%8a%98%e8%85%be" class="header-anchor"&gt;&lt;/a&gt;回头看这次折腾&#10;&lt;/h2&gt;&lt;p&gt;我最开始只是受不了每次连接都要输入账号密码，再拿手机、解锁宁盾令牌、抄六位码，也不喜欢 Cisco AnyConnect 的服务一直随系统启动。&lt;/p&gt;&#10;&lt;p&gt;中间试过 GUI、命令行脚本、密码自动填写、宁盾 Push、二维码识别，甚至差点走到手机抓包。真正解决问题的几个点都很具体：Batch mode 能保存密码；个人重新获取的二维码与公共二维码不同；Hex、Base32 和 &lt;code&gt;otpauth://&lt;/code&gt; 只是同一密钥的不同表示；OpenConnect 9.20 之后才支持完整 URI 与非默认 TOTP 周期。&lt;/p&gt;&#10;&lt;p&gt;现在我打开 OpenConnect-GUI，点击一次 Connect 就结束了。折腾过程比每天多输几次验证码花的时间多得多，但我终于不用再和那个重复流程较劲。&lt;/p&gt;&#10;&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://haotian22.top/546c6c89.html" target="_blank" rel="noopener"&#10; &gt;HaoTian：UIC AnyConnect 的校园 VPN 研究&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://baka.link/zh/posts/dkey-token-to-standard-totp/" target="_blank" rel="noopener"&#10; &gt;Kirin：将 Dkey Token（宁盾令牌）转换为标准 TOTP&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://blog.steven53.top/p/%E5%AE%81%E7%9B%BE%E4%BB%A4%E7%89%8Cdkey-token%E8%BD%AC%E6%8D%A2%E4%B8%BA%E6%A0%87%E5%87%86totp/" target="_blank" rel="noopener"&#10; &gt;Steven：宁盾令牌（Dkey Token）转换为标准 TOTP&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://www.jianyun.run/posts/convert-ningdun-token-to-standard-2fa-totp/" target="_blank" rel="noopener"&#10; &gt;唐长老：宁盾令牌转标准 2FA TOTP&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://www.infradead.org/openconnect/token.html" target="_blank" rel="noopener"&#10; &gt;OpenConnect 软件令牌说明&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://www.infradead.org/openconnect/changelog.html" target="_blank" rel="noopener"&#10; &gt;OpenConnect 更新记录&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://itsc.bnbu.edu.cn/BNBU%20Campus%20VPN%20Service%20Guideline--2025.pdf" target="_blank" rel="noopener"&#10; &gt;BNBU Campus VPN Service Guideline 2025&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://itsc.uic.edu.cn/ningdunzizhujiebangcaozuoshouce.pdf" target="_blank" rel="noopener"&#10; &gt;宁盾自助解绑操作手册&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;学校认证策略、门户页面和客户端版本以后都可能变化。重新操作时，以本人二维码里的字段、OpenConnect 实际版本和一次真实连接为准。&lt;/p&gt;&#10;</description></item><item><title>从毛玻璃到液态玻璃：这个博客是怎么搭起来的</title><link>https://www.huangqixuan.com/p/liquid-glass-blog-build/</link><pubDate>Thu, 27 Aug 2026 00:00:00 +0800</pubDate><guid>https://www.huangqixuan.com/p/liquid-glass-blog-build/</guid><description>&lt;img src="https://www.huangqixuan.com/img/tahoe-light-2560.webp" alt="Featured image of post 从毛玻璃到液态玻璃：这个博客是怎么搭起来的" /&gt;&lt;p&gt;家里已经买了 &lt;code&gt;huangqixuan.com&lt;/code&gt;，一直空着有点可惜。我想拿它写教程，也给自己留一个整理问题的地方。&lt;/p&gt;&#10;&lt;p&gt;我以前用 InfinityFree 的免费空间和 &lt;code&gt;wuaze.com&lt;/code&gt; 子域名建过网站，装过评论区，也折腾过论坛。后来账号因为长期没有使用被暂停并删除，子域名随之报出 DNS 解析错误。网页文件、域名和服务器原来是三件事，免费平台收回空间时，内容和网址都有可能一起失效。&lt;/p&gt;&#10;&lt;p&gt;所以这次我把源文件留在电脑里。以后可以换托管平台，也可以重新解析域名，但文章不能跟着平台消失。&lt;/p&gt;&#10;&lt;h2 id="先决定网站该怎么运行"&gt;&lt;a href="#%e5%85%88%e5%86%b3%e5%ae%9a%e7%bd%91%e7%ab%99%e8%af%a5%e6%80%8e%e4%b9%88%e8%bf%90%e8%a1%8c" class="header-anchor"&gt;&lt;/a&gt;先决定网站该怎么运行&#10;&lt;/h2&gt;&lt;p&gt;动态网站会在访问时查询数据库，再生成页面。WordPress、论坛和带用户系统的网站通常这样运行。登录、发帖、后台管理和复杂评论都更容易实现，代价是要长期维护服务器、数据库、更新和安全设置。&lt;/p&gt;&#10;&lt;p&gt;静态网站会提前把文章生成成 HTML。读者打开网页时，服务器只需发送已经做好的文件。我的教程博客不需要注册和数据库，静态方案省事，也少了一批服务器配置问题。&lt;/p&gt;&#10;&lt;p&gt;我最后选了这条流程：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Markdown 文章&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Hugo 生成 HTML&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Cloudflare Pages 发布&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;www.huangqixuan.com&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;GitHub 在这里不是必需的服务器，更像一个保存修改历史并触发自动部署的代码仓库。这个网站现在不依赖 GitHub，我可以直接用 Wrangler 发布到 Cloudflare Pages。以后如果改用 GitHub 自动构建，文章源文件也不用重写。&lt;/p&gt;&#10;&lt;h2 id="为什么用了-hugo-stack"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e7%94%a8%e4%ba%86-hugo-stack" class="header-anchor"&gt;&lt;/a&gt;为什么用了 Hugo Stack&#10;&lt;/h2&gt;&lt;p&gt;看了几个教程博客之后，我比较喜欢 Hugo Stack 的三栏布局。头像和导航在左边，文章在中间，搜索、分类和标签在右边。桌面上不会显得太空，到了手机上又能收成单栏。&lt;/p&gt;&#10;&lt;p&gt;Hugo 负责把内容生成网页，Stack 提供基础模板。文章仍然是普通的 Markdown，例如：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;文章标题&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ld"&gt;2026-08-28&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;建站笔记&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Hugo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;正文写在这里。&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样一来，重做网站样式不需要搬文章。哪天不想用 Stack，甚至不想用 Hugo，这些 Markdown 文件照样能打开。&lt;/p&gt;&#10;&lt;h2 id="第一版为什么看起来不像液态玻璃"&gt;&lt;a href="#%e7%ac%ac%e4%b8%80%e7%89%88%e4%b8%ba%e4%bb%80%e4%b9%88%e7%9c%8b%e8%b5%b7%e6%9d%a5%e4%b8%8d%e5%83%8f%e6%b6%b2%e6%80%81%e7%8e%bb%e7%92%83" class="header-anchor"&gt;&lt;/a&gt;第一版为什么看起来不像液态玻璃&#10;&lt;/h2&gt;&lt;p&gt;刚开始时，我只知道自己喜欢苹果的 Liquid Glass 风格，于是先去找现成的 Skill。最早找到的是 &lt;code&gt;apple-design&lt;/code&gt;、&lt;code&gt;frontend-design&lt;/code&gt; 这类设计规范。它们能指导层级、动效和控件组织，却不会凭空产生真实折射。&lt;/p&gt;&#10;&lt;p&gt;第一批 Demo 主要用了这些 CSS：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;background&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;rgba&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;12&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;backdrop-filter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;blur&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;24px&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;saturate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;140&lt;/span&gt;&lt;span class="o"&gt;%);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;border&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;1px&lt;/span&gt; &lt;span class="nt"&gt;solid&lt;/span&gt; &lt;span class="nt"&gt;rgba&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;2&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这几行 CSS 能做出毛玻璃，但不会改变背景像素的位置。玻璃下面的线条没有弯曲，边缘也没有凸透镜感，更像一张半透明塑料片。&lt;/p&gt;&#10;&lt;p&gt;后来我不再只看模糊程度，而是检查这些细节：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;背景细节在玻璃边缘发生位移。&lt;/li&gt;&#10;&lt;li&gt;曲面边缘有连续折射，不只是模糊。&lt;/li&gt;&#10;&lt;li&gt;色差和高光要克制，不能像霓虹描边。&lt;/li&gt;&#10;&lt;li&gt;控件移动时要重新采样它下面的画面。&lt;/li&gt;&#10;&lt;li&gt;内容仍然清楚，按钮和输入框也必须能点。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="把六个原项目放在一起比较"&gt;&lt;a href="#%e6%8a%8a%e5%85%ad%e4%b8%aa%e5%8e%9f%e9%a1%b9%e7%9b%ae%e6%94%be%e5%9c%a8%e4%b8%80%e8%b5%b7%e6%af%94%e8%be%83" class="header-anchor"&gt;&lt;/a&gt;把六个原项目放在一起比较&#10;&lt;/h2&gt;&lt;p&gt;截图很容易骗人。我把六个公开项目下载到本地，保留各自的引擎和默认参数，再做了一页统一入口。对比对象如下：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;项目&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;实现路线&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;我最后记住的特点&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;a class="link" href="https://github.com/ybouane/liquidglass" target="_blank" rel="noopener"&#10; &gt;ybouane/liquidglass&lt;/a&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;WebGL Shader&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;折射、色差和光照完整，适合独立按钮和工具栏&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;a class="link" href="https://github.com/archisvaze/liquid-glass" target="_blank" rel="noopener"&#10; &gt;archisvaze/liquid-glass&lt;/a&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;SVG + WebGL&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;像一个光学实验台，适合理解厚度、IOR 和边缘宽度&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;a class="link" href="https://github.com/xanaxknight/liquid-glass-css-svg" target="_blank" rel="noopener"&#10; &gt;xanaxknight/liquid-glass-css-svg&lt;/a&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;SVG displacement&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;单文件也能做出可见位移，适合研究位移图&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;a class="link" href="https://github.com/el-gladiador/liquid-glass-react" target="_blank" rel="noopener"&#10; &gt;el-gladiador/liquid-glass-react&lt;/a&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;React + WebGL2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;共享 WebGL 上下文，适合 React 项目中的多层玻璃&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;a class="link" href="https://github.com/deepika-builds/liquid-glass" target="_blank" rel="noopener"&#10; &gt;deepika-builds/liquid-glass&lt;/a&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;JavaScript + SVG&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;中心清楚、边缘折射，普通 DOM 内容仍可交互&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;a class="link" href="https://github.com/dpawlikowski/liquid-glass" target="_blank" rel="noopener"&#10; &gt;dpawlikowski/liquid-glass&lt;/a&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CSS + SVG&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;不依赖 WebGL，也能组合折射和色差&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;对比之后，我放弃了把所有参数硬凑成一套的做法。一个项目里的 IOR 不能直接换算成另一个项目的 &lt;code&gt;refraction&lt;/code&gt;。大多数时候，原项目调好的默认值比我随手改的数字平衡。&lt;/p&gt;&#10;&lt;p&gt;我最喜欢 1 号的 WebGL 光学和 5 号的可读窗口。5 号 Demo 还带了一套果冻拖动：窗口用 &lt;code&gt;stiffness = 0.12&lt;/code&gt; 和 &lt;code&gt;damping = 0.72&lt;/code&gt; 追赶鼠标，再按速度沿移动方向拉长，最大形变约 8%。折射负责玻璃的样子，拖动负责弹性，它们其实是两套机制。&lt;/p&gt;&#10;&lt;h2 id="把研究结果写成自己的-skill"&gt;&lt;a href="#%e6%8a%8a%e7%a0%94%e7%a9%b6%e7%bb%93%e6%9e%9c%e5%86%99%e6%88%90%e8%87%aa%e5%b7%b1%e7%9a%84-skill" class="header-anchor"&gt;&lt;/a&gt;把研究结果写成自己的 Skill&#10;&lt;/h2&gt;&lt;p&gt;原项目解决了效果，但没有告诉我建站时该把效果放在哪里。我把对比结果整理成 &lt;code&gt;liquid-glass-webgl-jelly&lt;/code&gt; Skill。以后改页面时先看组件是做什么的，不再见到一个框就套一层玻璃。&lt;/p&gt;&#10;&lt;p&gt;这套 Skill 目前遵循这些规则：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;可读窗口、搜索框和信息面板使用边缘位移方案，中心尽量保持清楚。&lt;/li&gt;&#10;&lt;li&gt;独立按钮和紧凑工具栏可以使用 WebGL，但不能在一页堆几十个 Canvas。&lt;/li&gt;&#10;&lt;li&gt;长文章、表格和重复列表不做强折射，阅读比材质展示重要。&lt;/li&gt;&#10;&lt;li&gt;玻璃后面必须有可辨认的壁纸、线条或网格，否则无法确认折射是否真的存在。&lt;/li&gt;&#10;&lt;li&gt;选中、按下和成功状态不能只靠折射，还要有文字、边框或圆点提示。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;prefers-reduced-motion&lt;/code&gt; 和降低透明度设置必须有对应降级。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;有一条规则来自实际遇到的 Bug。早期 Demo 在玻璃导航条里又放了一层透明的 WebGL 选中透镜。它虽然没有填充色，却会重新折射下面的竹林背景。点击标签之后，画面不像胶囊在移动，反而像背景突然换了一块。我删掉第二层折射，只保留清楚的选中状态，问题才消失。&lt;/p&gt;&#10;&lt;p&gt;网站分类卡也用了同一个处理。外层窗口已经有真实折射，内层卡片只保留稳定的浅色衬底、细边框和很小的悬浮变化。再套一层光学滤镜会让画面变脏，也会拖慢滚动。&lt;/p&gt;&#10;&lt;h2 id="把-stack-改成现在的样子"&gt;&lt;a href="#%e6%8a%8a-stack-%e6%94%b9%e6%88%90%e7%8e%b0%e5%9c%a8%e7%9a%84%e6%a0%b7%e5%ad%90" class="header-anchor"&gt;&lt;/a&gt;把 Stack 改成现在的样子&#10;&lt;/h2&gt;&lt;p&gt;我没有直接照搬某个 Demo，而是保留 Stack 的三栏结构，再慢慢替换里面的部件。&lt;/p&gt;&#10;&lt;p&gt;左栏删掉多余介绍，只留下头像、标题、首页、归档、关于和暗色模式。中间还是普通文章流，整张卡片都可以点击。右栏原本有好几个搜索入口，我把它们合并成一个苹果式搜索框，顶部和导航里不再重复放搜索按钮。&lt;/p&gt;&#10;&lt;p&gt;背景用的是 Tahoe 亮色壁纸。原图是 6016 × 6016 的 PNG，直接放进网页太大，我把它压成了 2560 像素的 WebP。线条和颜色变化还在，文件只剩几十 KB。&lt;/p&gt;&#10;&lt;p&gt;网站现在主要使用 SVG 位移滤镜。脚本会为头像区、导航、工具栏、搜索面板、文章卡和正文窗口初始化折射。不支持 SVG &lt;code&gt;backdrop-filter&lt;/code&gt; 的浏览器会退回磨砂效果。研究时准备的 WebGL 和果冻拖动代码仍然留着，但没有一起铺满整页。少几个 Shader，滚动会更稳。&lt;/p&gt;&#10;&lt;p&gt;改样式的过程中，我遇到过这些问题：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;整页套了暗色滤镜，导致亮壁纸变灰，文字也发闷。&lt;/li&gt;&#10;&lt;li&gt;搜索框沿用了暗色主题背景，变成一块纯黑矩形。&lt;/li&gt;&#10;&lt;li&gt;某些文字为了显得精致被缩得太小，在手机上很难看清。&lt;/li&gt;&#10;&lt;li&gt;卡片静止时完全透明，鼠标移上去才突然出现一大块底色。&lt;/li&gt;&#10;&lt;li&gt;移动端背景高度计算错误，页面底部出现黑色区域。&lt;/li&gt;&#10;&lt;li&gt;每次切页都重新加载全部效果，滚动和跳转显得卡。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;我对着截图逐个修正，没有一次性推倒重做。现在卡片从静止到悬浮不会突然换色，正文对比度稳定，手机页面底部也不会再露出黑底。&lt;/p&gt;&#10;&lt;h2 id="发布到-cloudflare-pages"&gt;&lt;a href="#%e5%8f%91%e5%b8%83%e5%88%b0-cloudflare-pages" class="header-anchor"&gt;&lt;/a&gt;发布到 Cloudflare Pages&#10;&lt;/h2&gt;&lt;p&gt;本地写文章时先启动 Hugo：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;hugo&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="n"&gt;-D&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;浏览器会立即显示本地修改。页面确认正常后，再生成发布用的静态文件：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;hugo&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-minify&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-cleanDestinationDir&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-destination&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;cloudflare-dist&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最后用 Wrangler 上传到已有的 Pages 项目：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;npx&lt;/span&gt; &lt;span class="n"&gt;wrangler&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="n"&gt;deploy&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;cloudflare-dist&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-project-name&lt;/span&gt; &lt;span class="nb"&gt;huangqixuan-blog&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;-branch&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Cloudflare Pages 会生成一个版本地址，并把生产版本更新到 &lt;code&gt;huangqixuan-blog.pages.dev&lt;/code&gt;。项目已经绑定 &lt;code&gt;www.huangqixuan.com&lt;/code&gt;，Hugo 的 &lt;code&gt;baseURL&lt;/code&gt; 也指向这里。&lt;/p&gt;&#10;&lt;p&gt;现在的发布流程固定为四步：写 Markdown、本地构建、检查视觉和功能、同步到 Cloudflare 并确认生产状态。托管平台保存的是生成后的网页，我需要长期保留的是 Markdown、模板、样式和脚本。&lt;/p&gt;&#10;&lt;h2 id="现在回头看"&gt;&lt;a href="#%e7%8e%b0%e5%9c%a8%e5%9b%9e%e5%a4%b4%e7%9c%8b" class="header-anchor"&gt;&lt;/a&gt;现在回头看&#10;&lt;/h2&gt;&lt;p&gt;这个网站不是选好技术后一次搭成的。我从一个闲置域名开始，先后考虑过免费空间、GitHub Pages、闲置电脑服务器和动态博客，最后才选定 Hugo 与 Cloudflare Pages。&lt;/p&gt;&#10;&lt;p&gt;液态玻璃也绕了不少路。我先做出毛玻璃，发现不对后又下载原项目比较，接着处理玻璃叠加、背景突变、文字对比度和性能问题。最后整理出了一套使用边界。&lt;/p&gt;&#10;&lt;p&gt;这篇文章也算一份备忘录。以后网站哪里改坏了，我可以回来看看当时为什么这样选，不用再从十几个标签页和聊天记录里重新找答案。&lt;/p&gt;&#10;</description></item><item><title>你好，欢迎来到我的博客</title><link>https://www.huangqixuan.com/p/hello/</link><pubDate>Wed, 26 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.huangqixuan.com/p/hello/</guid><description>&lt;p&gt;我想给自己留个能长期写东西的地方。以后遇到值得记下来的问题，就把过程和最后能用的办法放在这里，不再只丢进聊天记录或收藏夹。&lt;/p&gt;&#10;&lt;h2 id="这里会写什么"&gt;&lt;a href="#%e8%bf%99%e9%87%8c%e4%bc%9a%e5%86%99%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;这里会写什么&#10;&lt;/h2&gt;&lt;p&gt;大多是软件怎么用、电脑出了问题怎么查，还有搭这个网站时踩过的坑。偶尔也会放一点我不想忘掉的东西。&lt;/p&gt;&#10;&lt;blockquote class="alert alert-note"&gt;&#10; &lt;div class="alert-header"&gt;&#10; &lt;span class="alert-icon"&gt;📝&lt;/span&gt;&#10; &lt;span class="alert-title"&gt;备注&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class="alert-body"&gt;&#10; &lt;p&gt;现在还是个刚搭起来的空架子，内容会慢慢补。&lt;/p&gt;&#10; &lt;/div&gt;&#10; &lt;/blockquote&gt;&#10;&lt;h2 id="一段示例代码"&gt;&lt;a href="#%e4%b8%80%e6%ae%b5%e7%a4%ba%e4%be%8b%e4%bb%a3%e7%a0%81" class="header-anchor"&gt;&lt;/a&gt;一段示例代码&#10;&lt;/h2&gt;&lt;p&gt;教程里可以直接放命令。比如查看网络配置：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ipconfig&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;搜索和分类已经能用。下一步是把真正遇到过的问题整理成文章。&lt;/p&gt;&#10;</description></item></channel></rss>