更新记录(仅首版,2026年08月28日 发布)
- 首次发布
从资源管理器启动时读得到设置文件,从任务计划程序启动却说「找不到」。改成服务运行之后,保存的密码解不开了。手边的浏览器明明已经登录,CI 上却又回到登录页面。
碰到这一类问题,比看代码更先要确认的是「以谁的身份、以什么方式在运行」。因为同一台 PC 上的文件和设置,换一个运行用户未必就能照样使用。
本文要回答的问题是:代码一行没改,只是改成了计划任务、服务或 CI 作业,应用为什么就坏了。想从症状入手排查的话,请从下表进入对应的位置。
| 症状 | 首先要对比的东西 | 阅读位置 |
|---|---|---|
| 找不到设置文件或命令 | 运行用户、AppData 的实际路径、用户环境变量 | AppData 与环境变量 |
| 明明写进去的注册表值不见了 | HKCU 指向的那个用户的配置单元 | HKCU |
| 文件读得到,机密信息却解不开 | DPAPI 的作用域与主密钥的归属 | DPAPI |
| 浏览器的登录状态带不过去 | 运行用户、配置文件、受 DPAPI 保护的密钥 | 浏览器配置文件 |
| 已保存的凭据或证书用不了 | 运行账户的保险库与证书存储、任务的登录类型 | 凭据与证书 |
| 以管理员身份运行就没有 Z: 盘 | 提权前后的令牌与登录会话 | UAC 权限提升 |
| 想在迁移前整体检查一遍 | 各运行形态各自依赖什么、需要怎样的安装配置 | 按运行形态的检查清单、排查步骤 |
本文的前提
| 项目 | 内容 |
|---|---|
| 目标读者 | 要把业务应用改成服务、计划任务或 CI 作业的开发者,以及排查「在我这儿明明能跑」的运维人员 |
| 前提环境 | Windows 10/11。验证代码在 PowerShell 5.1 以上运行 |
| 难度 | 中级 |
1. 先说结论
划分 Windows 运行环境的基本单位不是 PC,而是访问令牌和 SID,也就是「以谁的身份在运行」。AppData、HKCU、DPAPI 的密钥、浏览器配置文件、凭据,都属于那个用户的环境。
你以交互式登录的身份备好的设置和登录状态,不会自动被 SYSTEM 或别的服务账户继承。放全机数据的地方是 ProgramData 和 HKLM,就算要共享,也需要设计访问权限。
flowchart TB
accTitle: 同一台 PC 里的两个世界
accDescr: 同一台 PC 只共享 HKLM 与 ProgramData,你的 SID 的世界和另一个 SID 的世界各自拥有独立的 AppData、注册表配置单元和 DPAPI 密钥,隔着用户边界互相看不见
pc["同一台 PC"] --> shared["共享:HKLM・ProgramData"]
pc --> wa["你的 SID 的世界"]
pc --> wb["另一个 SID 的世界(SYSTEM 等)"]
wa --> ra["AppData・HKCU・DPAPI 密钥"]
wb --> rb["另一套 AppData・另一个配置单元・另一组密钥"]
ra -.-|"用户边界:互相看不见"| rb
图 1: 即使是同一台 PC,运行用户不同,AppData、注册表和密钥就是另外一套。
不过,SID 相同也未必就是同一个环境。任务的登录类型、IIS 是否加载配置文件、UAC 提权带来的登录会话差异,也都要确认。同一账户提权并不会让 HKCU 和保险库变成别人的,把会变的边界分开来考虑才是关键。
下面的顺序是:作为前提的运行用户(第 2 章)、五道边界(第 3~7 章)、按运行形态的检查(第 8 章)、设计与排查(第 9 章)。
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 33 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
2. 前提:「以谁的身份运行」决定一切
2.1 要确认的不是用户名,而是 SID 和令牌
Windows 用来识别用户的 ID 不是用户名,而是 SID(安全标识符)。进程持有访问令牌,其中包含运行用户的 SID。在考虑文件的 ACL 判定、注册表的实体、加密密钥时,这个运行主体都是出发点。
第一步用 whoami /user 确认。要看的不是你自己终端里的结果,而是出问题的那个运行环境中的结果。
> whoami /user
用户信息
----------------
用户名 SID
=============== =============================================
desktop\you S-1-5-21-3623811015-3361044348-30300820-1001
C:\Users\you 就是与那个 SID 绑定的用户配置文件的实体。配置文件里有 AppData 等一组文件夹,还有用户注册表配置单元 NTUSER.DAT。它是在首次登录时从默认配置文件复制出来的,所以别人的配置文件,是从与你调教好的环境不同的初始状态开始的。1
flowchart TB
accTitle: 从启动路径到配置文件的流程
accDescr: 不论经由双击、任务计划程序还是服务或 IIS 哪条路径启动,进程都带着访问令牌里的 SID,与该 SID 绑定的那整套用户配置文件就是它的运行环境
e1["双击"] --> tok["进程的令牌(SID)"]
e2["任务计划程序"] --> tok
e3["服务・IIS"] --> tok
tok --> prof["与 SID 绑定的整套配置文件"]
prof -.-> note["SID 不同就是处于初始状态的另一套配置文件"]
图 2: 由哪条路径启动,决定了背着哪个 SID 的配置文件运行。
2.2 服务用的账户,也各有各的环境
Windows 里有一些不需要人登录也能运行的内置账户。它们各自拥有独立的运行环境。2
| 账户 | SID | 配置文件与注册表的引用位置 |
|---|---|---|
| SYSTEM(LocalSystem) | S-1-5-18 |
配置文件在 C:\Windows\System32\config\systemprofile 下。HKCU 关联到默认用户3 |
| LocalService | S-1-5-19 |
在 C:\Windows\ServiceProfiles\LocalService 下。在 HKEY_USERS 里有自己的子项4 |
| NetworkService | S-1-5-20 |
在 C:\Windows\ServiceProfiles\NetworkService 下。和 LocalService 一样,有自己的配置文件和配置单元4 |
IIS AppPool\<名称> |
S-1-5-82-… |
应用程序池专用的标识。默认不加载配置文件5 |
双击、任务计划程序、服务、IIS 这些启动路径的差别,最终就是使用哪个运行主体的环境的差别。不要以为你在自己的交互式登录里备好的设置、密钥和凭据,在那个运行目标里也一样有。
2.3 确认完「同一个用户」之后还要看的三个条件
| 要确认的条件 | 差异显现的例子 |
|---|---|
| 登录类型 | 任务的 S4U 登录不保存密码,访问不了网络和 EFS6 |
| 配置文件的加载 | 在 IIS 中,loadUserProfile 决定了能否使用 AppData 和用户配置单元7 |
| 登录会话与提权 | 即使 SID 相同,提权后的进程也可能看不到网络驱动器89 |
不要确认完 SID 就收手,用这三点来梳理「同一个账户里到底哪里不一样」。任务在第 8 章、IIS 在第 4~5 章、提权在第 7 章详述。
3. 边界 1:AppData ── 同一个环境变量指向别的地方
典型的症状是,一改成计划任务就找不到设置文件了。并不是路径解析失败,而是解析到了另一个用户的正确路径,而那里没有那个文件。
3.1 AppData 的分区,以及运行用户带来的差别
AppData 是用户配置文件下的文件夹。用途分为下面几类。10
| 区域 | 主要用途 |
|---|---|
%APPDATA%(Roaming) |
希望跟着配置文件一起漫游的用户设置 |
%LOCALAPPDATA%(Local) |
机器本地的数据和缓存 |
| AppData\LocalLow | 供低完整性级别的进程使用的数据 |
即使用同样的代码打开 %APPDATA%,引用位置也会随运行用户而变。
| 运行用户 | 查找设置文件的位置示例 |
|---|---|
| 用户 A | C:\Users\a\AppData\Roaming\MyApp |
| SYSTEM | systemprofile 下的 AppData |
用户 A 保存的文件,在 SYSTEM 那一侧并不存在。光看「找不到设置文件」这条错误很难看明白,所以排查时要看的不是环境变量名,而是实际打开的路径。
flowchart TB
accTitle: %APPDATA% 的解析结果随用户而变
accDescr: 同样的代码打开 %APPDATA%,运行用户是你时解析到 C:\Users 下,是 SYSTEM 时解析到 systemprofile 下,而后者并没有你的设置文件
code["同样的代码:打开 %APPDATA%"] --> q{"运行用户是谁?"}
q -->|"你"| a["C:\Users\you\AppData\Roaming"]
q -->|"SYSTEM"| b["systemprofile 下的 AppData"]
b -.-> miss["以为放好了的文件并不在"]
图 3: 环境变量不会撒谎,但解析到哪里由令牌决定。
3.2 PATH 也有按用户区分的部分
系统环境变量是全机共享的,用户环境变量则按用户各自独立。自己加进 PATH 的命令服务找不到时,也要确认这个差别。
保存位置由「这份数据给谁读」来决定。只属于那个用户的设置放进 AppData,全体用户和服务共享的数据放到 %ProgramData% 下,后者要设计 ACL。详细的选法请参阅「Windows应用的数据存储位置怎么选 ── SQLite / JSON / 注册表 / Access 判断表」。
flowchart TB
accTitle: 保存位置由读者决定
accDescr: 只有那个用户会读的数据放进 AppData 或 HKCU,要和全体用户及服务共享的数据放进 ProgramData 或 HKLM,后者伴随 ACL 的设计
q{"这份数据由谁来读"} -->|"只有那个用户"| f1["AppData・HKCU"]
q -->|"全体用户・服务"| f2["ProgramData・HKLM"]
f1 -.-> w1["将来要改成服务的话得重新考虑"]
f2 -.-> w2["设计写入权限和 ACL"]
图 4: 「一改成服务就读不到了」,是设计时跳过了这个分支的结果。
4. 边界 2:HKCU ──「当前用户」随调用方而变
典型的症状是,安装程序写入的许可证信息,服务却找不到。写入的地方和读取的地方即使都叫 HKCU,也未必是同一个实体。
4.1 HKCU 是通往用户配置单元的别名
HKEY_CURRENT_USER(HKCU)不是独立的配置单元,而是会按调用方用户转接到实体的别名。在普通的用户进程中,它指向 HKEY_USERS 下那个 SID 的项。其内容就是登录时加载的 NTUSER.DAT。1
例外是 HKCU\Software\Classes,它的实体是另一个配置单元文件 UsrClass.dat。这个文件放在 %LOCALAPPDATA%\Microsoft\Windows 下。11
LocalSystem 的 HKCU 关联到默认用户(HKEY_USERS\.DEFAULT)。以管理员账户运行的安装程序往 HKCU 写,而 SYSTEM 的服务从 HKCU 读,引用位置就对不上了。3
flowchart TB
accTitle: HKCU 这个别名的实体
accDescr: 应用打开 HKCU 时,在你的进程中会转接到 HKEY_USERS 下你的 SID 项,在 LocalSystem 的进程中则转接到默认用户的项,于是安装程序写下的值就看不见了
app["应用的代码:打开 HKCU"] --> alias["HKCU 是通往实体的别名"]
alias -->|"你的进程"| ha["HKEY_USERS 下你的 SID"]
alias -->|"SYSTEM 的进程"| hd["HKEY_USERS\.DEFAULT"]
hd -.-> gone["以为写进去的值并不存在"]
图 5: 就算用 HKCU 这同一个名字,用户一变,读写的实体也跟着变。
全机范围的设置放进 HKLM。Microsoft 不建议从服务访问 HKCU。需要读取用户的设置时,要先模拟(impersonation)那个用户,然后使用 RegOpenCurrentUser。12
4.2 还要确认配置文件有没有被加载
除了运行账户之外,配置文件是否已加载有时也会成为问题。
IIS 的应用程序池默认是不加载用户配置文件就运行的。启用 loadUserProfile 之后,才能使用配置文件下的 AppData 和配置单元。7 这个设置也关系到下一章 DPAPI 和 ASP.NET Core Data Protection 的密钥保存位置。
任务的「不存储密码」设置和这个是两回事,要作为登录类型的限制来确认。即使指定了同一个用户,S4U 登录也用不了网络和 EFS。具体的配置差别整理在第 8 章。6
5. 边界 3:DPAPI ── 加密是用「用户的密钥」上的锁
AppData 和 HKCU 是「找的地方不对」的问题,而 DPAPI 带来的问题是文件读得到,内容却解不开。把用到已保存密码的应用改成服务后出现 CryptographicException,就是这种例子。
5.1 主密钥的主人不同就解不开
DPAPI(Data Protection API)是 Windows 提供给应用的加密功能。只要调用 CryptProtectData 或 .NET 的 ProtectedData.Protect,应用自己不用在代码里带着加密密钥也能保护数据。13
不过,这并不意味着密钥就不需要了。它用的是由操作系统管理的主密钥。在 CurrentUser 作用域下,随机生成的每个用户的主密钥,会用从登录凭据派生出的密钥加以保护,并保存在配置文件下。14
flowchart TB
accTitle: DPAPI 的密钥链条
accDescr: 随机生成的用户主密钥由从登录凭据派生出的密钥保护,而这把主密钥加密着应用的机密信息,用别人的主密钥解不开同一段密文
pwd["登录凭据"] -->|"用派生密钥保护"| mk["你的主密钥"]
mk -->|"加密"| sec["应用的机密信息(保存的密码等)"]
mk2["别的用户的主密钥"] -.->|"解不开"| sec
mk -.-> loc["保存位置在配置文件下"]
图 6: 不是用凭据直接加密数据,而是用源自凭据的密钥保护主密钥。
下面是一个最小示例:用户 A 加密数据后放到共享位置,再换个用户尝试解密。就算共享了保存位置,CurrentUser 的解密者也不会变。
# 在用户 A 的会话中: 用 CurrentUser 作用域加密并保存到共享位置
Add-Type -AssemblyName System.Security
$bytes = [Text.Encoding]::UTF8.GetBytes("secret")
$enc = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, "CurrentUser")
[Convert]::ToBase64String($enc) | Set-Content C:\ProgramData\demo.bin
# 换个用户(例如用 PsExec 变成 SYSTEM)尝试解密
$enc = [Convert]::FromBase64String((Get-Content C:\ProgramData\demo.bin))
[Security.Cryptography.ProtectedData]::Unprotect($enc, $null, "CurrentUser")
# → CryptographicException: 数据在当前状态下无效
5.2 作用域要从「应该由谁解密」来选
| 作用域 | 解密的单位 | 设计上的注意事项 |
|---|---|---|
CurrentUser |
加密者本人的主密钥 | 换到别的运行账户就解不开 |
LocalMachine |
同一台机器共用的密钥 | 同一台机器上的任意进程都能解密,所以要用文件 ACL 收紧能读到密文的对象 |
服务和交互用户都要读的机密信息,一开始就该把 LocalMachine 和文件 ACL 组合起来设计,或者用加密者本人的账户运行。不要只为了消掉解密错误而改作用域,而要决定允许谁来解密。在共用终端上,还要注意按机器保护的范围过宽所带来的风险。15
flowchart TB
accTitle: DPAPI 作用域的选法
accDescr: 应该能解密的主体只有那个用户就选 CurrentUser 作用域,是同一台 PC 上的多个运行主体就选 LocalMachine 作用域,后者用文件 ACL 收紧读者
q{"应该由谁来解密"} -->|"只有那个用户"| cu["CurrentUser 作用域"]
q -->|"同一台 PC 的多个运行主体"| lm["LocalMachine 作用域"]
cu -.-> r1["换个用户运行就解密失败"]
lm -.-> r2["读者用文件 ACL 收紧"]
图 7: 作用域不是挑「碰巧能跑的那个」,而是作为解密者的设计来选。
5.3 密码的「更改」和「重置」不是一回事
保护主密钥的那把密钥依赖登录凭据。用户自己更改密码时,主密钥会被源自新密码的密钥重新保护,解密能力得以延续。
另一方面,管理员重置本地账户的密码时,可能导致过去的密文解不开。就算账户相同,也需要确认凭据是以哪种方式变更的。14
5.4 用 ASP.NET Core 时也要确认密钥的保存位置
即使没有直接调用 DPAPI,也可能依赖这道边界。ASP.NET Core Data Protection 会把用于保护 Cookie 认证等的密钥环,保存到与环境相应的位置。16
| 环境 | 密钥的存放位置与影响 |
|---|---|
| 用户配置文件可用 | %LOCALAPPDATA%\ASP.NET\DataProtection-Keys。在 Windows 上用 DPAPI 加密 |
| 配置文件不可用,且托管在 IIS 上 | 回退到为工作进程账户设置了 ACL 的 HKLM 注册表下 |
| 两者都不符合 | 变成仅限进程内的临时密钥。一重启就丢失密钥,认证 Cookie 等受保护的数据随之失效 |
要把 loadUserProfile、setProfileEnvironment 和托管形态成套确认,弄清密钥会被放到哪里的配置。重要的不只是「能启动」,还有重启之后能否继续用同一把密钥。
作用域的选择以及与 Credential Manager 的分工,在「Windows 应用的敏感信息存储 - 用 DPAPI 摆脱明文配置」中有详细讨论。
6. 边界 4:浏览器配置文件 ──「已登录」是那个用户的私有物
手边明明已经登录,在 CI 上启动浏览器却回到了登录页面。把配置文件的保存位置和加密密钥分开看,这个问题就理清楚了。
6.1 保存位置和密钥,是两道边界
Chrome、Edge 等 Chromium 系浏览器的配置文件,默认位于 %LOCALAPPDATA% 下的 User Data 文件夹。历史记录、Cookie、扩展、保存的密码等,都属于那个 Windows 用户的环境。17
Cookie 和保存的密码由配置文件内的加密密钥加密,而这把密钥本身受 DPAPI 保护。因此就算把文件夹复制到别的用户或别的机器,密钥也对不上,解不开。
近年的 Chrome 还叠上了 App-Bound Encryption(应用绑定加密)。它把密钥的解密改为经由 SYSTEM 权限的服务进行,不只验证用户,还验证发出请求的应用的标识。18 这说的是 Chromium 系;拥有自家一套配置文件保护机制的 Firefox 等则另当别论。
| 边界 | 在自动化那一侧会发生什么 |
|---|---|
| AppData 的边界 | CI 代理或服务所用的用户,没有你平时用的那套配置文件 |
| DPAPI 的边界 | 就算复制了文件夹,也解不开受保护的密钥 |
flowchart TB
accTitle: 支撑浏览器登录状态的两道边界
accDescr: 浏览器配置文件位于 LOCALAPPDATA 下属于边界 1,Cookie 的加密密钥受 DPAPI 保护属于边界 3,所以配置文件和密钥都不会被另一个用户继承
prof["浏览器配置文件"] --> loc["存放位置在 LOCALAPPDATA 下"]
prof --> key["Cookie 加密密钥受 DPAPI 保护"]
loc -.-> ci["CI 的运行用户拿到的是空的另一套配置文件"]
key -.-> copy["复制文件夹带不走"]
图 8: 「把已登录状态带过去」会同时被边界 1 和边界 3 挡住。
6.2 自动化中要明确写出登录状态是怎么造出来的
Selenium 和 Playwright 默认会新建一个用完即弃的临时配置文件来启动。因此,即使是同一个用户,平时浏览器的登录状态也不会被自动拿来用。
就算指定了持久化配置文件的目录,一旦要挪到别的用户的 CI 或服务上,上一节的保存位置和密钥问题依然在。对策不是复制已登录的配置文件,而是下面两者之一。
- 把测试用账户的登录步骤写成代码。
- 用自动化工具的 storage state 机制,明确地保存和恢复 Cookie 等。
别的用户没法只靠复制就用上 Cookie,这不是不方便,而是安全上的一道边界。自动化的设计要以这道边界的存在为前提。
7. 边界 5:凭据与证书 ── 保险库按用户各自独立
用 cmdkey 保存的凭据,任务运行时没被用上,于是认证出错。这里同样不是看「保存在这台 PC 上了」,而是要确认「是以哪个用户保存的」。
7.1 凭据管理器是按运行用户区分的保险库
用 cmdkey /list 能查看的凭据管理器,是按用户区分的保险库。19 保存的凭据虽然在磁盘上,但受 DPAPI 保护,由以那个用户身份运行的程序使用。20
保险库里会放着文件服务器和网络驱动器的保存凭据、git-credential-manager 保存的 Git 令牌、RDP 连接的保存密码,以及使用 Credential API 的应用的机密信息等。
即使在你交互式登录的保险库里有,服务或任务的运行账户的保险库里也没有。需要的凭据,要准备一套以运行账户自身的上下文写入的安装步骤。只有手边的 git pull 能成功时,同样要确认用的是哪个保险库。
不过,S4U 配置的任务,光往里塞凭据是解决不了的。要先确认登录类型,再考虑改成存储密码的配置或切换到服务账户。步骤的先后顺序在第 8 章整理。
flowchart TB
accTitle: 凭据的保险库按用户各自独立
accDescr: 你的保险库里放着 git 的凭据、文件服务器的保存凭据和 RDP 的密码,而服务运行用户的保险库在没有写入凭据之前是空的,这正是认证出错的真相
you["你的保险库"] --> g["git 的凭据"]
you --> n["文件服务器的保存凭据"]
you --> r["RDP 的保存密码"]
svc["服务运行用户的保险库"] -.-> empty["没写入就是空的=认证出错的真相"]
图 9: 「手边认证过得去」只是「你的保险库用得上」的简写。
7.2 证书要把存储位置和私钥的权限分开确认
| 存储 | 边界与用途 |
|---|---|
Cert:\CurrentUser |
按用户区分的存储。放进这里的客户端证书,其私钥按用户为单位受保护 |
Cert:\LocalMachine |
全机范围的存储。放服务用的证书,并用私钥的 ACL 允许服务账户读取 |
服务要用的证书,基本上要把 LocalMachine 存储和私钥的 ACL 成套设计。21 用户存储的实体在 HKCU\Software\Microsoft\SystemCertificates 下,所以它位于 HKCU 这道边界的里侧。22
详情请参阅「Windows证书存储实务指南 ── 应该放入用户存储还是计算机存储」。
7.3 UAC 提权要把「同一账户」和「不同账户」分开看
在启用了 UAC 的管理员用户登录时,会创建两个相互链接的令牌:受限权限的标准令牌,和完整的管理员令牌。8
网络驱动器的映射是按登录会话保存的。资源管理器里看得到 Z:,以管理员身份运行的工具里却可能看不到。9
| 运行方式 | 会变的和不会变的 |
|---|---|
| 保持同一账户做 UAC 提权 | SID 相同,HKCU 和凭据保险库也照旧。按登录会话保存的驱动器映射可能就看不见了 |
| 用别的管理员账户的凭据提权/RunAs | SID 也变了。HKCU 和保险库都变成那个管理员的。AppData、密钥、浏览器状态也都受另一个用户边界的影响 |
不要把「一提权就跑不动了」全都归结成用户变了,先确认是不是同一个账户。
flowchart TB
accTitle: UAC 提权造成的同一用户内部的分裂
accDescr: 启用 UAC 的管理员登录会造出标准令牌和提升令牌两个令牌,在标准令牌一侧映射的网络驱动器,从提升令牌的进程里看不到
logon["管理员用户的登录"] --> t1["标准令牌"]
logon --> t2["提升令牌"]
t1 --> d1["在这里映射的 Z: 盘"]
t2 -.->|"另一个登录会话"| d2["提权后的工具看不到 Z:"]
图 10: 提权造出的是「同一个用户的另一个世界」。边界不只是 SID 的事。
8. 按运行形态的检查清单
8.1 任务要先看运行账户,再看登录类型
任务计划程序即使指定了同一个用户,能用什么也会随登录类型而变。6
| 登录类型 | 要确认的事 |
|---|---|
| 交互式令牌(InteractiveToken) | 在已登录的会话中运行的配置 |
| 存储密码(Password) | 非交互也能使用凭据的配置 |
| S4U(不存储密码) | 不保存密码,访问不了网络资源和加密文件(EFS) |
用不了保存凭据的任务,要先确认是不是 S4U 的限制。不要把「保持 S4U 再往保险库里加凭据」当成对策。先考虑存储密码的配置或切换到服务账户,在此之上如有必要,再写入运行账户的保险库。
设置的细节在「任务计划程序的任务不执行、以 0x1 结束 ── 原因排查与安全的运维设计」中讨论。
flowchart TB
accTitle: 任务的登录类型这第二根轴
accDescr: 任务计划程序的运行配置带有交互式令牌、存储密码、S4U 这几种登录类型,S4U 下不保存密码,访问不了网络和 EFS
task["任务的运行配置"] --> lt{"登录类型"}
lt -->|"交互式令牌"| it["在已登录的会话中运行"]
lt -->|"存储密码"| pw["非交互也能使用凭据"]
lt -->|"S4U(不存储密码)"| s4u["够不着网络和 EFS"]
图 11: 就算是同一个运行用户,登录的方式不同,能用的东西也不同。
8.2 更换运行目标之前的检查表
| 运行形态 | 运行主体 | 特别要检查的边界与症状 |
|---|---|---|
| 任务计划程序 | 注册时指定的账户 | 边界 1・2・3・5。确认 AppData 和 HKCU 的引用位置、DPAPI 的解密、保险库。S4U 下用不了网络和 EFS |
| Windows 服务 | SYSTEM、LocalService、NetworkService、服务账户 | 边界 1~5。SYSTEM 用的是 systemprofile 和默认用户的 HKCU,LocalService/NetworkService 用的是 ServiceProfiles 下各自的环境。开发者的密钥、保险库、浏览器状态都不会被继承 |
| IIS 应用程序池 | IIS AppPool\<名称> 等应用程序池专有的标识 |
边界 1・2・3・5。确认默认未加载配置文件、Data Protection 的密钥存放处、对 CurrentUser 证书私钥的访问 |
| RunAs/UAC 提权 | 指定的用户,或同一用户的另一个令牌 | 同一账户的话 SID、HKCU、保险库都相同,驱动器映射则受会话差异影响。不同账户的话边界 1~5 全都要检查 |
| CI/CD 代理 | 代理的服务用户。很多情况下从未交互式登录过 | 边界 1~5。确认有没有依赖浏览器的配置文件和登录状态、Git 的凭据、开发者的 HKCU 设置或 DPAPI 保护的数据 |
| RDP/共用服务器 | 同一用户的多重会话,或多个用户 | 同一用户的多重会话共享 AppData 和 HKCU,所以要注意写入冲突。不同用户的话在边界 1~5 上分开 |
最后一行是反方向的提醒。同一个用户就算有多个 RDP 会话,AppData 和 HKCU 也不会按会话各自独立。这里的问题不是「看不见」,而是「共享同一份东西并往里写」。
9. 设计与排障的指引
9.1 保存位置和安装步骤,由使用它的主体来决定
| 对象 | 设计的基本原则 |
|---|---|
| 用户专属的设置 | 放进 AppData、HKCU |
| 全体用户和服务共享的数据与设置 | 放进 ProgramData、HKLM,并设计写入权限和 ACL |
| 机密信息 | DPAPI 的作用域从「应该由谁解密」来选。用 LocalMachine 时连文件 ACL 一起设计 |
| 服务用的凭据与证书 | 把凭据写入运行账户的保险库、把证书放进 LocalMachine 证书存储并设置私钥的 ACL,都要写进安装步骤 |
如果将来打算改成服务,那么在决定保存位置的阶段,就要把那个运行主体也算进读者里。「凑巧开发者的环境里有就拿来用」这种依赖,原则上不要带进运维。
9.2 排查从运行主体出发,走向实际的引用位置
flowchart TB
accTitle: 用户边界故障的排查步骤
accDescr: 用 whoami 确认运行用户,用 Process Explorer 看令牌,用 Process Monitor 定位实际读取的路径和注册表项,必要时用 psexec 从对方的世界里复现
s1["用 whoami /all 确认运行用户"] --> s2["用 Process Explorer 确认令牌"]
s2 --> s3["用 ProcMon 定位实际的路径与项"]
s3 --> s4["用 psexec 从对方的世界复现"]
s3 -.-> hint["与预期不符的配置文件路径就是线索"]
图 12: 先确认运行用户,看清实际的路径和注册表项,再在目标运行主体上复现。
| 步骤 | 确认方法 | 要看的点 |
|---|---|---|
| 1. 确认运行用户 | 启动后立刻把 whoami /all 输出到日志 |
出问题的那个任务或服务的运行主体,是否和手边相同 |
| 2. 确认令牌 | Process Explorer | 进程的用户和会话。是另一个用户,还是同一用户的另一个运行上下文 |
| 3. 看实际的引用位置 | Process Monitor | 打开的文件路径和注册表项。PATH NOT FOUND 里有没有出现和预想不同的配置文件 |
| 4. 在目标运行主体上复现 | 是 SYSTEM 的话用 psexec -s -i cmd |
在 SYSTEM 的 shell 里做同样的操作,确认与自己的交互环境有什么差别 |
设置找不到时回到 AppData 和 HKCU,文件读得到却解不开时回到 DPAPI,只有认证失败时回到保险库、证书和登录类型。不要只凭错误消息下判断,把「谁、在哪里、用哪把密钥或哪份凭据」对齐才是捷径。
10. 总结
「在我这儿能跑」的意思是「以那个用户、那套配置文件、那些密钥和那个保险库能跑」。即使在同一台 PC 上启动同一个 .exe,改成计划任务、服务或 CI 作业也必须当作运行环境的迁移来对待。
AppData 和用户环境变量引用的是运行用户的配置文件,HKCU 也朝着那个用户的配置单元。DPAPI 的 CurrentUser 作用域依赖用户的主密钥,浏览器的登录状态和凭据管理器同样受这道边界的影响。
再往下,即使 SID 相同,登录类型、配置文件加载、UAC 提权带来的会话差异依然存在。反过来,同一用户的多个会话共享 AppData 和 HKCU 时,要考虑写入冲突。
设计评审时,请确认下面这个问题。
这段代码,不论以哪个用户身份运行都是正确的吗?
把需要的保存位置、密钥、凭据,明确地为实际的运行主体准备好。只要在迁移前确认这条原则,就能在设计阶段减少「代码一行没改却坏了」这类事故。
相关文章
- Windows 应用的敏感信息存储 - 用 DPAPI 摆脱明文配置
- Windows应用的数据存储位置怎么选 ── SQLite / JSON / 注册表 / Access 判断表
- 任务计划程序的任务不执行、以 0x1 结束 ── 原因排查与安全的运维设计
- Windows 服务账户选定 ── LocalSystem、虚拟账户与 gMSA
- Windows证书存储实务指南 ── 应该放入用户存储还是计算机存储
相关的咨询领域
小村软件有限公司承接业务应用改成服务、计划任务时的运行环境设计,「在我这儿明明能跑」这类故障的调查,以及 Windows 应用机密信息管理的设计。
参考链接
-
Microsoft Learn, About User Profiles. 关于用户配置文件在首次登录时创建,以及配置文件由注册表配置单元 NTUSER.DAT(登录时加载并映射到 HKEY_CURRENT_USER)和文件系统上的一组配置文件文件夹构成等内容。 ↩ ↩2
-
Microsoft Learn, Local accounts. 关于 SYSTEM(S-1-5-18)、NETWORK SERVICE(S-1-5-20)、LOCAL SERVICE(S-1-5-19)是用于运行操作系统和服务的默认本地系统账户等内容。 ↩
-
Microsoft Learn, LocalSystem Account. 关于 LocalSystem 的令牌包含 NT AUTHORITY\SYSTEM、它不与任何已登录的用户账户关联、因而 HKEY_CURRENT_USER 关联到默认用户,以及要访问其他用户的配置文件必须模拟那个用户等内容。 ↩ ↩2
-
Microsoft Learn, LocalService Account. 关于 LocalService 账户在 HKEY_USERS 下拥有自己的子项,以及 HKEY_CURRENT_USER 关联到 LocalService 账户等内容。NetworkService 同理(NetworkService Account)。 ↩ ↩2
-
Microsoft Learn, Application Pool Identities. 关于应用程序池以池专有的标识运行、IIS 默认不加载 Windows 用户配置文件,以及把 LoadUserProfile 属性设为 true 就能加载配置文件等内容。 ↩
-
Microsoft Learn, logonType Simple Type. 关于任务的登录类型有 S4U、Password、InteractiveToken,以及 S4U 登录不保存密码、既访问不了网络也访问不了加密文件等内容。 ↩ ↩2 ↩3
-
Microsoft Learn, Process Model Settings for an Application Pool. 关于应用程序池的 processModel 有 loadUserProfile 属性和 setProfileEnvironment 属性,用于控制工作进程是否加载用户配置文件等内容。 ↩ ↩2
-
Microsoft Learn, How User Account Control works. 关于启用 UAC 时,管理员用户登录会创建标准用户令牌和完整管理员访问令牌这两个相互链接的令牌等内容。 ↩ ↩2
-
Microsoft Learn, Mapped drives are not available from an elevated prompt. 关于以标准令牌登录的会话中映射的网络驱动器无法从提权后的进程使用,以及其背景是两个相互链接的登录会话各自持有驱动器映射等内容。 ↩ ↩2
-
Microsoft Learn, KNOWNFOLDERID. 关于 FOLDERID_RoamingAppData(%APPDATA%)、FOLDERID_LocalAppData(%LOCALAPPDATA%)、FOLDERID_LocalAppDataLow 被定义为按用户区分的已知文件夹等内容。 ↩
-
Microsoft Learn, Error occurs during desktop setup and desktop location is unavailable. 关于用户配置文件拥有 NTUSER.DAT 和 UsrClass.dat 两个配置单元文件,以及 UsrClass.dat 放在 AppData\Local\Microsoft\Windows 下等内容。 ↩
-
Microsoft Learn, Services and the Registry. 关于服务不应访问 HKEY_CURRENT_USER 或 HKEY_CLASSES_ROOT,以及模拟用户时应使用 RegOpenCurrentUser 函数等内容。 ↩
-
Microsoft Learn, CryptProtectData function. 关于 CryptProtectData 通常用与已登录用户关联的会话密钥保护数据、以同一用户解密为前提,以及可用 CRYPTPROTECT_LOCAL_MACHINE 标志切换成按机器保护等内容。 ↩
-
Microsoft Learn, Windows Data Protection. 关于 DPAPI 用从用户密码派生的密钥加密随机生成的主密钥加以保护、主密钥保存在用户配置文件下,以及更改密码时主密钥会被重新保护等内容。 ↩ ↩2
-
Microsoft Learn, ProtectedData Class. 关于 DataProtectionScope.CurrentUser 只允许保护数据的那个用户解密,而 LocalMachine 允许同一台机器上的任意进程解密等内容。 ↩
-
Microsoft Learn, Data Protection key management and lifetime in ASP.NET Core. 关于用户配置文件可用时密钥保存在 %LOCALAPPDATA%\ASP.NET\DataProtection-Keys 并在 Windows 上用 DPAPI 加密、以 IIS 托管而配置文件不可用时回退到为工作进程账户设置了 ACL 的 HKLM 注册表、哪个条件都不满足时密钥随进程结束而丢失导致受保护的载荷无法解密,以及 setProfileEnvironment 属性相关等内容。 ↩
-
Chromium project, User Data Directory. 关于 Windows 上 Chrome 的 User Data 目录默认是 %LOCALAPPDATA%\Google\Chrome\User Data,以及配置文件(历史记录、书签、Cookie 等)放在它下面等内容。 ↩
-
Google Security Blog, Improving the security of Chrome cookies on Windows. 关于 Chrome 在 Windows 上一直使用 DPAPI 加密 Cookie 等数据,以及借助 App-Bound Encryption 改为经由 SYSTEM 权限的服务保护密钥并验证请求解密的应用的标识等内容。 ↩
-
Microsoft Learn, cmdkey. 关于用 cmdkey 命令可以列出、创建和删除已保存的用户名和密码(凭据)等内容。 ↩
-
Microsoft Learn, Cached and Stored Credentials Technical Overview. 关于保存在凭据管理器中的凭据放在磁盘上并受 DPAPI 保护,以及以那个用户身份运行的程序可以访问这个存储中的凭据等内容。 ↩
-
Microsoft Learn, Local Machine and Current User Certificate Stores. 关于证书存储有本地计算机存储(全机范围)和当前用户存储(按用户区分)两种等内容。 ↩
-
Microsoft Learn, System Store Locations. 关于 CERT_SYSTEM_STORE_CURRENT_USER 的系统存储放在注册表的 HKEY_CURRENT_USER\Software\Microsoft\SystemCertificates 下等内容。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
PowerShell 中凭据的安全处理方式 ── 把明文密码逐出脚本
本文整理将 PowerShell 脚本中的明文密码迁移到安全存储的步骤。讲解 SecureString 的真实面貌与局限、Export-Clixml 基于 DPAPI 保存的原理,直至 SecretManagement/SecretStore 的适用场景。
注册表的32位/64位重定向与虚拟化陷阱 ── Wow6432Node与「写入的值找不到了」问题
从实务角度梳理32位应用写入HKLM\Software时被重定向到Wow6432Node的机制、UAC虚拟化转发到VirtualStore的触发条件、COM注册按位数分离带来的实际危害,直至用RegistryView、reg.exe /reg:64正确读写的方法。
Power Automate 与 PowerShell+任务计划程序的使用场景划分 ── 不混用自动化工具,物尽其用地对接
面向 PowerShell+任务计划程序的夜间批处理与 Power Automate 流程开始在企业内部混用的中小企业信息系统部门,整理两者擅长领域的差异、该用哪种工具制作的判断表、通过 SharePoint 实现松耦合对接的联动模式,以及许可证与维护方面的注意事项。
PowerShell 的错误处理与重试设计 ── 从 try/catch 失效的陷阱到 exit code、重试的实务定式
本文从实务角度整理 PowerShell 终止错误与非终止错误的区别、try/catch 失效陷阱与 -ErrorAction Stop 的应对定式、用 $LASTEXITCODE 判断成败、exit code 设计,直至指数退避重试的完整流程。
Windows应用的数据存储位置怎么选 ── SQLite / JSON / 注册表 / Access 判断表
Windows桌面应用的数据该存在哪里、用什么格式保存?本文整理AppData/ProgramData的使用区分,以及SQLite、JSON文件、注册表、Access(.accdb)各自的优势与陷阱,并附判断表,从实务角度讲解防止数据损坏与位数问题等注意事项。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
常见问题
汇总了咨询这一主题时常见的问题。
- 从资源管理器启动能跑的应用,从任务计划程序启动却找不到设置文件。这是为什么?
- 因为 %APPDATA% 之类的环境变量会解析到「正在运行的那个用户」的配置文件。把任务改成以 SYSTEM 或别的账户运行时,环境变量就指向另一个配置文件(SYSTEM 的话是 systemprofile 下),而你保存的设置文件并不在那里。请确认任务的运行账户,应当共享的数据放到 ProgramData 下才是长久的做法。
- 用 ProtectedData.Protect 保存的密码,改成服务运行之后就解不开了。
- CurrentUser 作用域的 DPAPI 依赖加密者本人的主密钥。服务的运行账户不同,主密钥也就是另一把,解密会以 CryptographicException 失败。服务和交互用户都要读的机密信息,请改用 LocalMachine 作用域加文件 ACL 来重新设计,或者用加密者本人的账户运行服务。
- 以 SYSTEM 运行的进程读取 HKCU 会怎样?
- 在 LocalSystem 的进程中,HKEY_CURRENT_USER 会关联到默认用户(HKEY_USERS\.DEFAULT),所以交互用户写进 HKCU 的值是看不到的。全机共享的设置请放到 HKLM;实在需要读取用户设置时,请先模拟那个用户,然后使用 RegOpenCurrentUser。
- 能把已登录的 Chrome/Edge 配置文件复制到 CI 机器上使用吗?
- 基本上不行。Chrome、Edge 等 Chromium 系浏览器的配置文件位于该用户的 %LOCALAPPDATA% 下,Cookie 和保存密码的加密密钥受那个用户的 DPAPI 保护。把文件夹复制到别的用户或别的机器上,密钥对不上,也就解不开(Firefox 等拥有自己一套配置文件保护机制的浏览器情况不同)。自动化中请把测试用账户的登录步骤写成代码,或者使用自动化工具的 storage state 机制。
- 用 cmdkey 保存的凭据,在任务计划程序运行时没有被用上。
- 因为凭据管理器的保险库按用户各自独立,而你保存进去的是交互式登录的那个你自己的保险库。此外,配置成「不存储密码」(S4U)的任务是在没有网络凭据的情况下运行的,所以只要还是 S4U,往保险库里塞凭据也用不上。请先考虑改成「存储密码」的配置或切换到服务账户,在此之上如有必要,再把凭据写入运行账户自身的保险库。