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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 15:33:16