Windows 系统级双账号登录代理 — 实现"金库双人原则"的安全认证
WinSLA 是一个 Windows 系统级双账号认证登录代理。在 Active Directory 域环境中,它要求 两个独立 AD 账号的密码同时验证通过 方可登录桌面,类似银行金库的"双人原则"(Two-Person Rule)。适用于高安全场景:
- 财务系统终端登录
- 特权管理员操作授权
- 涉密计算机双人管控
- 关键基础设施访问控制
- 双人双控:两个不同用户分别输入各自 AD 域密码,两者均验证成功才允许登录
- 真实验证:通过 Windows
LogonUserWAPI 进行真实密码验证(支持域账号/本地账号) - 密码过期处理:主账号/审批人密码过期时自动弹出模态对话框,可直接在登录界面修改密码,改密成功后自动回填并继续登录(v2.2.5 起)
- 配对规则安全机制:禁用最后启用的配对规则时显示警告弹窗并自动启用 Windows 默认 Tile 作为安全兜底(v2.2.8)
- 原生登录界面:基于 Windows Credential Provider,在 LogonUI 安全桌面层提供原生双输入 Tile
- 双 Tile 设计:登录界面同时显示「双控登录」和「应急登录」两个 Tile
- 服务桥接:Windows Service 后台处理验证,CP 与服务通过 Named Pipe 安全通信
- 失败锁定:可配置的失败次数阈值与锁定时长,防止暴力破解
- 应急覆盖:支持授权管理员在紧急情况下单人登录(需填写原因并记录审计)
- 离线缓存:AD/LDAP 网络不可达时可用本地缓存凭据进行应急验证(需预先配置)
- 审计日志:所有认证事件记录到本地数据库与日志
- 集中管理:管理端 GUI 提供仪表盘、配对规则、应急账号、审计日志、策略配置
经实测验证(Windows 11 25H2 / Build 26100),Windows 在 RDP 会话的锁屏解锁 场景对第三方 Credential Provider 存在未文档化的区别对待:winlogon 对内置凭据提供程序以 LogonType=7 (Unlock) 向 LSA 提交,而第三方提供程序的提交走 RemoteInteractive 路径,在 LSA 入口即失败(0xC000006D / 0xC00000E5,且不产生任何审计事件)。已穷尽序列化侧全部变量(缓冲格式、认证包 Negotiate/Kerberos 直选、LogonId、SID 匹配)均无法规避——与 CP 代码无关。
因此 WinSLA 采用优雅降级策略:
| 场景 | 行为 |
|---|---|
| 本地登录 / 本地 Win+L 解锁 | ✅ 双控(不受限) |
| RDP 全新登录(无既有会话) | ✅ 双控(不受限) |
| RDP 会话解锁(断线重连 / 会话内 Win+L) |
安全建议:双控语义覆盖「进入会话」的全部路径;仅「已建立 RDP 会话的解锁」降级为单因素,属于 Windows 平台限制。需要使用双控登录的敏感区域,建议不要启用远程桌面——物理控制台的登录与解锁均为完整双控。
┌─────────────────────────────────────────────────────────────────┐
│ Windows LogonUI (Secure Desktop) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ DualAuthCP.dll (Credential Provider) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │User A │ │Pass A │ │User B │ │Pass B │ [提交] │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────┬───────────────────────────────────┘ │
└──────────────────────────┼──────────────────────────────────────┘
│ Named Pipe (\\.\pipe\winsla-auth-pipe)
▼
┌─────────────────────────────────────────────────────────────────┐
│ winsla-service.exe (Windows Service) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Pipe Server │ │Dual Validator│ │ Emergency Override │ │
│ │ (tokio async)│→ │(LogonUserW) │ │ 应急覆盖 │ │
│ └──────────────┘ └──────┬───────┘ └──────────────────────┘ │
│ ┌──────┴───────┐ │
│ │ AD Bridge │ │
│ │ (LDAP Bind) │ │
│ └──────┬───────┘ │
└───────────────────────────┼──────────────────────────────────────┘
│ LDAP (389/636) / LogonUserW
▼
┌──────────────────────────┐
│ Active Directory DC │
│ / 本地 SAM 数据库 │
└──────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ winsla-management.exe (管理端 GUI) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Vue 3.5 UI │ │ SQLite 存储 │ │ 服务控制 / 审计查询 │ │
│ │(Element Plus)│ │ (rusqlite) │ │ (axum + WebView2) │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
| 组件 | 技术 | 说明 |
|---|---|---|
| Credential Provider | Rust + windows-rs 0.58 | COM DLL(DualAuthCP.dll),静态链接 CRT,加载到 LogonUI 安全桌面 |
| Windows Service | Rust + tokio + windows-service | 异步 Named Pipe 服务端 + LogonUserW 真实验证 |
| AD 认证桥接 | Rust(ad_bridge) |
LDAP Simple Bind 验证(备用) |
| 管理端后端 | Rust + axum + rusqlite + rust-embed | 内嵌 Web 服务 + SQLite 策略存储 |
| 管理端界面 | wry + tao(WebView2)+ Vue 3.5 + Element Plus + Vite | 原生桌面窗口承载 Vue 前端 |
| 安装部署 | NSIS + PowerShell 脚本 | CP 注册 + 服务安装 + 注册表配置 |
| 安全加固 | zeroize | 敏感字段内存零化 |
- 操作系统:Windows 10/11 x64(已在 25H2 / Build 26100 验证)
- Rust:>= 1.75(stable-x86_64-pc-windows-msvc)
- Node.js:>= 20(管理端前端构建)
- NSIS:3.x(打包安装程序)
- 域环境:目标机器已加入 Active Directory 域,网络可达 DC
git clone https://github.com/YanGLweI/WinSLA.git
cd WinSLA
# 构建所有组件 (Release)
cargo build --release
# 构建管理端前端(输出到 management_app/src-tauri/frontend/dist)
cd management_app
npm install
npm run build
cd ..
# 重新构建以嵌入最新前端资源
cargo build --release构建产物位于 target/release/:
| 文件 | 说明 |
|---|---|
DualAuthCP.dll |
Credential Provider DLL |
winsla-service.exe |
Windows 认证服务 |
winsla-management.exe |
管理端 GUI 应用 |
从 GitHub Releases 下载 WinSLA-v2.2.8-Setup.exe,以管理员身份运行。安装程序自动完成:
- 复制 DLL/EXE 到
C:\Program Files\WinSLA - 注册 Credential Provider CLSID 到 64 位注册表视图
- 安装并启动 Windows 服务(
WinSLA Service) - 创建开始菜单/桌面快捷方式
# 以管理员身份运行
.\scripts\install.ps1
# 卸载
.\scripts\unregister.ps1
⚠️ 重要:Credential Provider 注册后直接影响系统登录流程,请务必在虚拟机中测试,并确保有恢复手段(安全模式 + 删除 CLSID 注册表项,或运行scripts\emergency-recovery.ps1)。
- 安装并重启后,
Win+L锁屏或注销。 - 登录界面出现 WinSLA 双账号 Tile(双控登录)。
- 分别输入主账号 / 审批人的账号与密码,点击提交。
- 两个账号均通过验证后进入桌面;任一失败则显示对应错误。
- 若主账号或审批人密码已过期,提交后自动弹出「修改密码」对话框:输入旧密码与新密码(两次输入需一致)后点击确定,修改成功后新密码自动填入对应账号,再次点击提交即可登录;也可点取消返回登录界面。
- 连续失败达到阈值(默认3次)后账号锁定,需等待锁定时长(默认10分钟)后重试。
sc.exe query "WinSLA Service"
net start "WinSLA Service"
net stop "WinSLA Service"
# 独立模式运行(开发调试,无需注册为服务)
.\target\release\winsla-service.exe.\target\release\winsla-management.exe管理端功能模块:
| 模块 | 说明 |
|---|---|
| 仪表盘 | 服务状态与系统概览 |
| 配对规则 | 配置哪些用户必须成对登录 |
| 应急账号 | 紧急情况下允许单人登录的授权账号 |
| 审计日志 | 查询所有认证事件 |
| 策略配置 | 认证策略参数设置 |
当双账号验证不可用时(如一人不在场),授权管理员可触发应急覆盖:
- 在登录界面选择「应急登录」Tile
- 输入授权管理员凭据
- 填写应急原因(必填)
- 验证通过后允许单人登录,同时记录审计事件(
emergency_override)
应急账号需在管理端「应急账号」模块中预先配置。
如果 winsla-service.exe 因异常退出导致登录失败(双控/应急均无法使用):
-
尝试重启服务(无需注销或重启):
# 以管理员身份运行 net start "WinSLA Service"
-
如果服务无法启动,检查事件查看器 → Windows 日志 → 应用程序中的错误信息。
-
临时禁用 WinSLA 恢复默认登录:
# 方式一:禁用 Credential Provider scripts\emergency-uninstall.ps1 # 方式二:直接删除 CLSID 注册表项 reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Provider" /v "{WinSLA-CLSID}" /f
-
重启系统即可使用原始 AD 账号登录。
# 强制停止服务
taskkill /F /IM winsla-service.exe
# 等待 CP 缓存刷新(约 5-10 秒),或直接重启资源管理器
restart-appxprovisionedpackage –Online -PackageName Microsoft.Windows.ContentDeliveryPlatform_cw5n1h2txyewy
restart-appxprovisionedpackage –Online -PackageName Microsoft.Win32WebViewHost_cw5n1h2txyewy
# 或在任务管理器中结束 "Windows 资源管理器" 进程当域控制器不可达时,启用本地离线缓存验证:
-
管理端策略配置 → 启用「离线缓存"
-
确保应急账号已提前在离线缓存中预置凭据(安装后首次登录需在线完成缓存初始化)
-
登录界面选择「应急登录」,使用已缓存的应急账号凭证登录。
⚠️ 重要提醒:建议在虚拟机中测试所有应急方案!安装前创建系统快照,确保可以快速回滚。
-
进入安全模式:
- 重启电脑,在登录界面出现前按
F8(或使用 Windows 恢复环境) - 选择「带命令提示符的安全模式」
- 重启电脑,在登录界面出现前按
-
删除 CLSID 注册表项:
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Provider" /v "{WinSLA-CLSID}" /t REG_SZ /d "" /f
-
或者运行应急卸载脚本:
powershell -ExecutionPolicy Bypass -File scripts\emergency-recovery.ps1 -
重启系统恢复正常登录。
# PowerShell 脚本卸载
scripts\unregister.ps1
# 或手动卸载
sc.exe delete "WinSLA Service"
Remove-Item "C:\Program Files\WinSLA" -Recurse -Force
reg delete "HKLM\SOFTWARE\WinSLA" /fsc.exe query "WinSLA Service"
Get-Service -Name "WinSLA Service"# 64 位注册表视图
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Provider" /f "WinSLA" /s# 管理端 → 审计日志模块
# 或直接查询数据库
sqlite3 "%ProgramFiles%\WinSLA\data\winsla.db" "SELECT * FROM audit_log ORDER BY created_at DESC LIMIT 20;"WinSLA/
├── Cargo.toml # Workspace 根配置
├── README.md
├── ARCHITECTURE.md # 架构详解
├── LICENSE
├── .gitignore
│
├── cp_provider/ # Credential Provider DLL
│ ├── Cargo.toml
│ ├── build.rs # linker 配置
│ └── src/
│ ├── lib.rs # DllMain + COM 导出
│ ├── provider_com.rs # ICredentialProvider 多接口实现(双 Tile)
│ ├── credential_com.rs # ICredentialProviderCredential + 序列化
│ ├── dual_auth_credential.rs # 双账号凭据状态管理
│ ├── class_factory.rs # COM 类工厂
│ ├── pipe_client.rs # Named Pipe 客户端
│ ├── ui_controls.rs # UI 字段辅助
│ └── com_types.rs # COM/通信类型(AuthMode: Dual/Emergency)
│
├── win_service/ # Windows Service
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs # 服务入口
│ ├── service.rs # 服务注册/安装
│ ├── pipe_server.rs # tokio Named Pipe 服务端(策略路由)
│ ├── audit.rs # 审计日志 + login_attempts 锁定表
│ ├── com_types.rs # 通信协议类型(明文密码+AuthMode)
│ └── auth/
│ ├── mod.rs # AuthError 定义
│ ├── dual_validator.rs # LogonUserW 真实验证
│ ├── ldap_verifier.rs # LDAP 验证器(备用)
│ ├── sspi_verifier.rs # SSPI/NTLM 验证器(备用)
│ └── emergency.rs # 应急覆盖机制
│
├── ad_bridge/ # AD/LDAP 认证库
│ └── src/
│ ├── lib.rs # DomainAuthClient
│ └── ldap.rs # LDAP Simple Bind 实现
│
├── management_app/ # 管理端
│ ├── package.json # Vue 3.5 + Element Plus
│ ├── vite.config.ts
│ ├── src/ # Vue 前端(仪表盘/配对/应急/审计/策略)
│ └── src-tauri/ # Rust 后端(axum + rusqlite + wry/tao)
│ ├── Cargo.toml
│ ├── build.rs # winres 版本信息嵌入
│ ├── main.rs / lib.rs
│ ├── server.rs # axum Web 服务
│ ├── database.rs # SQLite 策略存储
│ ├── commands.rs # 管理命令
│ ├── frontend.rs # 内嵌前端资源
│ ├── gui.rs # WebView2 窗口
│ └── wincred.rs # Windows 凭据交互
│
├── installer/ # NSIS 安装程序
│ └── winsla-installer.nsi # 安装脚本(CP注册+服务+卸载)
│
├── assets/
│ └── winsla.ico
│
└── scripts/ # 部署/运维脚本
├── install.ps1 # 安装
├── unregister.ps1 # 卸载
├── register-cp.ps1 # CP 注册
├── manual-register.ps1 # 手动注册
├── diagnose-cp.ps1 # CP 诊断
├── troubleshoot-tile.ps1 # Tile 不显示排查
├── flush-cp-cache.ps1 # 刷新 CP 缓存
├── emergency-recovery.ps1 # 应急恢复
├── emergency-uninstall.ps1 # 应急卸载
├── create-package.ps1 # 部署包打包
├── full-package.ps1 # 完整打包
├── build_installer.ps1 # NSIS 构建
└── auto-deploy.ps1 # 自动部署
主账号输入账号密码 ──┐
├──→ CP 收集凭据 ──→ Named Pipe ──→ Service
审批人输入账号密码 ──┘ │
▼
┌─────────────────────┐
│ 并行 LogonUserW 验证 │
│ 主账号 → DC/SAM │
│ 审批人 → DC/SAM │
└──────────┬──────────┘
┌──────────────┼──────────────┐
▼ ▼ ▼
两者成功 一方失败 网络超时
│ │ │
▼ ▼ ▼
CredPack 序列化 阻止+提示 降级策略
→ LSA 登录
双账号验证通过后,CP 使用 CredPackAuthenticationBufferW 生成 KERB_INTERACTIVE_LOGON 序列化缓冲区,经 GetSerialization 返回给 LogonUI,由 LSA 完成实际登录。
支持的账号格式:
DOMAIN\user- 域账号(NetBIOS 域名)[email protected]- UPN 格式user- 本地账号(SAM 数据库)
- 密码不落盘:凭据仅在内存中短暂存在,验证后立即清零
- 内存零化:使用
zeroize::Zeroizing包装敏感字段,析构时自动零化内存 - 传输保护:CP → Service 通信使用本机 Named Pipe(SYSTEM↔SYSTEM 隔离)
- 失败锁定:可配置的失败次数阈值与锁定时长,防止暴力破解
- 审计追踪:所有认证事件(成功/失败/应急覆盖/锁定)写入日志与数据库
- 应急管控:应急覆盖需授权账号 + 填写原因
- 最小权限:Service 以 LocalSystem 运行,CP 在安全桌面隔离执行
- 静态 CRT:DLL 静态链接 CRT,避免干净系统缺少 VC++ 运行库导致加载失败
# 安装 NSIS 3.x
winget install NSIS.NSIS
# 构建 Release 二进制
cargo build --release
# 编译 NSIS 安装程序
& "C:\Program Files (x86)\NSIS\makensis.exe" installer\winsla-installer.nsi
# 输出: installer\WinSLA-v2.2.5-Setup.exe# 运行全部单元测试
cargo test --workspace
# 按模块运行
cargo test -p cp_provider
cargo test -p winsla-service
cargo test -p ad-bridge
⚠️ Credential Provider 测试必须在虚拟机中进行:CP 注册后直接影响登录界面,崩溃或配置错误可能导致无法进入桌面。务必先创建快照,并备好scripts\emergency-recovery.ps1恢复手段。
- CP 调试需要 VM(崩溃会导致无法进入桌面)
- LDAP 验证需要网络可达域控制器
- 安装/卸载脚本必须以管理员权限运行
CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION结构体必须严格匹配 SDK 布局(32 字节、无cbSize),否则会导致 LogonUI 崩溃GetSerialization返回凭据必须使用CPGSR_RETURN_CREDENTIAL_FINISHED = 2
- 技术参考:Microsoft Credential Provider Samples (Windows SDK)
- 依赖:windows-rs、tokio、axum、wry