.NET 7 API仍接受Keycloak已撤销令牌的问题咨询
问题分析
默认的JWT认证仅验证令牌的签名有效性、过期时间、受众(Audience)等字段,不会主动检查令牌是否被撤销——因为JWT是无状态令牌,本身不包含撤销状态。这就是你撤销Keycloak令牌后,API仍接受该令牌请求的核心原因。
解决方案:实时检查令牌活跃性
以下是几种实现每次请求验证令牌活跃状态的方案,同时兼顾性能优化:
1. 调用Keycloak Introspection端点验证
Keycloak提供了/protocol/openid-connect/token/introspect端点,可直接查询令牌的活跃状态。你可以在.NET认证流程中集成该端点的调用。
实现步骤
- 安装
IdentityModelNuGet包(简化Introspection请求的封装):
Install-Package IdentityModel
- 修改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的方式实现验证,避免每次请求调用外部服务。
实现步骤
配置分布式缓存(如Redis),确保API和令牌撤销服务共享同一缓存源。
修改认证流程,在
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("令牌已被撤销"); } } };
- 在调用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
相关产品推荐
相关产品推荐

