Skip to content

SPI User Storage Extension 说明文档

功能概述

spi-user-storage-extension 是一个 Keycloak 扩展,实现了 Keycloak 的用户存储 SPI,用于从外部数据库中读取用户信息,支持多种数据库类型,包括 MySQL、SQL Server、Oracle、达梦、金仓、OceanBase 和 GaussDB。

该扩展为 Keycloak 提供了灵活的用户存储能力,允许企业将现有的用户数据无缝集成到 Keycloak 中,无需进行数据迁移,同时保持用户认证的一致性。

界面预览

SPI User Storage Extension

SPI User Storage Extension 信息

技术支持

核心组件

1. CustomUserStorageProvider

功能:实现了 Keycloak 的 UserStorageProviderUserLookupProviderUserQueryProviderCredentialInputValidator 接口,负责从外部数据库中读取用户信息并验证用户凭证。

主要方法

  • getUserById(RealmModel realm, String id):根据用户ID获取用户

    • 参数
      • realm - Keycloak 领域对象
      • id - 用户ID,格式为 storageId:externalId
    • 返回值UserModel 实例,如未找到返回 null
    • 功能:解析用户ID,提取外部ID,然后根据外部ID获取用户
  • getUserByUsername(RealmModel realm, String username):根据用户名获取用户

    • 参数
      • realm - Keycloak 领域对象
      • username - 用户名
    • 返回值UserModel 实例,如未找到返回 null
    • 功能:根据用户名从外部数据库中查询用户信息
  • getUserByEmail(RealmModel realm, String email):根据邮箱获取用户

    • 参数
      • realm - Keycloak 领域对象
      • email - 邮箱地址
    • 返回值UserModel 实例,如未找到返回 null
    • 功能:根据邮箱从外部数据库中查询用户信息
  • isValid(RealmModel realm, UserModel user, CredentialInput input):验证用户凭证

    • 参数
      • realm - Keycloak 领域对象
      • user - 用户模型对象
      • input - 凭证输入对象
    • 返回值:布尔值,表示凭证是否有效
    • 功能:验证用户提供的凭证是否与外部数据库中存储的凭证匹配
  • getUsersCount(RealmModel realm):获取用户数量

    • 参数realm - Keycloak 领域对象
    • 返回值:整数,表示用户数量
    • 功能:查询外部数据库中的用户总数
  • searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults):搜索用户

    • 参数
      • realm - Keycloak 领域对象
      • search - 搜索关键词
      • firstResult - 起始结果索引
      • maxResults - 最大结果数量
    • 返回值UserModel
    • 功能:根据关键词搜索用户,支持分页
  • searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults):根据参数搜索用户

    • 参数
      • realm - Keycloak 领域对象
      • params - 搜索参数映射
      • firstResult - 起始结果索引
      • maxResults - 最大结果数量
    • 返回值UserModel
    • 功能:根据指定参数搜索用户,支持分页
  • getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults):获取组成员

    • 参数
      • realm - Keycloak 领域对象
      • group - 组模型对象
      • firstResult - 起始结果索引
      • maxResults - 最大结果数量
    • 返回值UserModel 流,当前返回空流
    • 功能:获取指定组的成员,当前未实现
  • getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults):获取角色成员

    • 参数
      • realm - Keycloak 领域对象
      • role - 角色模型对象
      • firstResult - 起始结果索引
      • maxResults - 最大结果数量
    • 返回值UserModel 流,当前返回空流
    • 功能:获取指定角色的成员,当前未实现
  • searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue):根据用户属性搜索用户

    • 参数
      • realm - Keycloak 领域对象
      • attrName - 属性名称
      • attrValue - 属性值
    • 返回值UserModel 流,当前返回空流
    • 功能:根据用户属性搜索用户,当前未实现
  • getConnection():获取数据库连接

    • 返回值Connection 实例
    • 功能:根据配置获取数据库连接

2. CustomUserStorageProviderFactory

功能:实现了 Keycloak 的 UserStorageProviderFactory 接口,负责创建 CustomUserStorageProvider 实例并提供配置选项。

主要方法

  • getId():返回提供者工厂的ID

    • 返回值:字符串,固定为 "bima-spi-user-storage-extension"
  • create(KeycloakSession session, ComponentModel model):创建用户存储提供者实例

    • 参数
      • session - Keycloak 会话对象
      • model - 组件模型对象,包含配置信息
    • 返回值CustomUserStorageProvider 实例
  • getConfigProperties():提供配置选项

    • 返回值List<ProviderConfigProperty> 列表
    • 功能:定义用户存储提供者的配置选项
  • init(Config.Scope config):初始化配置

    • 参数config - Keycloak 配置对象
    • 返回值:无
    • 功能:初始化提供者工厂
  • postInit(KeycloakSessionFactory factory):后初始化

    • 参数factory - Keycloak 会话工厂对象
    • 返回值:无
    • 功能:在所有提供者初始化后执行的操作
  • close():清理资源

    • 返回值:无
    • 功能:清理提供者工厂资源

3. CustomUserModel

功能:实现了 Keycloak 的 UserModel 接口,用于表示从外部数据库读取的用户。

主要属性

  • realm - Keycloak 领域对象
  • id - 用户ID
  • username - 用户名
  • email - 邮箱地址
  • enabled - 是否启用

主要方法

  • getId():获取用户ID
  • getUsername():获取用户名
  • setUsername(String username):设置用户名
  • getEmail():获取邮箱
  • setEmail(String email):设置邮箱
  • isEnabled():是否启用
  • setEnabled(boolean enabled):设置是否启用
  • getFirstName():获取名
  • setFirstName(String firstName):设置名
  • getLastName():获取姓
  • setLastName(String lastName):设置姓

4. 数据库方言组件

功能:为不同类型的数据库提供适配,处理SQL语法差异。

DatabaseDialect 接口

主要方法

  • getDriverClassName():获取数据库驱动类名
  • getLimitOffsetSql(String sql, int limit, int offset):获取带分页的SQL语句

DatabaseDialectFactory

功能:根据数据库类型创建对应的数据库方言实例。

主要方法

  • getDialect(String dbType):根据数据库类型获取数据库方言

配置与使用

1. 开发环境搭建

前提条件

  • JDK 11 或更高版本
  • Maven 3.6 或更高版本
  • Keycloak 17.0.0 或更高版本
  • 目标数据库及其驱动

编译构建

bash
cd spi-user-storage-extension
mvn clean package

编译完成后,在 target 目录下会生成 spi-user-storage-extension-1.0.0.jar 文件。

2. 部署

将编译好的 JAR 文件放入 Keycloak 的 standalone/deployments 目录。

3. 数据库驱动配置

需要将目标数据库的驱动 JAR 文件放入 Keycloak 的 standalone/lib 目录。

示例

  • MySQL:mysql-connector-java-8.0.28.jar
  • SQL Server:mssql-jdbc-9.4.1.jre11.jar
  • Oracle:ojdbc11.jar
  • 达梦:DmJdbcDriver18.jar
  • 金仓:kingbase8-8.6.0.jar
  • OceanBase:oceanbase-client-2.4.0.jar
  • GaussDB:gsjdbc4.jar

4. 配置

4.1 Keycloak 管理控制台配置

  1. 登录 Keycloak 管理控制台
  2. 进入 Realm 设置 → 用户 federation
  3. 添加 bima-spi-user-storage-extension 提供者
  4. 配置数据库连接信息:
    • Database Type:选择数据库类型(mysql、sqlserver、oracle、dameng、kingbase、oceanbase、gaussdb)
    • Connection URL:数据库连接 URL
    • Username:数据库用户名
    • Password:数据库密码
    • User Table:用户表名(默认:users)
    • Username Column:用户名列名(默认:username)
    • Password Column:密码列名(默认:password)
    • Email Column:邮箱列名(默认:email)
  5. 保存配置

4.2 数据库表结构

示例用户表结构

sql
-- MySQL 示例
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    enabled BOOLEAN DEFAULT TRUE,
    first_name VARCHAR(255),
    last_name VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 添加索引
CREATE INDEX idx_username ON users(username);
CREATE INDEX idx_email ON users(email);

5. 使用

配置完成后,Keycloak 会从配置的外部数据库中读取用户信息,用户可以使用外部数据库中的凭证登录 Keycloak。

5.1 登录测试

  1. 启动 Keycloak 服务器
  2. 打开 Keycloak 登录页面
  3. 输入外部数据库中存在的用户名和密码
  4. 点击登录
  5. 验证是否成功登录

5.2 用户搜索测试

  1. 登录 Keycloak 管理控制台
  2. 进入用户管理页面
  3. 尝试搜索外部数据库中的用户
  4. 验证是否能正确显示用户信息

扩展与定制

支持新的数据库类型

  1. 实现 DatabaseDialect 接口
java
public class PostgreSQLDialect implements DatabaseDialect {
    @Override
    public String getDriverClassName() {
        return "org.postgresql.Driver";
    }
    
    @Override
    public String getLimitOffsetSql(String sql, int limit, int offset) {
        return sql + " LIMIT " + limit + " OFFSET " + offset;
    }
}
  1. 在 DatabaseDialectFactory 中注册新的数据库方言
java
public class DatabaseDialectFactory {
    private static final Map<String, DatabaseDialect> dialects = new HashMap<>();
    
    static {
        // 注册现有数据库方言...
        
        // 注册 PostgreSQL 方言
        dialects.put("postgresql", new PostgreSQLDialect());
    }
    
    public static DatabaseDialect getDialect(String dbType) {
        return dialects.get(dbType);
    }
}

自定义用户属性映射

可以修改 CustomUserModel 类,添加对更多用户属性的支持。

示例

java
public class CustomUserModel implements UserModel {
    // 现有属性...
    private String firstName;
    private String lastName;
    private Map<String, List<String>> attributes = new HashMap<>();
    
    // 构造函数...
    
    // 现有方法...
    
    @Override
    public String getFirstName() {
        return firstName;
    }
    
    @Override
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    
    @Override
    public String getLastName() {
        return lastName;
    }
    
    @Override
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    
    @Override
    public Map<String, List<String>> getAttributes() {
        return attributes;
    }
    
    @Override
    public List<String> getAttribute(String name) {
        return attributes.getOrDefault(name, Collections.emptyList());
    }
    
    @Override
    public void setAttribute(String name, List<String> values) {
        attributes.put(name, values);
    }
    
    @Override
    public void removeAttribute(String name) {
        attributes.remove(name);
    }
    
    // 其他方法...
}

然后修改 CustomUserStorageProvider 类,在获取用户时加载这些属性:

java
@Override
public UserModel getUserByUsername(RealmModel realm, String username) {
    try (Connection connection = getConnection()) {
        String userTable = model.getConfig().getFirst("userTable");
        String usernameColumn = model.getConfig().getFirst("usernameColumn");
        String emailColumn = model.getConfig().getFirst("emailColumn");
        String firstNameColumn = model.getConfig().getFirst("firstNameColumn", "first_name");
        String lastNameColumn = model.getConfig().getFirst("lastNameColumn", "last_name");

        String sql = "SELECT * FROM " + userTable + " WHERE " + usernameColumn + " = ?";
        try (PreparedStatement stmt = connection.prepareStatement(sql)) {
            stmt.setString(1, username);
            try (ResultSet rs = stmt.executeQuery()) {
                if (rs.next()) {
                    String userUsername = rs.getString(usernameColumn);
                    String userEmail = rs.getString(emailColumn);
                    String userFirstName = rs.getString(firstNameColumn);
                    String userLastName = rs.getString(lastNameColumn);

                    CustomUserModel userModel = new CustomUserModel(realm, StorageId.keycloakId(model, userUsername), userUsername);
                    userModel.setEmail(userEmail);
                    userModel.setFirstName(userFirstName);
                    userModel.setLastName(userLastName);
                    userModel.setEnabled(true);
                    return userModel;
                }
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return null;
}

优化查询性能

可以根据实际数据库性能情况,优化 SQL 查询语句,例如添加索引、使用分页等。

示例

  1. 添加索引
sql
-- 为常用查询列添加索引
CREATE INDEX idx_username ON users(username);
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_enabled ON users(enabled);
  1. 优化 SQL 查询
java
// 优化搜索查询
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
    List<UserModel> users = new ArrayList<>();
    try (Connection connection = getConnection()) {
        String userTable = model.getConfig().getFirst("userTable");
        String usernameColumn = model.getConfig().getFirst("usernameColumn");
        String emailColumn = model.getConfig().getFirst("emailColumn");

        // 使用索引列进行搜索
        String sql = "SELECT " + usernameColumn + ", " + emailColumn + " FROM " + userTable + 
                    " WHERE " + usernameColumn + " LIKE ? OR " + emailColumn + " LIKE ?" +
                    " ORDER BY " + usernameColumn; // 添加排序以确保结果一致性
        
        String dbType = model.getConfig().getFirst("dbType");
        DatabaseDialect dialect = DatabaseDialectFactory.getDialect(dbType);
        String limitOffsetSql = dialect.getLimitOffsetSql(sql, maxResults != null ? maxResults : Integer.MAX_VALUE, firstResult != null ? firstResult : 0);

        try (PreparedStatement stmt = connection.prepareStatement(limitOffsetSql)) {
            String searchPattern = "%" + search + "%";
            stmt.setString(1, searchPattern);
            stmt.setString(2, searchPattern);
            try (ResultSet rs = stmt.executeQuery()) {
                while (rs.next()) {
                    String username = rs.getString(usernameColumn);
                    String email = rs.getString(emailColumn);

                    CustomUserModel userModel = new CustomUserModel(realm, StorageId.keycloakId(model, username), username);
                    userModel.setEmail(email);
                    userModel.setEnabled(true);
                    users.add(userModel);
                }
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return users.stream();
}

实现连接池管理

在生产环境中,应该使用连接池管理数据库连接,以提高性能和可靠性。

示例

java
public class CustomUserStorageProvider implements UserStorageProvider, UserLookupProvider, UserQueryProvider, CredentialInputValidator {
    // 现有属性...
    private DataSource dataSource;
    
    public CustomUserStorageProvider(KeycloakSession session, ComponentModel model) {
        this.session = session;
        this.model = model;
        this.dataSource = initDataSource();
    }
    
    private DataSource initDataSource() {
        try {
            String dbType = model.getConfig().getFirst("dbType");
            String connectionUrl = model.getConfig().getFirst("connectionUrl");
            String username = model.getConfig().getFirst("username");
            String password = model.getConfig().getFirst("password");
            
            // 使用 HikariCP 连接池
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl(connectionUrl);
            config.setUsername(username);
            config.setPassword(password);
            config.setDriverClassName(DatabaseDialectFactory.getDialect(dbType).getDriverClassName());
            
            // 连接池配置
            config.setMaximumPoolSize(10);
            config.setMinimumIdle(5);
            config.setIdleTimeout(30000);
            config.setMaxLifetime(1800000);
            
            return new HikariDataSource(config);
        } catch (Exception e) {
            throw new RuntimeException("Failed to initialize data source", e);
        }
    }
    
    private Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
    
    @Override
    public void close() {
        if (dataSource instanceof HikariDataSource) {
            ((HikariDataSource) dataSource).close();
        }
    }
    
    // 其他方法...
}

需要添加 HikariCP 依赖:

xml
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>4.0.3</version>
</dependency>

代码结构

spi-user-storage-extension/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── cc/bima/keycloak/extension/storage/
│   │   │       ├── CustomUserModel.java                   # 用户模型实现
│   │   │       ├── CustomUserStorageProvider.java          # 用户存储提供者实现
│   │   │       ├── CustomUserStorageProviderFactory.java   # 用户存储提供者工厂
│   │   │       └── dialect/                               # 数据库方言包
│   │   │           ├── DatabaseDialect.java                # 数据库方言接口
│   │   │           ├── DatabaseDialectFactory.java         # 数据库方言工厂
│   │   │           ├── MySQLDialect.java                  # MySQL 方言实现
│   │   │           ├── SQLServerDialect.java              # SQL Server 方言实现
│   │   │           ├── OracleDialect.java                 # Oracle 方言实现
│   │   │           ├── DamengDialect.java                 # 达梦方言实现
│   │   │           ├── KingbaseDialect.java               # 金仓方言实现
│   │   │           ├── OceanBaseDialect.java              # OceanBase 方言实现
│   │   │           └── GaussDBDialect.java                # GaussDB 方言实现
│   │   └── resources/
│   │       └── META-INF/
│   │           └── services/
│   │               └── org.keycloak.storage.UserStorageProviderFactory # 服务提供者配置
│   └── test/
│       └── java/
│           └── cc/bima/keycloak/userstorage/provider/
│               └── CustomUserStorageProviderTest.java     # 测试代码
└── pom.xml                                               # Maven 配置文件

部署与维护

1. 部署方式

1.1 标准部署

将编译好的 JAR 文件和数据库驱动 JAR 文件放入 Keycloak 的相应目录:

  • 扩展 JAR:standalone/deployments/
  • 数据库驱动:standalone/lib/

1.2 Docker 部署

如果使用 Docker 运行 Keycloak,可以将扩展 JAR 文件和数据库驱动 JAR 文件复制到容器中。

Dockerfile 示例

dockerfile
FROM quay.io/keycloak/keycloak:17.0.0

# 复制数据库驱动
COPY mysql-connector-java-8.0.28.jar /opt/keycloak/standalone/lib/

# 复制用户存储扩展
COPY spi-user-storage-extension-1.0.0.jar /opt/keycloak/standalone/deployments/

ENV KEYCLOAK_ADMIN=admin
ENV KEYCLOAK_ADMIN_PASSWORD=admin

ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start-dev"]

2. 监控与日志

2.1 日志配置

在 Keycloak 的 standalone/configuration/standalone.xml 文件中,可以配置日志级别:

xml
<logger category="cc.bima.keycloak.extension.storage" level="info"/>

2.2 监控指标

可以通过 Keycloak 的管理 API 或 JMX 监控扩展的运行状态。

3. 故障排除

3.1 常见问题

问题原因解决方案
扩展未加载JAR 文件未正确部署检查 JAR 文件是否在 standalone/deployments 目录中,是否有部署错误
数据库连接失败数据库驱动未部署或连接配置错误确保数据库驱动已放入 standalone/lib 目录,检查连接 URL、用户名和密码是否正确
用户查询失败SQL 语法错误或表结构不匹配检查数据库表结构是否符合要求,验证 SQL 语句是否正确
性能问题数据库查询未优化或连接池配置不当优化 SQL 查询,添加索引,配置合适的连接池参数

4. 性能优化

  1. 连接池配置:使用连接池管理数据库连接,设置合适的池大小和超时时间
  2. 索引优化:为常用查询列添加索引,提高查询速度
  3. SQL 优化:使用高效的 SQL 语句,避免全表扫描
  4. 缓存策略:实现用户缓存,减少数据库查询次数
  5. 分页查询:使用分页查询,避免一次性加载大量数据
  6. 批量操作:对于批量操作,使用批处理语句,减少数据库交互次数

5. 安全考虑

  1. 密码安全:建议对密码进行加密存储,避免明文存储密码
  2. 连接安全:使用 SSL 连接数据库,保护数据传输安全
  3. 权限控制:数据库用户应只具有必要的权限,避免使用管理员权限
  4. 输入验证:对用户输入进行验证,防止 SQL 注入攻击
  5. 审计日志:记录关键操作,便于安全审计和故障排查

注意事项

  1. 数据库驱动:确保数据库驱动已添加到 Keycloak 类路径中,且版本与数据库兼容
  2. 连接池管理:生产环境中应使用连接池管理数据库连接,避免频繁创建和关闭连接
  3. 性能优化:建议对数据库查询进行性能优化,添加适当的索引,避免全表扫描
  4. 密码安全:注意密码存储安全,建议使用加密存储密码,如 BCrypt 等
  5. 数据备份:定期备份用户数据,确保数据安全
  6. 版本兼容性:注意 Keycloak 版本与扩展版本的兼容性,避免因版本不匹配导致的问题
  7. 测试:在生产环境部署前,进行充分的测试,确保功能正常
  8. 文档:维护详细的文档,记录配置选项和使用方法
  9. 监控:定期监控扩展的运行状态,及时发现和解决问题
  10. 合规性:确保用户数据处理符合相关法律法规要求

免责声明

本项目基于 GitHub 开源软件进行定制化开发,旨在为企业和开发者提供更便捷的项目基座解决方案。使用本项目时,请务必了解以下免责声明:

  1. 开源基础:本项目基于 GitHub 开源软件构建,遵循原开源协议的相关规定。
  2. 定制开发:我们对原开源软件进行了定制和扩展,以提供更优质的开发体验和功能支持。
  3. 责任限制:对于使用本项目可能产生的任何直接或间接的经济损失、数据丢失或其他问题,北京必码科技工作室不承担任何责任。
  4. 使用建议:在生产环境中使用本项目前,请进行充分的测试和验证,确保其符合您的业务需求和安全要求。
  5. 技术支持:我们提供技术支持服务,但不保证解决所有可能出现的问题。
  6. 合规使用:用户应确保在使用本项目时遵守相关法律法规和行业规范,不得用于任何违法或违规用途。