Azure AD B2C自定义PolicyConfigurationManager的MergeConfig偶发空引用异常问题
问题根因
- Clone方法违规修改共享缓存对象
你当前的Clone方法为了完成克隆,直接清空了传入的OpenIdConnectConfiguration实例的SigningKeys集合,而这个实例是内置ConfigurationManager缓存的全局共享对象。当并发请求刚好走到Clear操作和重新回填密钥的间隙,其他线程读取该配置的SigningKeys时就会拿到空/无效的密钥项,触发空引用。 - 缺少线程安全控制
PolicyConfigurationManager是单例实例,OpenIdConnectConfiguration的SigningKeys等集合都是非线程安全的,多线程并发执行合并配置、元数据自动刷新操作时,会出现枚举集合过程中集合被修改、拿到null元素的问题。 - 空值校验缺失
MergeConfig方法中判断密钥是否存在时,没有校验SecurityKey实例本身是否为null、KeyId是否为null,当出现无效密钥项时直接访问KeyId就会抛出空引用异常。
解决方案
- 重构Clone方法,禁止修改原配置对象
不要修改传入的原始配置实例,调整克隆逻辑不触碰原对象的成员:
private static OpenIdConnectConfiguration Clone(OpenIdConnectConfiguration configuration) { // 直接序列化完整对象,不需要清空原对象的成员 var json = OpenIdConnectConfiguration.Write(configuration); var clone = OpenIdConnectConfiguration.Create(json); // 手动同步签名密钥,避免序列化丢失非公开属性 clone.SigningKeys.Clear(); foreach (var key in configuration.SigningKeys) { clone.SigningKeys.Add(key); } clone.JsonWebKeySet = configuration.JsonWebKeySet; return clone; }
- 增加空值校验,修复MergeConfig的空引用风险
修改MergeConfig中密钥判断的逻辑,提前做空值过滤:
private static void MergeConfig(OpenIdConnectConfiguration result, OpenIdConnectConfiguration source) { // 其他集合遍历也建议先判断是否为null,避免元数据返回异常 if (source.IdTokenSigningAlgValuesSupported != null) { foreach (var alg in source.IdTokenSigningAlgValuesSupported) { if (!result.IdTokenSigningAlgValuesSupported.Contains(alg)) { result.IdTokenSigningAlgValuesSupported.Add(alg); } } } if (source.ResponseTypesSupported != null) { foreach (var type in source.ResponseTypesSupported) { if (!result.ResponseTypesSupported.Contains(type)) { result.ResponseTypesSupported.Add(type); } } } if (source.SubjectTypesSupported != null) { foreach (var type in source.SubjectTypesSupported) { if (!result.SubjectTypesSupported.Contains(type)) { result.SubjectTypesSupported.Add(type); } } } if (source.SigningKeys != null) { foreach (var key in source.SigningKeys.Where(k => k != null && !string.IsNullOrEmpty(k.KeyId))) { if (result.SigningKeys.All(k => k != null && !string.IsNullOrEmpty(k.KeyId) && k.KeyId != key.KeyId)) { result.SigningKeys.Add(key); } } } }
- 增加线程安全锁
在PolicyConfigurationManager中添加全局锁,所有涉及配置读取、合并、刷新的操作都加锁,避免并发冲突:
private readonly object _syncLock = new object(); public async Task<OpenIdConnectConfiguration> GetConfigurationAsync(CancellationToken cancel) { OpenIdConnectConfiguration mergedConfiguration = null; foreach (var manager in managers) { var configuration = await manager.Value.GetConfigurationAsync(cancel); lock (_syncLock) // 加锁保证合并、克隆操作原子性 { if (mergedConfiguration == null) mergedConfiguration = Clone(configuration); else MergeConfig(mergedConfiguration, configuration); } } return mergedConfiguration; } public void RequestRefresh() { lock (_syncLock) { foreach (var manager in managers) { manager.Value.RequestRefresh(); } } }
- 可选优化:调整元数据刷新间隔
如果不需要太频繁的元数据刷新,可以在构造内置ConfigurationManager的时候指定刷新间隔,降低刷新和业务请求冲突的概率:
public PolicyConfigurationManager(string authority, IEnumerable<string> policies) { foreach (var policy in policies) { var metadataAddress = $"{authority}/.well-known/openid-configuration?{PolicyParameter}={policy}"; managers.Add(policy.ToLowerInvariant(), new ConfigurationManager<OpenIdConnectConfiguration>( metadataAddress, new OpenIdConnectConfigurationRetriever(), new HttpDocumentRetriever(), TimeSpan.FromDays(7) // 调整为7天刷新一次,按需修改 )); } }
内容的提问来源于stack exchange,提问作者Flying Squirrel
相关产品推荐
相关产品推荐

