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; }
问题原因分析
- 未同步模式的用户特性:未同步模式下LDAP用户是临时对象,调用
delegate.grantRole()不会持久化角色映射,仅在当前请求上下文有效,后续请求或Token生成时无法获取这些关联关系。 - 角色获取路径不完整:
getRoleMappingsStream()仅在管理控制台查看用户时被调用,但用户登录时的角色获取依赖getRealmRoleMappingsStream()或Token生成阶段的查询逻辑,现有代码未覆盖这些场景。 - 流操作的潜在问题:多次使用
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
相关产品推荐
相关产品推荐

