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

.NET 7 API仍接受Keycloak已撤销令牌的问题咨询

问题分析

默认的JWT认证仅验证令牌的签名有效性、过期时间、受众(Audience)等字段,不会主动检查令牌是否被撤销——因为JWT是无状态令牌,本身不包含撤销状态。这就是你撤销Keycloak令牌后,API仍接受该令牌请求的核心原因。

解决方案:实时检查令牌活跃性

以下是几种实现每次请求验证令牌活跃状态的方案,同时兼顾性能优化:

1. 调用Keycloak Introspection端点验证

Keycloak提供了/protocol/openid-connect/token/introspect端点,可直接查询令牌的活跃状态。你可以在.NET认证流程中集成该端点的调用。

实现步骤

  1. 安装IdentityModel NuGet包(简化Introspection请求的封装):
Install-Package IdentityModel
  1. 修改JWT认证配置,在OnTokenValidated事件中触发令牌状态检查:
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.Authority = "your-keycloak-authority-url";
    options.RequireHttpsMetadata = false;
    options.Audience = "your-audience";
    
    options.Events = new JwtBearerEvents
    {
        OnTokenValidated = async context =>
        {
            var jwtToken = context.SecurityToken as JwtSecurityToken;
            if (jwtToken == null)
            {
                context.Fail("无效令牌");
                return;
            }

            // 初始化Introspection客户端
            var introspectionClient = new IntrospectionClient(
                $"{options.Authority}/protocol/openid-connect/token/introspect",
                "your-client-id", // Keycloak客户端ID
                "your-client-secret"); // Keycloak客户端密钥

            // 发送状态查询请求
            var response = await introspectionClient.SendAsync(new IntrospectionRequest
            {
                Token = jwtToken.RawData,
                TokenTypeHint = "access_token"
            });

            // 验证令牌是否处于活跃状态
            if (!response.IsActive)
            {
                context.Fail("令牌已被撤销或处于非活跃状态");
            }
        }
    };
});

性能优化:缓存Introspection结果

频繁调用Keycloak端点会增加请求延迟,可通过缓存结果减少重复调用:

options.Events = new JwtBearerEvents
{
    OnTokenValidated = async context =>
    {
        var jwtToken = context.SecurityToken as JwtSecurityToken;
        if (jwtToken == null)
        {
            context.Fail("无效令牌");
            return;
        }

        var tokenId = jwtToken.Id;
        var cacheKey = $"TokenActive_{tokenId}";
        var memoryCache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>();

        // 优先从缓存读取状态
        if (!memoryCache.TryGetValue(cacheKey, out bool isActive))
        {
            var introspectionClient = new IntrospectionClient(
                $"{options.Authority}/protocol/openid-connect/token/introspect",
                "your-client-id",
                "your-client-secret");

            var response = await introspectionClient.SendAsync(new IntrospectionRequest
            {
                Token = jwtToken.RawData,
                TokenTypeHint = "access_token"
            });

            isActive = response.IsActive;
            // 缓存时长设为令牌剩余有效期,避免缓存过期令牌的状态
            var cacheExpiry = jwtToken.ValidTo - DateTime.UtcNow;
            memoryCache.Set(cacheKey, isActive, cacheExpiry);
        }

        if (!isActive)
        {
            context.Fail("令牌已被撤销或处于非活跃状态");
        }
    }
};

2. 基于分布式缓存的令牌撤销检查

如果担心Keycloak端点的性能开销,可以通过缓存已撤销令牌ID的方式实现验证,避免每次请求调用外部服务。

实现步骤

  1. 配置分布式缓存(如Redis),确保API和令牌撤销服务共享同一缓存源。

  2. 修改认证流程,在OnTokenValidated事件中检查缓存:

options.Events = new JwtBearerEvents
{
    OnTokenValidated = async context =>
    {
        var jwtToken = context.SecurityToken as JwtSecurityToken;
        if (jwtToken == null)
        {
            context.Fail("无效令牌");
            return;
        }

        var tokenId = jwtToken.Id;
        var cacheKey = $"RevokedToken_{tokenId}";
        var distributedCache = context.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();

        // 检查缓存中是否存在已撤销的令牌ID
        var isRevoked = await distributedCache.GetStringAsync(cacheKey) != null;
        if (isRevoked)
        {
            context.Fail("令牌已被撤销");
        }
    }
};
  1. 在调用Keycloak撤销令牌的逻辑中,同步将令牌ID存入缓存:
// 调用Keycloak撤销令牌后,同步更新缓存
public async Task RevokeToken(string tokenId, DateTime tokenExpiry)
{
    var distributedCache = _serviceProvider.GetRequiredService<IDistributedCache>();
    var cacheKey = $"RevokedToken_{tokenId}";
    // 缓存时长设为令牌剩余有效期
    var expiry = tokenExpiry - DateTime.UtcNow;
    await distributedCache.SetStringAsync(cacheKey, "true", new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = expiry
    });
}

优缺点

  • 优点:性能优异,避免外部服务调用延迟;
  • 缺点:需要同步撤销事件,可能存在短暂的状态不一致(如令牌已撤销但缓存未及时更新)。

3. 混合方案(平衡实时性与性能)

对于核心业务接口,直接调用Introspection端点保证实时性;对于非核心接口,使用缓存验证降低性能开销。可以通过自定义[Authorize]特性或策略来区分处理。

内容的提问来源于stack exchange,提问作者Augusto Ferbonink

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 06:35:12