Windows 应用程序不要把敏感信息以明文存进配置文件的最佳实践
· 小村 豪 · Windows 开发, 信息安全, DPAPI, C# / .NET, Win32
前一篇 「Windows 应用开发中为了守住最底限安全底线的检查清单」 里,
写下了「不要把敏感信息放进源代码或明文配置」「Win32 / .NET 的话就用 DPAPI / ProtectedData」这条最底限。
这次把其中 「用 DPAPI 让它至少比明文好一点」 这一块再往深挖一点。
适用对象是下列这类 Windows 应用。
- WPF / WinForms / WinUI 的桌面应用
- C# / .NET 的 Windows 客户端
- 会忍不住把连接凭证或 API Token 存进本地配置文件的应用
这篇要谈的是,「对于不得不存在本地的敏感信息,至少不要放任它在 appsettings.json 里以明文摆着」 的现实设计。
这不是「任何攻击者都打不穿的完全防御」这种话题。那种说法一旦过头,信息安全会瞬间变成怪谈。
1. 先说结论
先把结论写出来,实务上按下列顺序思考会比较清楚。
- 从根本上不要让客户端持有长期敏感信息
- 优先采用 Windows 身份验证、集成身份验证、用户交互式登录、服务端密钥管理
- 如果确实必须存在本地,就不要以明文存放
- 在 Windows 上首先考虑 DPAPI /
ProtectedData
- 在 Windows 上首先考虑 DPAPI /
- 一般桌面应用基本用
DataProtectionScope.CurrentUserLocalMachine的适用范围相当窄
- DPAPI 不是用来挡「终端已被完全攻陷」这种情况的
- 以相同用户权限运行的代码,原则上能解密该用户能解密的东西
而这篇文章最重要的论点是下面这个。
「反正密钥总得存在某个地方,那不管是明文还是 DPAPI,在安全上不都一样吗?」
这句话一半对,但结论错了。
- 自己做 AES 加密、密钥放在同一个应用或同一个配置文件里 的话,其实相当接近明文
- 但 DPAPI 是把密钥管理交给 OS,并把能解密的主体绑定到「那个 Windows 用户」或「那台电脑」上
- 结果,对于 配置文件单独外泄、被带到别的电脑、误发邮件、备份外泄、不小心混进代码仓库 这类事故的防御力会有很大差异
换言之,
只看「密钥放在某处」这个抽象说法的话是一样,但
「谁,在什么情境下,能多简单地用得到」这件事完全不同。
把「把钥匙放在门口地垫下」和「到管理室本人认证之后才能拿到钥匙」当成同一件事,有点粗暴。
2. 为什么明文配置很危险
明文保存危险的理由,与其说是密码学层面的问题,更多是很「土」的现实。实务上大致是通过以下几种途径漏出去的。
- 配置文件就这样被提交进 Git
- 故障排查用的 ZIP 里整个配置文件被打包进去
- 反馈问题时被要求附上配置文件
- 备份或文件分享让第三者读到
- 日志里直接打印出连接字符串或 Token
- 离职员工或其他用户能读到同一台电脑上的文件
明文最大的问题是 「被读到的那一瞬间,敏感信息就已经结束了」。
- 文件被打开就结束
- 被复制就结束
- 被附在邮件里就结束
- 一旦留在代码仓库里,就要半永久地背着它
攻击者根本不需要很高深的技术。
「用文本编辑器就能打开」这件事本身就已经相当弱。
3. 对「反正密钥总会存在某个地方,那不是一样吗?」的回答
这个疑问很合理。
而且如果这里回答得太草率,整篇安全文章会立刻变得模糊不清。
先说结论:以「某处一定要有密钥」这层意思来说是 yes,但要说「所以都一样」就是 no。
3.1. 哪里一样、哪里不一样
的确,加密最终总得有某个 root of trust。
敏感信息不会从宇宙某处凭空冒出来。这一点没有商量的余地。
但安全性上的差异,是由下列 3 点决定的。
- 密钥是不是应用自己在持有
- 密钥绑定在哪个主体上
- 仅凭文件被拿走,是否就能解开
把这些差异粗略整理成表,大致如下。
| 方式 | 配置文件被读到 | 只有文件被带到别的电脑 | 被同一台电脑的其他用户读到 | 以相同用户权限运行的代码 |
|---|---|---|---|---|
| 明文 | 当场外泄 | 直接外泄 | 直接外泄 | 当然读得到 |
| 自制加密 + 密钥放在同一配置/可执行文件 | 相当容易泄露 | 相当容易泄露 | 相当容易泄露 | 当然解得开 |
DPAPI + CurrentUser |
文件单独无法立即读取 | 通常难以解密 | 通常难以解密 | 可以解密 |
DPAPI + LocalMachine |
文件单独无法立即读取 | 在别的电脑上通常难以解密 | 同一台电脑上广泛可解密 | 可以解密 |
这里重要的是,DPAPI 把「能读文件」和「能用敏感信息」分开了。
在明文场景下,这两件事是一样的。
文件读得到,敏感信息也就读得到。
但在 DPAPI 下,至少 CurrentUser 的情况下,必须满足:
- 以该 Windows 用户身份
- 在该 Windows 的上下文中
- 通过 OS 的保护机制
才能解密。
这个差距在事故现场会相当大。
3.2. 「但同一个用户的话还是解得开吧?」——没错
这一点要老老实实地写出来。
以相同用户权限运行的代码,原则上能解开该用户可解的内容。
也就是说,DPAPI 并不是为下列场景设计的。
- 终端已经被恶意软件攻陷
- 攻击者能以该用户身份运行代码
- 终端管理员级别已被完全接管
在这种状态下,既然应用本身也能解密,攻击者的代码自然也解得开。
在这个场景下说「可是我加密了」其实不太能让人安心。
DPAPI 比较能发挥作用的,是「文件外泄、放错位置、离线带走、被其他用户看到」这些方面。
这里如果搞错,会同时发生:
- 把能守住的东西低估而不用
- 把守不住的东西高估而安心
两种情况。两边都会在不知不觉中变得很危险。
3.3. 所以到底好在哪
DPAPI 的好处一句话讲完就是:
「能把敏感信息本身,从配置文件的可读性中切开。」
举例来说,下列事故在明文和 DPAPI 之间就会出现差异。
- 用户把配置文件发给客服
- 排查问题用的 ZIP 里塞了配置文件
- 备份中只有配置文件外泄
- 被复制到共享文件夹
- 开发者只看得到密文而看不到内容
这些是相当现实的好处。
不需要把攻击者想象成电影里的超人,也能把日常事故的影响范围缩小。
4. DPAPI 恰到好处的理由
在 Windows 上处理本地保存的敏感信息时,DPAPI 在实务上恰到好处的理由如下。
4.1. 可以把密钥管理交给 OS
自己生成 AES 密钥、保存、赋予权限、轮换、评估泄漏影响,还要加上篡改检测。
比想象中更重。而且做得不认真的话,大概就是把密钥放在同一个地方了事。
用 DPAPI 就能 把「加密密钥怎么生成、放在哪里」这个问题,从应用实现中切开。
从这个意义上讲,与其把 DPAPI 看成
「选择加密算法的 API」,更接近本质的说法是「把密钥管理委托给 OS 的 API」。
4.2. 能把解密主体绑定在 Windows 用户或电脑上
一般桌面应用,绝大多数场景都可以选 CurrentUser。
- 以该用户已登录
- 以该用户上下文在运行
为前提解密。
这使得 只把密文复制到别的电脑也很难直接使用 这种特性变得可实现。
4.3. 比较容易顺带具备篡改检测
自制加密常见的情况是,
「反正我做了 AES 加密就结束了」然后 忘了加上篡改检测。
DPAPI 对加密数据本身就具备完整性保护,所以
偷偷篡改密文的检测 也顺带交给 OS 的机制,实务上是一个好处。
4.4. 从 C# / .NET 自然就能使用
C# 直接用 System.Security.Cryptography.ProtectedData 就可以。
不用再额外挂载别的库,对 Windows 专用程序来说帮助不小。
5. DPAPI 能守住、守不住的东西
这里最好明确区分,比较安全。
5.1. 变得比较能守住的
DPAPI 至少在下列场景是有效的。
- 配置文件明文外泄
- 把文件带到别的电脑
- 同一台电脑上其他用户尝试读取(以
CurrentUser为前提) - 以备份或附件形式外泄
- 开发 / 运维现场「不小心就被读到」的状态
5.2. 守不住或很弱的
另一方面,在下列场景就不要太相信它。
- 以相同用户权限运行的攻击代码
- 终端本身完全沦陷
- 被管理员权限接管
- 应用解密后,内存里的明文
- 分发给所有客户端、共用的长期敏感信息
最后那个「所有客户端共用的长期敏感信息」特别重要。
例如:
- 所有客户端内嵌同一个 API 密钥
- 所有终端用同一个共用密码
- 只靠客户端就能完成的固定解密密钥
这类设计 只要有一台被攻破,就很容易波及整体。
DPAPI 对于「把保存方式做得比明文好」是有效的,
但 它无法替「本来就不该放在客户端的敏感信息」背书。
6. CurrentUser 与 LocalMachine 的使用区分
这里相当重要。随便选会让含义完全不同。
6.1. 基本用 CurrentUser
一般 Windows 桌面应用,首先以 CurrentUser 为基本选择。
适合的例子:
- WPF / WinForms / WinUI 的面向用户的桌面应用
- 每位用户各自持有配置或凭证的应用
- 把配置放在
%LocalAppData%或%AppData%下的应用
这种情况下,会变成 「那个 Windows 用户的敏感信息」,处理起来比较自然。
6.2. LocalMachine 的用途相当有限
LocalMachine 看起来很方便,但对一般桌面应用来说范围太广。
适合的场景,例如:
- 可信的单一用途机器上的 Windows 服务
- 只会在那台机器的特定进程使用的敏感信息
- 必须跨登录用户在同一台机器上共用的场景
不过注意事项相当沉重。
- 那台电脑上运行的各种进程都能广泛解密
- 共用终端、RDS、跳板机、多用户共用环境容易变得危险
- 以「反正大家都能用很方便」为理由选它,通常后面都会后悔
6.3. 犹豫时这样想
- 一般 UI 应用 ->
CurrentUser - 真的需要以机器为单位守住的特殊情况 ->
LocalMachine - 任何用户都要能解密、但终端上还有其他用户在 -> 通常最好从设计上重新审视一次
6.4. 涉及服务或 impersonation 时,注意事项会变多
把 Windows 服务或 impersonation 掺在一起考虑时,CurrentUser 的含义会变重。
- 运行账号是谁
- 该账号的 Profile 是否已被加载
- 解密时机处于哪个上下文
只要这些对不上,就很容易出现 「能加密但解不开」 的情况。
服务场景通常不能靠「反正先用 CurrentUser」就解决。
7. 实现的最底限方针
在 Windows 应用中,光是「不要再用明文配置」,设计其实不需要搞得太复杂。
不过有几个不想漏掉的要点。
7.1. 只保护敏感信息本身
与其把整个配置全部加密,不如先只保护 敏感字段,比较好处理。
例如,像下面这样区分。
- Server URL
- 用户名
- 数据库名称
- 功能开关
这些通常可以维持明文。
另一方面,
- 密码
- API Token
- Refresh Token
- 共享文件夹凭证
是保护对象。
这样区分之后,
- 编辑配置更容易
- 比对差异更容易
- 清楚知道哪里是敏感信息
- 整体运维更简单
会同时成立。
7.2. 存储位置以 per-user 为基本
一般桌面应用,存储位置基本选在下列这类 per-user 位置。
%LocalAppData%\Vendor\App\settings.json%AppData%\Vendor\App\settings.json
至少 不要随便放在安装目录下或容易被共享的地方。
就算用 DPAPI 保护,存储位置的 ACL 乱七八糟的话,
就会变成「密文被读到」「配置结构被看光」「运维失误照样发生」的故事。防御不能只有一层,要叠加起来才有效。
7.3. optionalEntropy 不是万能的第二把密钥
ProtectedData 可以传入 optionalEntropy。
很方便,但 「把它嵌进可执行文件就会变安全的第二把魔法密钥」这种东西是不存在的。
- 放在同一个文件里,就不算是秘密
- 用固定值塞进可执行文件,也称不上是强敏感信息
- 不过对用途识别或误用防止仍有帮助
实务上的用法,比如:
- 应用名称
- 用途名称
- 版本标识
以固定的 byte 数组传入,
为了「不要不小心接受到其他用途的密文」 而使用,把握这种程度就刚好。
7.4. 不是说「密文就可以丢进 Git」
这点低调但重要。
DPAPI 的密文比明文好得多,但
并不代表配置文件可以整个放进代码仓库。
理由很直白:
- 密文会留很久
- 总有一天同一台终端、同一个上下文可能会被重现
- 文件里还有敏感信息之外的信息
- 容易养出「反正有保护,可以随便对待」的文化
「比明文好」 和
「放哪里都安全」
是完全不同的两件事。
7.5. 不要写进日志
意外常见的陷阱,是解密之后把东西写进日志,前面的努力就全白费了。
- 连接失败时把整个连接字符串打印出来
- API 返回 401 时保留 Authorization header
- 把敏感信息混进异常信息
只要做出上面那些事,就算不再用明文配置文件,日志最后还是会变成明文仓库。
虽然很悲哀,但相当真实。
8. C# / .NET 的最小实现示例
下面是一个用 CurrentUser 保护准备存进配置文件的字符串的最小示例。
为了用途识别放了固定的 optionalEntropy,但 千万不要把它当成秘密密钥。
using System;
using System.Security.Cryptography;
using System.Text;
public static class DpapiSecretProtector
{
// 用途识别用。不是第二把秘密密钥。
private static readonly byte[] Entropy =
Encoding.UTF8.GetBytes("ComComponent:DesktopApp:SettingsSecret:v1");
public static string ProtectToBase64(string plaintext)
{
ArgumentNullException.ThrowIfNull(plaintext);
byte[] plainBytes = Encoding.UTF8.GetBytes(plaintext);
byte[] protectedBytes = Array.Empty<byte>();
try
{
protectedBytes = ProtectedData.Protect(
plainBytes,
optionalEntropy: Entropy,
scope: DataProtectionScope.CurrentUser);
return Convert.ToBase64String(protectedBytes);
}
finally
{
Array.Clear(plainBytes, 0, plainBytes.Length);
if (protectedBytes.Length > 0)
{
Array.Clear(protectedBytes, 0, protectedBytes.Length);
}
}
}
public static string UnprotectFromBase64(string protectedBase64)
{
ArgumentNullException.ThrowIfNull(protectedBase64);
byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
byte[] plainBytes = Array.Empty<byte>();
try
{
plainBytes = ProtectedData.Unprotect(
protectedBytes,
optionalEntropy: Entropy,
scope: DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(plainBytes);
}
finally
{
Array.Clear(protectedBytes, 0, protectedBytes.Length);
if (plainBytes.Length > 0)
{
Array.Clear(plainBytes, 0, plainBytes.Length);
}
}
}
}
使用方式很简单。
string protectedPassword = DpapiSecretProtector.ProtectToBase64(password);
// 保存到 JSON 等
// settings.DbPasswordProtected = protectedPassword;
string password = DpapiSecretProtector.UnprotectFromBase64(settings.DbPasswordProtected);
配置文件可以做成例如下面这样。
{
"ApiBaseUrl": "https://api.example.com/",
"UserName": "app-user",
"PasswordProtected": "AQAAANCMnd8BFdERjHoAwE..."
}
这种形式的好处在于:
- URL 和用户名可以照常编辑
- 只有密码被保护
- 配置结构很好读
- 比明文摆在那边不容易出事
9. 即便如此仍然危险的设计
即便用了 DPAPI,下列设计仍然危险。
9.1. 把解密后的值到处长时间带着走
把解密后的值:
- 写进日志
- 显示在界面上
- 塞进异常信息
- 长时间挂在长生命周期的对象上
这些都是想避免的行为。
「存的时候加密」 和
「使用期间也安全」
是两件不同的事。
9.2. 让所有安装共用同一份敏感信息
例如让所有用户共用同一个 API 密钥,这种设计就算用 DPAPI 存也不能从根本上解决问题。
因为 只要任何一台上的应用能解密,那份敏感信息就能被提取出来。
这类敏感信息应该:
- 放到服务端
- 客户端只持有 Token
- 改成以用户为单位的凭证
- 采用带有有效期的 Token
往这些方向调整会更好。
9.3. 因为「比较省事」就选 LocalMachine
这种情况真的很常见。
- 用户切换后还是读得到
- 服务也能读
- 能用就方便
这些理由都会让人很想选 LocalMachine。
但对一般桌面应用来说,这个选择会
「把解密可能性扩大到那台电脑上的其他进程」,
意义完全不同。
9.4. 再叠一层自制加密就以为安心
用自己的加密取代 DPAPI,例如:
- 把 AES 密钥写死在源代码里
- 把 AES 密钥写在配置文件的另一个字段里
- 把「稍微混淆过的字符串」当密钥用
这些做法大多效果有限。
「不是明文」和「安全」之间还有一条相当大的鸿沟。
10. DPAPI 不够用的场景
DPAPI 方便,但不是万能的。下列场景应该考虑其他方案。
10.1. 想在 Windows 以外也能跑
DPAPI / ProtectedData 是 Windows 专属的。
跨平台应用无法以此为前提来构建。
10.2. 想在多台机器、多位用户间处理同一份敏感信息
想让同一份密文在多台电脑都能解开、想让多位用户共用,
都超出了「把敏感信息绑定在那台终端、那位用户身上」这个 DPAPI 的拿手领域。
这种情况应该考虑:
- 服务端的密钥管理
- 凭证管理基础设施
- Windows 身份验证 / 集成身份验证
- 应用专用的凭证库
依据需求采用其他设计。
10.3. 要保存的就是用户的账号密码本身
如果是 packaged desktop app / WinUI 系,而且保存对象明确是:
- 用户名
- 密码
这组,那么凭据管理器(Credential Locker)也是可选方案之一。
不过本文的主线仍然是 「Windows 客户端不要再用明文配置文件」 的 DPAPI 实务路线。
11. 实务上建议的优先顺序
最后,如果在实务中犹豫,按下列顺序思考会比较好整理。
优先级 1:一开始就不要持有
- Windows 身份验证
- 集成身份验证
- 交互式登录
- 在服务端保管敏感信息
- 短期 Token
优先级 2:向「每位用户的敏感信息」靠拢
- 舍弃共用敏感信息,改走 per-user
- 舍弃长期固定凭证,改用可更新的 Token
- 避免所有客户端共用同一个密钥
优先级 3:如果必须本地保存,就用 DPAPI
- 通常是
CurrentUser - 存储位置以 per-user
- 只保护敏感字段
- 不要写进日志
优先级 4:LocalMachine 作为例外处理
- 是否真的必须以机器为单位
- 那台终端会不会有其他用户登录
- 就服务设计而言是否合理
12. 结语
在 Windows 应用中必须把敏感信息存进配置文件时,
应尽量避免以明文摆放。
然后对于:
「反正密钥总得存在某个地方,那不都一样吗?」
这个疑问,比较实务的回答是:
- 自己做加密、密钥放在同一个地方的话,几乎一样
- DPAPI 并不一样
- 可以把密钥管理交给 OS
- 可以把解密主体绑定在 Windows 用户 / 电脑上
- 能避免让文件单独外泄直接等同于敏感信息外泄
- 但是
- 以相同用户权限运行的代码
- 完全被攻陷的终端
- 根本不该放在客户端的长期共用敏感信息
这几件事,它解决不了。
换言之,DPAPI 不是 万能的城墙。
但它至少能 把「配置文件明文」这面整个透光的玻璃,换成一面起码像样的窗户。
在 Windows 客户端的实务中,这个差距相当大。
先从这里不要弄错开始,才是最现实的切入点。
13. 参考资料
- 前一篇文章: https://comcomponent.com/blog/2026/03/14/001-windows-app-security-minimum-checklist/
- Microsoft Learn:
CryptProtectData
https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata - Microsoft Learn:
ProtectedData
https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata?view=windowsdesktop-10.0 - Microsoft Learn:
DataProtectionScope
https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotectionscope?view=windowsdesktop-10.0 - Microsoft Learn: How to: Use Data Protection
https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection - Microsoft Learn: Credential locker for Windows apps
https://learn.microsoft.com/en-us/windows/apps/develop/security/credential-locker
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
伪随机数与真随机数的区别 - 如何区分的整理
本文整理伪随机数与真随机数的区别,重点不在输出外观而在生成器结构:普通 PRNG 重视可重现性、CSPRNG / DRBG 主打不可预测性、NRBG 则以物理熵源为基础。文中说明种子(seed)与重新播种(reseed)、健康检测(health test)的作用,并给出信息...
Windows 什么时候需要管理员权限 - UAC、保护区域与设计上的辨别方式
从边界与存储位置的角度,整理 Windows 什么时候真正需要管理员权限:UAC、保护区域、HKLM、服务、驱动、防火墙。同时说明 per-user 与 per-machine 的差异,以及把管理员处理拆成独立 EXE、服务或任务的设计取舍,帮读者判断该不该提升权限。
Windows Forms、WPF、WinUI 该怎么选 - 新建项目、存量资产、发布、UI 表现力判断表
从存量资产的规模、界面是表单为主还是需要表现力、现代 Windows UI 是否是产品刚需、发布与运维怎么落地这四个角度,整理 WinForms、WPF、WinUI 该怎么选的判断表,并提醒只想用 Windows App SDK 不必全面迁移到 WinUI。
将 .NET Framework 迁移到 .NET 之前该确认的事 - 动手前就决定成败的实战检查清单
整理将 .NET Framework 业务应用程序迁移到现代 .NET 之前必须先盘点的论点。涵盖落地版本、Windows 专用前提的取舍、不再支持的 API、共享库切分方式、第三方组件、运维与 CI/CD,帮助在动手前厘清范围并降低迁移风险。
Windows 网卡高级设置详解 - Jumbo Packet、RSS、LSO、RSC、Flow Control、EEE、Wake on LAN 到底怎么设置
本文以实务角度逐项拆解 Windows 网卡高级设置,从 Speed & Duplex、Jumbo Packet 到 RSS、RSC、LSO、Interrupt Moderation、Flow Control、EEE 与 Wake on LAN,说明每项调整在吞吐量、延迟、...
常见问题
汇总了咨询这一主题时常见的问题。
- Windows 应用的密码或 API Token 该怎么保存?
- 按顺序思考:首先从根本上不要让客户端持有长期敏感信息,优先采用 Windows 身份验证、集成身份验证、用户交互式登录或服务端密钥管理;如果确实必须存在本地,就不要以明文存放,在 Windows 上首先考虑 DPAPI / ProtectedData;一般桌面应用基本用 DataProtectionScope.CurrentUser,LocalMachine 的适用范围相当窄。
- 反正密钥总得存在某个地方,DPAPI 和明文不是一样吗?
- 这句话一半对,但结论错了。自己写 AES 加密、把密钥放在同一个应用或配置文件里,确实相当接近明文;但 DPAPI 是把密钥管理交给 OS,并把能解密的主体绑定到「那个 Windows 用户」或「那台电脑」上。结果对配置文件单独外泄、被带到别的电脑、误发邮件、备份外泄、混进代码仓库这类事故的防御力有很大差异。DPAPI 把「能读文件」和「能用敏感信息」这两件事分开了。
- DPAPI 能防住哪些情况、防不住哪些情况?
- 能发挥作用的是配置文件明文外泄、文件被带到别的电脑、同一台电脑上其他用户尝试读取(以 CurrentUser 为前提)、以备份或附件形式外泄等场景。防不住的是以相同用户权限运行的攻击代码、终端本身完全沦陷、管理员权限被接管、解密后内存中的明文,以及所有客户端共用的长期敏感信息——这类信息本来就不该放在客户端。
- DataProtectionScope 的 CurrentUser 和 LocalMachine 该怎么选?
- 一般 UI 桌面应用以 CurrentUser 为基本选择,会变成「那个 Windows 用户的敏感信息」,只把密文复制到别的电脑也难以直接使用。LocalMachine 的用途相当有限,适合可信的单一用途机器上的 Windows 服务等场景,但那台电脑上运行的各种进程都能广泛解密,共用终端或 RDS 环境容易变得危险。如果任何用户都要能解密、而且终端上还有其他用户,通常最好重新审视设计。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。