unprotect() 抛 cryptographicexception 主因是密钥上下文断裂:应用重启后默认内存密钥丢失、多实例未设 setapplicationname 导致密钥覆盖、密钥自动轮换后旧数据无法解密;purpose 字符串参与密钥派生,变更即失效;混用 protecteddata 与 idataprotectionprovider 亦必然失败。

ASP.NET Core 的 IDataProtectionProvider 不是“开箱即用”的加密工具——不显式配置,多实例、重启、升级后必炸,Unprotect() 抛 CryptographicException 是常态,不是你代码写错了。
为什么 Unprotect() 总抛 CryptographicException
这不是加解密逻辑出错,而是密钥上下文断了。三个最常见原因:
- 应用重启后密钥丢了:默认密钥存在
%LOCALAPPDATA%\ASP.NET\DataProtection-Keys(Windows)或~/.aspnet/DataProtection-Keys(Linux/macOS),进程一停就清空 - 多个服务实例共用同一密钥目录,但没调
SetApplicationName("MyApp-Prod"),互相覆盖密钥文件,旧密钥被删 - 密钥自动轮换(默认 90 天)后,你数据库里存的旧加密字符串再也找不到匹配密钥,
Unprotect()直接失败
验证很简单:同一进程内 Protect() → Unprotect() 成功,换进程或重启后失败 → 锁定密钥持久化问题。
PersistKeysToFileSystem 配置后还是报 UnauthorizedAccessException
挂载了 NFS 或 hostPath 并不等于“能用”,权限不对照样崩。关键点:
-
PersistKeysToFileSystem(new DirectoryInfo("/shared/keys"))要求运行进程对整个目录有 读+写+遍历 权限(Linux 下注意 SELinux 上下文,K8s 中若误用emptyDir,等同于没配) - Windows 上用
PersistKeysToRegistry(),必须给运行账户(如IIS AppPool\DefaultAppPool)明确授予注册表项的读写权限 - Azure 环境优先用
ProtectKeysWithAzureKeyVault(),避免自己管密钥生命周期
CreateProtector("Auth.Ticket") 的 purpose 字符串不能随便改
purpose 不是注释,它参与密钥派生计算。不同字符串 = 完全不同的密钥流:
-
provider.CreateProtector("Email.Token")和provider.CreateProtector("Email.ResetToken")加密的结果互不可逆 - 微服务间共享令牌(比如 Auth 服务签发、API 服务校验),必须所有服务共用完全一致的
purpose字符串 + 同一套密钥源 - 升级时改
purpose(如从"v1"改成"v2")是安全演进手段,但旧数据需迁移处理,否则直接失效
别把 ProtectedData(DPAPI)和 IDataProtectionProvider 混用
这是两类完全不同的机制,混用必踩坑:
-
ProtectedData.Protect()是 Windows 原生 API,只在 Windows 上跑,绑定当前用户会话或本机上下文,跨机器、换用户、重装系统后数据永久丢失 -
IDataProtectionProvider是 ASP.NET Core 的跨平台抽象层,支持密钥持久化、轮换、分布式共享,但必须手动配AddDataProtection() - Web 应用里用
ProtectedData解密配置?大概率失败——IIS 或 Kestrel 以ApplicationPoolIdentity或容器非交互用户运行,跟开发时你的登录用户不是同一个 DPAPI 上下文
真正容易被忽略的是:purpose 字符串一旦写死在代码里,就锁死了密钥隔离边界;而密钥存储路径的权限问题,往往在 CI/CD 流水线或 K8s YAML 里被 silently 忽略,直到上线才暴露。











