如何高效验证Keycloak用户会话状态?(规避频繁Admin API调用)
核心原因说明
Access Token是无状态的自包含令牌,只要未过期且签名合法,默认的JWT验证就会通过——这是OAuth2/OpenID Connect的设计特性。Keycloak手动登出只会失效对应的Refresh Token和服务器端会话,不会主动撤销已颁发的Access Token,所以需要额外机制来校验会话有效性。
高效的会话状态检查方案
1. 使用Keycloak Introspection端点(推荐)
Keycloak提供OpenID Connect Introspection端点,可以验证Access Token是否仍有效(包括是否被关联会话撤销),且可通过缓存降低调用成本。
ASP.NET集成方式:
- 先确保客户端在Keycloak中开启
Service Accounts Enabled权限,并分配introspect-tokens角色(根据Keycloak版本调整)。 - 在ASP.NET中配置JWT认证时,集成Introspection验证:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "http://localhost:8080/realms/{MyRealm}"; options.Audience = "{YourClientId}"; options.ClientId = "{YourClientId}"; options.ClientSecret = "{YourClientSecret}"; // 启用Introspection验证逻辑 options.Events = new JwtBearerEvents { OnTokenValidated = async context => { var token = context.SecurityToken as JwtSecurityToken; var introspectionClient = context.HttpContext.RequestServices.GetRequiredService<HttpClient>(); // 构造Introspection请求参数 var formData = new Dictionary<string, string> { {"token", token.RawData}, {"client_id", options.ClientId}, {"client_secret", options.ClientSecret} }; var response = await introspectionClient.PostAsync( $"{options.Authority}/protocol/openid-connect/token/introspect", new FormUrlEncodedContent(formData)); if (!response.IsSuccessStatusCode) { context.Fail("Token introspection failed"); return; } var introspectionResult = await response.Content.ReadFromJsonAsync<JsonElement>(); if (!introspectionResult.GetProperty("active").GetBoolean()) { context.Fail("Token is no longer active"); } } }; }); // 注册HttpClient供Introspection使用 builder.Services.AddHttpClient();
- 缓存优化:添加分布式缓存(如Redis)缓存Introspection结果,缓存键用Access Token的哈希值,缓存过期时间设为Access Token的剩余有效期,避免每次请求都调用Keycloak:
// 在OnTokenValidated事件中加入缓存逻辑 var cache = context.HttpContext.RequestServices.GetRequiredService<IDistributedCache>(); var cacheKey = $"token_introspect_{SHA256.HashData(Encoding.UTF8.GetBytes(token.RawData))}"; var cachedActiveStatus = await cache.GetStringAsync(cacheKey); if (!string.IsNullOrEmpty(cachedActiveStatus)) { if (!bool.Parse(cachedActiveStatus)) { context.Fail("Token is no longer active (cached)"); } return; } // 执行Introspection请求后,将结果存入缓存 var isActive = introspectionResult.GetProperty("active").GetBoolean(); await cache.SetStringAsync(cacheKey, isActive.ToString(), new DistributedCacheEntryOptions { AbsoluteExpiration = token.ValidTo });
2. 缩短Access Token有效期+Refresh Token自动刷新
将Keycloak中Access Token的有效期设置得较短(如5-15分钟),客户端通过Refresh Token定期获取新的Access Token。当用户手动登出后,Refresh Token会被Keycloak失效,客户端刷新时会收到错误,此时即可清除本地Token并终止会话。
ASP.NET端配合:无需额外修改认证逻辑,只需确保客户端正确处理Refresh Token的刷新失败场景,服务器端自然会拒绝失效的旧Access Token。
3. 改用Authorization Code Flow(可选)
如果业务场景允许,替换Direct Grant为Authorization Code Flow(带PKCE)。这种Flow下,客户端会维护与Keycloak的会话,登出时可调用Keycloak的Logout端点主动失效会话,同时客户端清除本地Token,从根源减少此类问题。
为什么不推荐Admin API查询会话
Admin API的/users/{UserId}/sessions端点需要较高权限,且每次请求都调用会大幅增加Keycloak和服务器的负担,缓存也难以精准控制(会话状态可能实时变化),因此仅适合后台批量校验场景,不适合接口实时验证。
内容的提问来源于stack exchange,提问作者Franco

