ASP.NET Core 3.1无well-known元数据时手动验证OIDC提供商Token
解决方案
你当前的场景可以通过两种可落地的方案实现Token验证,优先推荐直接使用已有的Token Introspection端点的方案,无需处理动态JWKS拉取逻辑,适配性更高。
方案一:使用Token Introspection端点验证(推荐)
你已经有可用的token_info端点,直接通过该端点做Token有效性校验,完全绕开JWKS拉取的问题,改造步骤如下:
- 调整OpenID Connect配置,禁用默认的签名校验逻辑,新增事件拦截处理自定义验证
- 在
OnTokenValidated事件中调用Introspection端点完成有效性校验,校验通过后再补充用户身份信息
完整配置代码示例:
services.AddAuthorization(cfg => { cfg.AddPolicy("MyPolicy", cfgPolicy => { cfgPolicy.AddRequirements().RequireAuthenticatedUser(); cfgPolicy.AddAuthenticationSchemes(OpenIdConnectDefaults.AuthenticationScheme); }); }).AddAuthentication(cfg => { cfg.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; cfg.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect(cfg => { cfg.ClientId = authenticationConfig.ClientId; cfg.ClientSecret = authenticationConfig.ClientSecret; cfg.ResponseType = "code"; cfg.CallbackPath = "/login/callback"; cfg.Scope.Clear(); cfg.Scope.Add("openid"); // 禁用默认签名校验,改用Introspection端点验证 cfg.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = "https://myissuer", ValidateIssuerSigningKey = false, // 按需开启其他基础校验项,比如Audience校验 ValidateAudience = true, ValidAudience = authenticationConfig.ClientId }; cfg.Configuration = new OpenIdConnectConfiguration { AuthorizationEndpoint = "https://mysts/api/oauth/authorize", TokenEndpoint = "https://mysts/api/oauth/token", UserInfoEndpoint = "https://mysts/api/oauth/token_info" }; // 禁止自动拉取well-known配置 cfg.ConfigurationManager = null; // 自定义事件处理Token验证 cfg.Events = new OpenIdConnectEvents { OnTokenValidated = async context => { if (context.SecurityToken is not JwtSecurityToken jwtToken) { context.Fail("无效的Token格式"); return; } // 建议通过IHttpClientFactory创建HttpClient,避免Socket泄漏,此处为简化示例 using var httpClient = new HttpClient(); var requestParam = new FormUrlEncodedContent(new Dictionary<string, string> { {"token", jwtToken.RawData}, {"client_id", authenticationConfig.ClientId}, {"client_secret", authenticationConfig.ClientSecret} }); var response = await httpClient.PostAsync("https://mysts/api/oauth/token_info", requestParam); if (!response.IsSuccessStatusCode) { context.Fail("Token验证请求失败"); return; } var introspectionResult = await response.Content.ReadFromJsonAsync<IntrospectionResult>(); // 校验Token是否有效 if (!introspectionResult.Active) { context.Fail("Token已失效"); return; } // 可选二次校验Iss、Aud等信息,和预期值匹配 if (introspectionResult.Iss != "https://myissuer" || !introspectionResult.Aud.Contains(authenticationConfig.ClientId)) { context.Fail("Token参数不匹配"); return; } // 校验通过后可按需补充用户声明到身份信息中 if (context.Principal.Identity is ClaimsIdentity identity) { identity.AddClaim(new Claim("sub", introspectionResult.Sub)); // 其他声明按需添加 } } }; });
你需要新增Introspection返回结果的实体类,匹配你token_info接口的返回结构即可,示例参考:
public class IntrospectionResult { [JsonPropertyName("active")] public bool Active { get; set; } [JsonPropertyName("iss")] public string Iss { get; set; } [JsonPropertyName("aud")] public List<string> Aud { get; set; } [JsonPropertyName("sub")] public string Sub { get; set; } // 其他返回字段按需扩展 }
方案二:自定义签名密钥解析器实现动态JWKS拉取
如果你需要本地做JWT签名校验、不想调用Introspection接口,可以自定义IssuerSigningKeyResolver委托,根据Token携带的kid动态拉取对应公钥,改造点如下:
在TokenValidationParameters中新增自定义密钥解析逻辑:
cfg.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = "https://myissuer", ValidateIssuerSigningKey = true, ValidateAudience = true, ValidAudience = authenticationConfig.ClientId, // 自定义签名密钥解析逻辑 IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => { if (securityToken is not JwtSecurityToken) { throw new SecurityTokenInvalidSignatureException("无效的Token格式"); } // 拼接动态JWKS地址 var jwksUrl = $"https://mysts/offline/jwks/{kid}/{authenticationConfig.ClientId}"; // 拉取公钥,建议此处增加本地缓存,避免每次验证都请求接口 using var httpClient = new HttpClient(); var jwksContent = httpClient.GetStringAsync(jwksUrl).ConfigureAwait(false).GetAwaiter().GetResult(); var jwks = new JsonWebKeySet(jwksContent); return jwks.GetKeyById(kid); } };
该方案需要注意添加JWKS的本地缓存(比如用IMemoryCache缓存不同kid对应的公钥,设置合理的过期时间),避免频繁请求JWKS接口导致性能下降。
注意事项
- 两种方案都需要配置
cfg.ConfigurationManager = null,避免OpenID Connect处理器默认尝试拉取.well-known/openid-configuration配置导致报错 - 生产环境请使用依赖注入的
IHttpClientFactory创建HttpClient实例,避免频繁新建HttpClient导致的Socket资源耗尽问题
内容的提问来源于stack exchange,提问作者Ronaldo Lanhellas
相关产品推荐
相关产品推荐

