Appearance
SPI User Storage Extension 说明文档
功能概述
spi-user-storage-extension 是一个 Keycloak 扩展,实现了 Keycloak 的用户存储 SPI,用于从外部数据库中读取用户信息,支持多种数据库类型,包括 MySQL、SQL Server、Oracle、达梦、金仓、OceanBase 和 GaussDB。
该扩展为 Keycloak 提供了灵活的用户存储能力,允许企业将现有的用户数据无缝集成到 Keycloak 中,无需进行数据迁移,同时保持用户认证的一致性。
界面预览


技术支持
- 技术支持: 北京必码科技工作室
- 官方文档: https://bima.cc
- 官方店铺: https://bima.taobao.com
- 联系邮箱: bima.cc@qq.com
- 联系微信: e18929958
核心组件
1. CustomUserStorageProvider
功能:实现了 Keycloak 的 UserStorageProvider、UserLookupProvider、UserQueryProvider 和 CredentialInputValidator 接口,负责从外部数据库中读取用户信息并验证用户凭证。
主要方法:
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- 用户IDusername- 用户名email- 邮箱地址enabled- 是否启用
主要方法:
getId():获取用户IDgetUsername():获取用户名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 管理控制台配置
- 登录 Keycloak 管理控制台
- 进入 Realm 设置 → 用户 federation
- 添加
bima-spi-user-storage-extension提供者 - 配置数据库连接信息:
- 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)
- 保存配置
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 登录测试
- 启动 Keycloak 服务器
- 打开 Keycloak 登录页面
- 输入外部数据库中存在的用户名和密码
- 点击登录
- 验证是否成功登录
5.2 用户搜索测试
- 登录 Keycloak 管理控制台
- 进入用户管理页面
- 尝试搜索外部数据库中的用户
- 验证是否能正确显示用户信息
扩展与定制
支持新的数据库类型
- 实现 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;
}
}- 在 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 查询语句,例如添加索引、使用分页等。
示例:
- 添加索引
sql
-- 为常用查询列添加索引
CREATE INDEX idx_username ON users(username);
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_enabled ON users(enabled);- 优化 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. 性能优化
- 连接池配置:使用连接池管理数据库连接,设置合适的池大小和超时时间
- 索引优化:为常用查询列添加索引,提高查询速度
- SQL 优化:使用高效的 SQL 语句,避免全表扫描
- 缓存策略:实现用户缓存,减少数据库查询次数
- 分页查询:使用分页查询,避免一次性加载大量数据
- 批量操作:对于批量操作,使用批处理语句,减少数据库交互次数
5. 安全考虑
- 密码安全:建议对密码进行加密存储,避免明文存储密码
- 连接安全:使用 SSL 连接数据库,保护数据传输安全
- 权限控制:数据库用户应只具有必要的权限,避免使用管理员权限
- 输入验证:对用户输入进行验证,防止 SQL 注入攻击
- 审计日志:记录关键操作,便于安全审计和故障排查
注意事项
- 数据库驱动:确保数据库驱动已添加到 Keycloak 类路径中,且版本与数据库兼容
- 连接池管理:生产环境中应使用连接池管理数据库连接,避免频繁创建和关闭连接
- 性能优化:建议对数据库查询进行性能优化,添加适当的索引,避免全表扫描
- 密码安全:注意密码存储安全,建议使用加密存储密码,如 BCrypt 等
- 数据备份:定期备份用户数据,确保数据安全
- 版本兼容性:注意 Keycloak 版本与扩展版本的兼容性,避免因版本不匹配导致的问题
- 测试:在生产环境部署前,进行充分的测试,确保功能正常
- 文档:维护详细的文档,记录配置选项和使用方法
- 监控:定期监控扩展的运行状态,及时发现和解决问题
- 合规性:确保用户数据处理符合相关法律法规要求
免责声明
本项目基于 GitHub 开源软件进行定制化开发,旨在为企业和开发者提供更便捷的项目基座解决方案。使用本项目时,请务必了解以下免责声明:
- 开源基础:本项目基于 GitHub 开源软件构建,遵循原开源协议的相关规定。
- 定制开发:我们对原开源软件进行了定制和扩展,以提供更优质的开发体验和功能支持。
- 责任限制:对于使用本项目可能产生的任何直接或间接的经济损失、数据丢失或其他问题,北京必码科技工作室不承担任何责任。
- 使用建议:在生产环境中使用本项目前,请进行充分的测试和验证,确保其符合您的业务需求和安全要求。
- 技术支持:我们提供技术支持服务,但不保证解决所有可能出现的问题。
- 合规使用:用户应确保在使用本项目时遵守相关法律法规和行业规范,不得用于任何违法或违规用途。