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

Keycloak原生LDAP联邦:通过JTA实体从外部数据库动态加载角色

问题描述

使用Keycloak 20.0.2原生LDAP联合提供程序(未同步模式),需求如下:

  • 角色从外部数据库动态加载,无需提前在Keycloak中定义;
  • 用户登录、管理控制台查看用户时,优先从外部数据库查询角色,可同步但不依赖Keycloak本地角色数据;
  • 加载LDAP用户时不同步角色,完全通过外部数据库动态获取。

目前基于AbstractLDAPStorageMapper结合JTA数据源实现,管理控制台查看用户时能获取到角色列表,但角色未被Keycloak正确识别(比如登录后Token中不包含这些角色、权限校验不生效)。

现有代码
@Override
public Stream<RoleModel> getRoleMappingsStream() {
     System.out.println("]--> getRoleMappingsStream");
     Stream<RoleModel> roleMappings = super.getRoleMappingsStream();
 
     String email = delegate.getEmail();
     List<String> rolesDB = getRolesFromDB(email);
     for (String roleDB : rolesDB){
         System.out.println("]--> " + roleDB);
         RoleModel roleModel = realm.getRole(roleDB);
         if (roleModel == null){
             roleModel = realm.addRole(roleDB);
             logger.debugf("Adding role [%s] ", roleDB);
             System.out.println("[--> Adding role " + roleDB);
         }
         logger.debugf(
           "Granting role [%s] to user [%s] during user import from LDAP", 
           roleDB, 
           email
         );
         System.out.println("Granting role " + roleDB + " to user " + email + " during user import from LDAP");
         delegate.grantRole(roleModel);
         roleMappings = Stream.concat(roleMappings, Stream.of(roleModel));
     }

     RoleModel role = getRole(realm);
     if (role != null) {
         roleMappings = Stream.concat(roleMappings, Stream.of(role));
     }
     return roleMappings;
}
问题原因分析
  1. 未同步模式的用户特性:未同步模式下LDAP用户是临时对象,调用delegate.grantRole()不会持久化角色映射,仅在当前请求上下文有效,后续请求或Token生成时无法获取这些关联关系。
  2. 角色获取路径不完整:getRoleMappingsStream()仅在管理控制台查看用户时被调用,但用户登录时的角色获取依赖getRealmRoleMappingsStream()或Token生成阶段的查询逻辑,现有代码未覆盖这些场景。
  3. 流操作的潜在问题:多次使用Stream.concat()可能导致流重复消费,部分角色未被正确加入最终返回结果。
解决方案

1. 重写完整的角色映射方法

除getRoleMappingsStream()外,还需重写getRealmRoleMappingsStream(),确保所有场景都能获取外部数据库角色:

@Override
public Stream<RoleModel> getRealmRoleMappingsStream() {
    Stream<RoleModel> baseRoles = super.getRealmRoleMappingsStream();
    return Stream.concat(baseRoles, fetchExternalRoles());
}

@Override
public Stream<RoleModel> getRoleMappingsStream() {
    Stream<RoleModel> baseRoles = super.getRoleMappingsStream();
    return Stream.concat(baseRoles, fetchExternalRoles());
}

// 提取公共方法,统一从外部数据库获取角色
private Stream<RoleModel> fetchExternalRoles() {
    String email = delegate.getEmail();
    List<String> rolesDB = getRolesFromDB(email);
    
    return rolesDB.stream()
            .map(roleName -> {
                RoleModel role = realm.getRole(roleName);
                if (role == null) {
                    role = realm.addRole(roleName);
                    logger.debugf("Created dynamic role: %s", roleName);
                }
                return role;
            });
}

2. 移除无效的grantRole()调用

未同步模式下,直接返回角色流即可,Keycloak会根据返回的流识别用户角色映射,无需调用delegate.grantRole()(该操作无法持久化,无实际意义)。

3. 确保Token生成时能获取角色

若登录后Token仍不包含角色,需在processUserFederationRequest()中注入角色:

@Override
public void processUserFederationRequest(UserFederationRequest request, UserFederationProviderModel model) {
    super.processUserFederationRequest(request, model);
    
    if (request instanceof UserFederationLoginRequest) {
        UserModel user = ((UserFederationLoginRequest) request).getUser();
        fetchExternalRoles().forEach(user::grantRole);
    }
}

4. 优化角色创建逻辑

若外部角色数量较多,可添加缓存避免重复创建角色(注意Realm生命周期,防止内存泄漏):

private final Map<String, RoleModel> roleCache = new ConcurrentHashMap<>();

private RoleModel getOrCreateRole(String roleName) {
    return roleCache.computeIfAbsent(roleName, name -> {
        RoleModel existing = realm.getRole(name);
        return existing != null ? existing : realm.addRole(name);
    });
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 03:35:19