You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core 3.1无well-known元数据时手动验证OIDC提供商Token

解决方案

你当前的场景可以通过两种可落地的方案实现Token验证,优先推荐直接使用已有的Token Introspection端点的方案,无需处理动态JWKS拉取逻辑,适配性更高。


方案一:使用Token Introspection端点验证(推荐)

你已经有可用的token_info端点,直接通过该端点做Token有效性校验,完全绕开JWKS拉取的问题,改造步骤如下:

  1. 调整OpenID Connect配置,禁用默认的签名校验逻辑,新增事件拦截处理自定义验证
  2. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 12:45:03