WPF应用Azure认证出现CryptographicException数据无效错误排查
问题原因与修复方案
核心错误:加密/解密的熵(Entropy)不匹配
你在StoreAccessToken方法中使用AccessToken本身作为熵(Encoding.UTF8.GetBytes(accessToken)),但在RetrieveAccessToken中却使用资源名作为熵(Encoding.Unicode.GetBytes(_resourceName)),两者完全不一致。ProtectedData.Protect和Unprotect必须使用相同的熵才能正常解密,这是抛出CryptographicException的直接原因。
修复步骤
1. 统一加密和解密的熵
熵(entropy)的作用是增加加密数据的安全性,它不需要保密,但必须在加密和解密时保持一致。建议使用固定的、与应用相关的字符串作为熵,修改后的SecureStorage类如下:
internal static class SecureStorage { private const string _resourceName = "CDBManagerAccessToken"; private const string _entropyKey = "YourAppSpecificEntropyKey"; // 替换为应用专属固定字符串 private static readonly object _lockObject = new object(); public static void StoreAccessToken(string accessToken, DateTimeOffset expirationTime) { // 统一用UTF8编码生成固定熵 byte[] entropy = Encoding.UTF8.GetBytes(_entropyKey); // 序列化token和过期时间,统一加密存储 var tokenData = new { Token = accessToken, Expiration = expirationTime }; string serializedData = System.Text.Json.JsonSerializer.Serialize(tokenData); byte[] encryptedData = ProtectedData.Protect(Encoding.Unicode.GetBytes(serializedData), entropy, DataProtectionScope.CurrentUser); lock (_lockObject) { Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); if (config.AppSettings.Settings[_resourceName] == null) { config.AppSettings.Settings.Add(_resourceName, Convert.ToBase64String(encryptedData)); } else { config.AppSettings.Settings[_resourceName].Value = Convert.ToBase64String(encryptedData); } config.Save(ConfigurationSaveMode.Modified); ConfigurationManager.RefreshSection("appSettings"); // 刷新配置确保读取最新值 } } public static (string Token, DateTimeOffset Expiration) RetrieveAccessToken() { byte[] entropy = Encoding.UTF8.GetBytes(_entropyKey); lock(_lockObject) { Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); string encryptedData = config.AppSettings.Settings[_resourceName]?.Value; if (string.IsNullOrEmpty(encryptedData)) return (null, DateTimeOffset.MinValue); byte[] encryptedBytes = Convert.FromBase64String(encryptedData); byte[] decryptedData = ProtectedData.Unprotect(encryptedBytes, entropy, DataProtectionScope.CurrentUser); string serializedData = Encoding.Unicode.GetString(decryptedData); var tokenData = System.Text.Json.JsonSerializer.Deserialize<(string Token, DateTimeOffset Expiration)>(serializedData); return tokenData; } } }
2. 修复AuthenticationManager的认证状态逻辑
原代码中IsUserAuthenticated仅读取token但未更新过期时间,导致状态判断失效。同时调整token存储逻辑,同步保存过期时间:
internal class AuthenticationManager { private readonly string _clientId; private readonly string _tenantId; private readonly string _redirectUri; private string _authToken; private DateTimeOffset _tokenExpirationTime; private readonly TimeSpan _authenticationDuration; public AuthenticationManager() { _clientId = ConfigurationManager.AppSettings["ClientId"]; _tenantId = ConfigurationManager.AppSettings["TenantId"]; _redirectUri = ConfigurationManager.AppSettings["RedirectUri"]; _authenticationDuration = TimeSpan.FromDays(1); // 初始化时读取已存储的token和过期时间 var storedData = SecureStorage.RetrieveAccessToken(); _authToken = storedData.Token; _tokenExpirationTime = storedData.Expiration; } public bool IsUserAuthenticated() { return !string.IsNullOrEmpty(_authToken) && DateTime.UtcNow < _tokenExpirationTime; } public async Task<string> AuthenticateUser() { try { if (!IsUserAuthenticated()) { var pca = PublicClientApplicationBuilder.Create(_clientId) .WithAuthority($"https://login.microsoftonline.com/{_tenantId}") .WithRedirectUri(_redirectUri) .Build(); var scopes = new[] { "User.Read" }; var authResult = await pca.AcquireTokenInteractive(scopes).ExecuteAsync(); _authToken = authResult.AccessToken; // 优先使用Azure AD返回的token过期时间,比手动加1天更准确 _tokenExpirationTime = authResult.ExpiresOn; // 若需强制1天有效期,替换为:_tokenExpirationTime = DateTimeOffset.UtcNow.Add(_authenticationDuration); SecureStorage.StoreAccessToken(_authToken, _tokenExpirationTime); return authResult.AccessToken; } else { return _authToken; } } catch (Exception) { return null; } } }
额外注意事项
- 熵不能使用动态值(如AccessToken),必须固定,否则下次启动无法解密。
- 优先使用Azure AD返回的
ExpiresOn作为过期时间,避免手动设置的误差。 - 修改配置后调用
ConfigurationManager.RefreshSection,确保读取最新存储值。
内容的提问来源于stack exchange,提问作者wads
相关产品推荐
相关产品推荐

