1. 概述

Keycloak 是一个第三方身份和访问管理解决方案,能帮我们快速集成认证授权功能。本文将通过几个示例,演示如何在 Keycloak 中高效搜索用户。

2. Keycloak 配置

首先需要完成 Keycloak 的基础配置:

✅ 创建初始管理员用户(用户名:baeldung,密码:secretPassword
✅ 使用默认的 master realm(无需新建)
✅ 使用默认的 admin-cli 客户端(无需新建)
✅ 创建 10 个测试用户(用户名格式:user1user10,邮箱格式:user1@example.com

⚠️ 确保用户邮箱格式一致,例如 user1@example.com,避免后续搜索踩坑。

3. Keycloak Admin Client

Keycloak 提供 REST API 和 Java 客户端,后者能简化开发。这里我们通过 Spring Boot 集成 Java 客户端。

3.1. 添加依赖

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-admin-client</artifactId>
    <version>21.0.1</version>
</dependency>

❌ 客户端版本必须与 Keycloak 服务器版本严格匹配,否则可能报错。

3.2. 配置客户端

@Bean
Keycloak keycloak() {
    return KeycloakBuilder.builder()
      .serverUrl("http://localhost:8080")
      .realm("master")
      .clientId("admin-cli")
      .grantType(OAuth2Constants.PASSWORD)
      .username("baeldung")
      .password("secretPassword")
      .build();
}

3.3. 创建服务类

@Service
public class AdminClientService {
    @Autowired Keycloak keycloak;
    
    @PostConstruct
    void searchUsers() {
        // 后续搜索方法将在此调用
    }
}

3.4. 按用户名搜索

private static final String REALM_NAME = "master";

void searchByUsername(String username, boolean exact) {
    List<UserRepresentation> users = keycloak.realm(REALM_NAME)
      .users()
      .searchByUsername(username, exact);
    
    logger.info("匹配用户: {}", users.stream()
      .map(UserRepresentation::getUsername)
      .collect(Collectors.toList()));
}

测试用例:

searchUsers() {
    searchByUsername("user1", true);   // 精确匹配
    searchByUsername("user", false);   // 模糊匹配
    searchByUsername("1", false);      // 包含"1"的用户
}

输出结果:

精确匹配 user1: [user1]
模糊匹配 user: [user1, user10, user2, user3, user4, user5, user6, user7, user8, user9]
包含"1"的用户: [user1, user10]

3.5. 按邮箱搜索

void searchByEmail(String email, boolean exact) {
    List<UserRepresentation> users = keycloak.realm(REALM_NAME)
      .users()
      .searchByEmail(email, exact);
    
    logger.info("匹配邮箱: {}", users.stream()
      .map(UserRepresentation::getEmail)
      .collect(Collectors.toList()));
}

测试用例:

searchByEmail("user1@example.com", true);

输出结果:

匹配邮箱: [user1@example.com]

3.6. 按自定义属性搜索

假设用户 user1 添加了自定义属性 DOB:2000-01-05

void searchByAttributes(String query) {
    List<UserRepresentation> users = keycloak.realm(REALM_NAME)
      .users()
      .searchByAttributes(query);
    
    logger.info("匹配属性: {}", users.stream()
      .map(user -> user.getUsername() + " " + user.getAttributes())
      .collect(Collectors.toList()));
}

测试用例:

searchByAttributes("DOB:2000-01-05");

输出结果:

匹配属性: [user1 {DOB=[2000-01-05]}]

3.7. 按组搜索

void searchByGroup(String groupId) {
    List<UserRepresentation> users = keycloak.realm(REALM_NAME)
      .groups()
      .group(groupId)
      .members();
    
    logger.info("组成员: {}", users.stream()
      .map(UserRepresentation::getUsername)
      .collect(Collectors.toList()));
}

⚠️ 需使用组 ID(非组名),例如 c67643fb-514e-488a-a4b4-5c0bdf2e7477

输出结果:

组成员: [user1, user2, user3, user4, user5]

3.8. 按角色搜索

void searchByRole(String roleName) {
    List<UserRepresentation> users = keycloak.realm(REALM_NAME)
      .roles()
      .get(roleName)
      .getUserMembers();
    
    logger.info("角色成员: {}", users.stream()
      .map(UserRepresentation::getUsername)
      .collect(Collectors.toList()));
}

测试用例:

searchByRole("user");

输出结果:

角色成员: [user1]

4. 自定义 REST 接口

当内置搜索无法满足需求时(如同时按组和角色筛选),可通过扩展 Keycloak 实现自定义接口。

4.1. 创建资源提供者

public class KeycloakUserApiProvider implements RealmResourceProvider {
    private final KeycloakSession session;

    public KeycloakUserApiProvider(KeycloakSession session) {
        this.session = session;
    }

    @Override
    public void close() {}

    @Override
    public Object getResource() {
        return this;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Stream<UserRepresentation> searchUsersByGroupAndRole(
            @QueryParam("groupName") @NotNull String groupName,
            @QueryParam("roleName") @NotBlank String roleName) {
        
        RealmModel realm = session.getContext().getRealm();
        
        GroupModel group = session.groups()
          .getGroupsStream(realm)
          .filter(g -> g.getName().equals(groupName))
          .findAny()
          .orElseThrow(() -> new NotFoundException("组未找到: " + groupName));
        
        return session.users()
          .getGroupMembersStream(realm, group)
          .filter(user -> user.getRealmRoleMappingsStream()
            .anyMatch(role -> role.getName().equals(roleName)))
          .map(ModelToRepresentation::toBriefRepresentation);
    }
}

4.2. 创建提供者工厂

public class KeycloakUserApiProviderFactory implements RealmResourceProviderFactory {
    public static final String ID = "users-by-group-and-role";

    @Override
    public RealmResourceProvider create(KeycloakSession session) {
        return new KeycloakUserApiProvider(session);
    }

    @Override
    public String getId() {
        return ID;
    }
    
    // 其他空实现方法省略...
}

4.3. 注册提供者

META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory 文件中声明:

com.yourpackage.KeycloakUserApiProviderFactory

4.4. 部署与测试

  1. 打包为 JAR 并放入 Keycloak 的 providers 目录
  2. 执行构建命令:
    kc build
    
  3. 重启 Keycloak
  4. 访问接口:
    GET /realms/master/users-by-group-and-role?groupName=Test%20Group&roleName=user
    

返回结果:

[{
    "id": "2c59a20f-df38-4d14-8ff9-067ea30f7937",
    "username": "user1",
    "email": "user1@example.com",
    "firstName": "First",
    "lastName": "User"
}]

5. 总结

✅ 使用 Keycloak Admin Client 可快速实现基础用户搜索
✅ 通过自定义接口能灵活扩展复杂查询逻辑
⚠️ 注意客户端版本匹配和组 ID 使用细节

完整代码示例可在 GitHub 获取。


原始标题:Search Users With Keycloak in Java