Keycloak创建自定义外部用户认证端点时无法创建用户会话问题
问题根因
Keycloak 会话创建、Token 生成的全链路逻辑中,不会直接持有你传入的 UserModel 实例引用,会在多个流程节点通过 UserProvider 按用户 ID 回查用户数据。直接实例化的 InMemoryUserAdapter 没有被注册到当前 KeycloakSession 的用户查找链路中,回查时找不到对应用户,自然会导致会话绑定失败。
该场景不需要把外部用户持久化到 Keycloak 内置数据库,只要保证当前请求生命周期内,用户查询逻辑能命中你创建的内存用户即可。
修复步骤
- 修正HTTP方法注解:原代码接口标注为
@GET,却声明消费application/x-www-form-urlencoded格式,GET请求无法携带表单参数,会导致参数获取失败,需改成@POST。 - 注册内存用户到当前会话缓存:创建
InMemoryUserAdapter实例后,将其加入当前会话的用户缓存,后续所有用户查询逻辑会优先命中缓存中的内存用户,无需落库。 - 移除冗余的离线会话创建:如果业务不需要签发离线Refresh Token,不需要嵌套调用
createOfflineUserSession,直接创建普通用户会话即可。 - 补全基础校验逻辑:增加客户端存在性、密钥正确性校验,避免未授权调用。
- 修正Token过期时间计算:原代码
10 * 10 * 10 * 10计算结果仅为10000秒(约2.7小时),且逻辑可读性差,直接按实际需要的有效期基于当前时间戳计算即可。 - 替换写死的客户端IP:从请求上下文取真实来源IP,不要硬编码固定IP。
修正后核心代码
// 替换原接口方法注解 @POST @Path("authenticate") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response authenticate(@FormParam("token") String token, @FormParam("client_id") String clientId, @FormParam("client_secret") String clientSecret, @Context HttpRequest request) { // 先校验客户端 ClientModel clientModel = realm.getClientByClientId(clientId); if (clientModel == null || !clientModel.isEnabled()) { return Response.status(Response.Status.UNAUTHORIZED) .entity("{\"error\":\"invalid client\"}") .type(MediaType.APPLICATION_JSON) .build(); } if (!clientModel.validateSecret(clientSecret)) { return Response.status(Response.Status.UNAUTHORIZED) .entity("{\"error\":\"invalid client secret\"}") .type(MediaType.APPLICATION_JSON) .build(); } // 校验外部传入的token合法性(原有业务逻辑) logger.info("generating access token..."); // 创建内存用户并注册到当前会话缓存 String userId = UUID.randomUUID().toString(); InMemoryUserAdapter userModel = new InMemoryUserAdapter(session, realm, userId); userModel.setUsername(token); userModel.setEnabled(true); // 核心:注册用户到会话缓存,后续按ID查询可直接命中 session.userCache().addUserToCache(realm, userModel); // 创建普通用户会话,移除冗余的离线会话包装 UserSessionModel userSession = session.sessions().createUserSession( realm, userModel, token, request.getRemoteAddr(), // 取请求真实IP "MyCustomAuth", false, null, null ); logger.infof("Configurable token requested for username=%s and client=%s on realm=%s", userModel.getUsername(), clientModel.getClientId(), realm.getName()); AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, clientModel, userSession); ClientSessionContext clientSessionContext = DefaultClientSessionContext.fromClientSessionScopeParameter(clientSession, session); AccessToken newToken = tokenManager .createClientAccessToken(session, realm, clientModel, userModel, userSession, clientSessionContext); // 按实际需求设置有效期,示例为2小时 long expiresAt = System.currentTimeMillis() / 1000 + 7200; newToken.expiration((int) expiresAt); EventBuilder eventBuilder = new EventBuilder(realm, session, session.getContext().getConnection()); AccessTokenResponse response = tokenManager .responseBuilder(realm, clientModel, eventBuilder, session, userSession, clientSessionContext) .accessToken(newToken) .build(); return buildCorsResponse(request, response); }
可选扩展
如果你的业务需要支持Refresh Token刷新、调用UserInfo接口等标准OIDC能力,仅靠当前请求生命周期的缓存是不够的——后续刷新请求是新的会话,拿不到之前缓存的内存用户。这种场景可以实现一个轻量的UserStorageProvider(只实现用户查询方法),收到用户查询请求时,先判断是不是你自定义签发的token对应的用户,是则直接返回包装好的内存用户,否则走原有存储逻辑即可,不需要把用户数据同步到Keycloak库。
内容的提问来源于stack exchange,提问作者Muhammad Hewedy
相关产品推荐
相关产品推荐

