# login-totp：Windows 登录 MFA 方案选型与实施

## 背景

原本计划用第三方 MFA 工具给 Windows 本地/RDP 登录加一道多因素验证。但调研后发现，那类工具的登录链路是：本地输入账号密码 → 工具再去连第三方的云端服务器做一次校验 → 校验通过才放行。这样一来，本来"本地/RDP 直接登录"这一条链路，多绕了一次到外部服务器，远程登录的延迟因此变得不可接受。基于这个问题，转向调研有没有不依赖外部服务器、校验完全在本地完成的方案。

## 方案调研

### Rublon（已放弃）

Rublon 是一个成熟的商用 MFA 产品，支持 Windows Logon。放弃的原因是需企业注册登录，依赖外部服务器验证。

### RustDesk（已放弃）

RustDesk 是开源的远程桌面工具，本身也在讨论范围内，放弃的原因是：

1. **保护的层级不对**：RustDesk 的 2FA 验证的是"谁能登录 RustDesk 自己的管理控制台/账号"，也就是谁能用这个工具发起远程连接，并不是 Windows 系统登录本身的二次验证。
3. **RustDesk 本质上是另一套远程访问工具**，要用它等于把现在基于 Windows 自带 RDP 的远程访问方式整个换掉，而不是在现有 RDP 登录上加一道验证，强依赖外部服务器，连接链路变长，延迟不可接受。

### multiOTP Credential Provider（已采用）

[multiOTP/multiOTPCredentialProvider](https://github.com/multiOTP/multiOTPCredentialProvider) 是 Apache 2.0 协议的开源项目，核心优势：

- **免费开源**，没有授权成本，没有按用户数收费的问题
- **支持完全本地（standalone）校验模式**：TOTP 校验在本机由内置的 `multiotp.exe` 完成，不需要连接任何外部/云端服务器，从架构上消除了"多一跳网络请求"的延迟问题
- **原生 Windows Credential Provider**，直接接管 Windows 登录界面（V2 Credential Provider），本地控制台登录和 RDP 远程登录都能覆盖，且可以按场景单独控制（比如只对 RDP 强制、本地不受影响）
- **兼容标准 TOTP（RFC 6238）**，可以直接用 Google Authenticator 扫码，不需要专用 App
- 持续维护中（最新版本 5.10.2.2，发布于 2026-04），支持 Windows 7 到 Windows 11/Server 2025

底层用到的另一个开源项目是 [multiOTP/multiotp](https://github.com/multiOTP/multiotp)——一个独立维护的开源 TOTP/HOTP CLI 工具，multiOTP Credential Provider 内嵌了它来做本地 secret 管理和校验。

## 本项目做了什么

**不重复造轮子**：真正干活的部分（凭证提供程序 DLL、COM 注册、TOTP 算法、`multiotp.exe`）全部使用官方编译好的 MSI 安装，代码没有做任何修改。本项目只是在"安装 / 配置 / 启用 / 回滚"这几个操作上补了一层自动化脚本和安全防护，方便重复部署。

脚本按操作风险分成了四个独立步骤，每一步都可以单独执行、单独确认：

| 脚本 | 作用 |
|---|---|
| `install.ps1` | 用项目自带 `vendor\` 里的本地安装包（不连网下载）校验 SHA256，静默安装，安装完成后立即设为"不要求 OTP"状态，不影响任何现有登录方式；随后去 ifconfig.me 查本机公网 IPv4，把 Google Authenticator 里显示的发行方名字（issuer）从默认的 `multiOTP` 改成这个 IP，方便多台机器用同一个账号名时能分清是哪台 |
| `enroll.ps1` | 给指定账号生成 TOTP 密钥，把 otpauth:// 链接上传到 Galaxy（网页上现渲染二维码给用户扫），上传成功即完成，不再交互式验证 |
| `configure.ps1` | 切换到实际生效的模式（比如"只对 RDP 生效"），操作前自动备份注册表，操作后读回确认生效 |
| `disable.ps1` | 应急回滚：一条命令立即恢复到"不要求 OTP"的状态 |


## 目录结构

```
login-totp\
  install.ps1          校验+静默安装 multiOTP Credential Provider（只用 vendor\ 本地副本，不连网下载），装完立即设为 Disabled
  enroll.ps1           给一个账号生成 TOTP secret，上传到 Galaxy
  configure.ps1        切换强制模式（真正开始拦截登录的操作），带自动备份+读回确认
  disable.ps1          应急开关：立刻恢复成 Disabled，不卸载程序本身
  lib\Functions.ps1    共享函数（日志、幂等判断、文件哈希、注册表备份）
  config\settings.ps1  配置（版本号/哈希/安装路径/模式对照表/排除账号）
  vendor\              校验通过的官方 zip 本地副本，install.ps1 唯一的安装包来源
```


## 详细用法

以下命令都要在**以管理员身份打开的 PowerShell** 里执行（`cd` 到本项目目录之后）。全程建议保留一个已经登录的管理员会话不要退出，作为出问题时的退路。

### 0. 先装好 VC++ 运行库，关掉杀毒软件，powershell管理员运行后先调整脚本执行级别

装 [VC++ 2015-2022 Redistributable (x64)](https://aka.ms/vs/17/release/vc_redist.x64.exe)，然后重启。

**装的过程中、以及 `install.ps1`/`enroll.ps1` 跑的时候，建议先临时关掉杀毒软件（包括 Windows Defender 实时保护）**。原因：`multiOTP` 干活的方式是 `multiotp.exe` 内嵌一个 PHP 解释器，运行时再去调用装在同一个目录下的 `multiotp.windows.php` 脚本——这种"exe 自带脚本引擎 + 现场跑脚本"的行为模式，很容易被杀毒软件的启发式/行为检测当成可疑特征拦一下（哪怕不弹出提示，也可能在 `enroll.ps1` 执行的那一刻悄悄拒绝访问该脚本文件，报出类似 `Could not open input file: ...multiotp.windows.php` 的错误，跑完再看文件本身其实完好无损）。装完、绑定完账号之后再打开杀毒软件即可，不需要一直关着。

管理员身份打开powershell后执行
```powershell
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
```

还需要

### 1. 安装

```powershell
.\install.ps1
```

用项目自带 `vendor\` 里的本地安装包校验 SHA256，静默安装 multiOTP Credential Provider——不连网下载，`vendor\` 里没有校验通过的文件就直接报错退出。装完会立即写入"不要求 OTP"的状态，这一步本身不影响任何人登录。

### 2. 绑定账号（enroll）

```powershell
.\enroll.ps1 -Username playz
```

不用传主机名。脚本会自动查这台机器的公网出口 IPv4（跟 `install.ps1` 设 issuer 用的是同一个 `Get-InitPublicIp` 函数），上报给 Galaxy；Galaxy 那边按这个 IP 反查 `win_hosts` 补全主机名——前提是这台机器已经在 Galaxy 里登记过，且出口 IP 跟登记的主IP一致，不然上传会被拒绝（不会存一条主机名缺失/挂错的记录）。

给指定账号（比如测试账号 `playz`）生成 TOTP 密钥，取出 `otpauth://` 链接后上传到 Galaxy（不再在这台机器上生成/留存二维码图片），本地那份临时文件上传成功后立刻删除，脚本到这里就结束，不再交互式验证真实验证码。去 Galaxy 的"Windows TOTP 二维码"页面能看到刚上传的这条记录，选中后网页会现渲染二维码，给用户扫码；扫完之后记得回 Galaxy 页面点"标记已交付并清除密钥"——这条记录不会自动过期，密钥是明文，不点清除就会一直留在 Galaxy 的表里。

**一个账号给多人使用**：如果一台机器要给两个人用同一个账号登录，办法是**在 Galaxy 上点"标记已交付并清除密钥"之前，让两个人的 Google Authenticator 都扫 Galaxy 页面上的同一个二维码**——密钥是同一个，谁的手机在同一时刻都会算出一样的验证码，然后再进行核销。

**换验证码（重新绑定）**：默认情况下，对一个已经 enroll 过的账号再跑一遍 `enroll.ps1` 不会生成新密钥——脚本会保留旧密钥，只是重新取一次链接、重新上传一次到 Galaxy（Galaxy 那边按出口IP+账号名 upsert，不会留下旧记录）。要真正作废旧密钥、生成一把新的，加 `-Rotate`：

```powershell
.\enroll.ps1 -Username playz -Rotate
```

会先要求输入 `yes` 确认，再删除旧密钥、生成新密钥并重新上传到 Galaxy，之后走跟首次 enroll 一样的扫码流程。**旧手机上的验证码扫码换密钥这一刻起立即失效**；如果这台机器已经用 `configure.ps1 -Mode RDPOnly` 开了强制模式，`playz` 在完成新一轮扫码之前，RDP 会登录失败——建议避免在对方正需要远程登录时执行。

**Google Authenticator 里显示的名字**：`install.ps1` 会把 issuer 设成本机公网 IPv4（比如 `156.234.228.74:playz`），而不是默认的 `multiOTP:playz`。这是全局设置，同一台机器上所有账号共用同一个 issuer；已经扫过的旧二维码不会跟着变，只有之后新生成/重新扫码（含 `-Rotate`）的才会用上新名字。如果机器没联网或者 ifconfig.me 连不上，会跳过这一步、issuer 保持不变，不影响安装本身。

### 3. 启用（configure）

```powershell
.\configure.ps1 -Mode RDPOnly
```

真正开始要求 OTP 的一步。`-Mode RDPOnly` 表示只对 RDP 远程登录生效，本地控制台登录不受影响。执行前会自动备份当前注册表配置，需要手动输入 `yes` 确认才会真正生效，改完会读回注册表值确认已经生效。

**本地控制台登录永远不启用 OTP**，只用 `RDPOnly` 这一个模式——脚本里还支持 `LocalAndRemoteExclusive` / `LocalAndRemoteAllowBypass` 这两个会同时影响本地登录的模式，但不作为本项目的使用方式，细节见 [`DEVELOPMENT.md`](./DEVELOPMENT.md)。

**启用之后，机器上其它账号的 RDP 访问会受影响**：`RDPOnly` 一旦生效，任何尝试 RDP 登录的账号都会被要求 OTP，能通过的只有两种账号——`enroll.ps1` 绑定过 TOTP 的账号，或者 `config\settings.ps1` 里 `$ExcludedAccount` 指定的那一个豁免账号（且只能指定一个）。除此之外的账号（比如没绑定过的 `admin`）会直接登录失败，RDP 访问被连带切断，不是"跳过验证放行"。启用前需要先确认这台机器上所有还需要保留 RDP 访问的账号，都已经逐个 enroll 过。

### 4. 关闭（应急回滚）

```powershell
.\disable.ps1
```

一条命令立刻恢复成"不要求 OTP"的状态，不会卸载 multiOTP Credential Provider 本身。任何时候发现登录不正常、验证码用不了，都可以马上执行这条来恢复正常登录。彻底不再使用的话，走 Windows"添加或删除程序"正常卸载即可。

