Keycloakを既存のシステムに統合することにより、認証中にユーザーに関する情報をかなり凝った形式で保存できる古いデータベースからユーザーをロードしなければならない可能性が高くなります。このタスクは、独自のユーザープロバイダー(Keycloak用語ではユーザーフェデレーションプロバイダー)を作成することで解決されます。以下は、そのようなプロバイダーを作成するための短いガイドです。
Keycloakに慣れていない場合は、ウィキペディアからの引用をご覧ください。
Keycloak は、最新のアプリケーションとサービスを対象とした、アクセス制御を備えたオープンソースのシングルサインオン製品です。
現代のマイクロサービスの世界では、Keycloakは主にOAuth 2.0プロバイダーとして興味深いものであり、クライアントにトークンを発行して特定のサービスにアクセスできます。
技術的には、KeycloakはWildFlyサーバー内のWebアプリケーションであり、血まみれの企業の記憶から誰かに鳥肌を与えることができます。しかし、十分な理論、それはあなたの袖をまくり上げる時です!
Keycloakプラグインは、小さなWARパッケージアプリケーションになります。ビルドするには、Java 8で十分です。ビルドツールとしてGradleを使用し、依存関係で次のモジュールを指定します。
compileOnly "org.keycloak:keycloak-core:12.0.3"
compileOnly "org.keycloak:keycloak-server-spi:12.0.3"
compileOnly "org.jboss.logging:jboss-logging:3.4.1.Final"
implementation "org.springframework:spring-core:5.3.3"
implementation "org.springframework:spring-jdbc:5.3.3"
implementation "org.springframework.security:spring-security-core:5.4.4"
testImplementation platform('org.junit:junit-bom:5.7.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.mockito:mockito-core:3.7.7'
testRuntimeOnly 'javax.ws.rs:javax.ws.rs-api:2.1.1'
testRuntimeOnly 'com.h2database:h2:1.4.200'
Spring Framework , , Spring. , .
- Keycloak. , Keycloak .
JBoss Logging, "" WildFly. , , - , SLF4J.
io.freefair.lombok
.
Keycloak org.keycloak.storage.UserStorageProvider
, , . , , , org.keycloak.storage.user.UserLookupProvider
org.keycloak.credential.CredentialInputValidator
. , . - . .
org.keycloak.models.UserModel
( ). org.keycloak.storage.adapter.AbstractUserAdapter
, org.keycloak.models.UserModel
:
public class LegacyDatabaseUserModel extends AbstractUserAdapter {
public static final String ATTRIBUTE_PASSWORD = "password";
private final MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
private final Set<RoleModel> roles;
private LegacyDatabaseUserModel(Builder builder) {
super(builder.session, builder.realm, builder.storageProviderModel);
this.attributes.putSingle(UserModel.USERNAME, builder.username);
this.attributes.putSingle(UserModel.FIRST_NAME, builder.firstName);
this.attributes.putSingle(UserModel.LAST_NAME, builder.lastName);
this.attributes.putSingle(ATTRIBUTE_PASSWORD, builder.password);
this.roles = Collections.unmodifiableSet(builder.roles);
}
public static Builder builder() {
return new Builder();
}
@Override
public String getUsername() {
return getFirstAttribute(UserModel.USERNAME);
}
@Override
public String getFirstName() {
return getFirstAttribute(UserModel.FIRST_NAME);
}
@Override
public String getLastName() {
return getFirstAttribute(UserModel.LAST_NAME);
}
@Override
public Map<String, List<String>> getAttributes() {
return new MultivaluedHashMap<>(attributes);
}
@Override
public String getFirstAttribute(String name) {
return attributes.getFirst(name);
}
@Override
public List<String> getAttribute(String name) {
return attributes.get(name);
}
@Override
protected Set<RoleModel> getRoleMappingsInternal() {
return roles;
}
public static class Builder {
...
}
}
, , , - . Map
, , - .
org.keycloak.models.RoleModel
. , :
@AllArgsConstructor
public class LegacyDatabaseRoleModel implements RoleModel {
@Getter
private final RoleContainerModel container;
@Getter
private final String name;
@Override
public String getId() {
return getName();
}
@Override
public void setName(String name) {
throw new ReadOnlyException("Role is read only for this update");
}
@Override
public String getDescription() {
return null;
}
@Override
public void setDescription(String description) {
throw new ReadOnlyException("Role is read only for this update");
}
@Override
public boolean isComposite() {
return false;
}
@Override
public void addCompositeRole(RoleModel role) {
throw new ReadOnlyException("Role is read only for this update");
}
@Override
public void removeCompositeRole(RoleModel role) {
throw new ReadOnlyException("Role is read only for this update");
}
@Override
public Stream<RoleModel> getCompositesStream() {
return Stream.empty();
}
@Override
public boolean isClientRole() {
return false;
}
@Override
public String getContainerId() {
return container.getId();
}
@Override
public boolean hasRole(RoleModel role) {
return false;
}
@Override
public Map<String, List<String>> getAttributes() {
return Collections.emptyMap();
}
@Override
public void setSingleAttribute(String name, String value) {
throw new ReadOnlyException("Role is read only for this update");
}
@Override
public void setAttribute(String name, List<String> values) {
throw new ReadOnlyException("Role is read only for this update");
}
@Override
public void removeAttribute(String name) {
throw new ReadOnlyException("Role is read only for this update");
}
@Override
public Stream<String> getAttributeStream(String name) {
return Stream.empty();
}
}
, . , - .
. . . :
private final ConcurrentMap<UserModelKey, LegacyDatabaseUserModel> loadedUsers = new ConcurrentHashMap<>();
@Override
public LegacyDatabaseUserModel getUserByUsername(String username, RealmModel realm) {
UserModelKey userKey = new UserModelKey(username, realm.getId());
return loadedUsers.computeIfAbsent(userKey, k -> {
LegacyDatabaseUserModel user = findUserByName(username, realm);
if (user != null) {
log.debugv("User is loaded by name \"{0}\"", username);
}
return user;
});
}
, Keycloak . java.util.concurrent.ConcurrentMap
. findUserByName
, org.springframework.jdbc.core.JdbcTemplate
org.springframework.jdbc.core.ResultSetExtractor
, , .
private LegacyDatabaseUserModel findUserByName(String username, RealmModel realm) {
return jdbcTemplate.query(SQL_FIND_USER_BY_NAME, new Object[]{username}, new int[]{Types.VARCHAR},
new LegacyDatabaseUserModelResultSetExtractor(realm));
}
@RequiredArgsConstructor
private class LegacyDatabaseUserModelResultSetExtractor implements ResultSetExtractor<LegacyDatabaseUserModel> {
final RealmModel realm;
@Override
public LegacyDatabaseUserModel extractData(ResultSet rs) throws SQLException, DataAccessException {
if (!rs.next()) {
return null;
}
LegacyDatabaseUserModel.Builder userModelBuilder = LegacyDatabaseUserModel.builder()
.session(session)
.realm(realm)
.storageProviderModel(storageProviderModel)
.username(rs.getString(1))
.password(rs.getString(2))
.firstName(rs.getString(3))
.lastName(rs.getString(4))
.withRole(new LegacyDatabaseRoleModel(realm, rs.getString(5)));
while (rs.next()) {
userModelBuilder.withRole(new LegacyDatabaseRoleModel(realm, rs.getString(5)));
}
return userModelBuilder.build();
}
}
. :
@Override
public LegacyDatabaseUserModel getUserById(String id, RealmModel realm) {
StorageId storageId = new StorageId(id);
String username = storageId.getExternalId();
return getUserByUsername(username, realm);
}
: f:<storageProvideId>:<username>
. org.keycloak.storage.StorageId
.
, :
@Override
public boolean isValid(RealmModel realm, UserModel userModel, CredentialInput credentialInput) {
if (!supportsCredentialType(credentialInput.getType())) {
log.debugv("Credential type \"{0}\" is not supported", credentialInput.getType());
return false;
}
String password = user.getFirstAttribute(LegacyDatabaseUserModel.ATTRIBUTE_PASSWORD);
return passwordEncoder.matches(credentialInput.getChallengeResponse(), password);
}
, , ( ). . - Map
. org.keycloak.models.UserModel
, , "" isValid
com.habr.keycloak.model.LegacyDatabaseUserModel
- . org.springframework.security.crypto.password.PasswordEncoder
.
Keycloak . . org.keycloak.storage.UserStorageProviderFactory
. :
;
.
@Override
public void init(Config.Scope config) {
initDataSource();
initPasswordEncoder();
}
:
private PropertySource<Map<String, Object>> getPropertySource() {
if (propertySource == null) {
propertySource = getDefaultPropertySource();
}
return propertySource;
}
private PropertySource<Map<String, Object>> getDefaultPropertySource() {
return new PropertiesPropertySource("default", System.getProperties());
}
, Keycloak-way *.properties
, standalone.xml
:
@Override
public void init(Config.Scope config) {
String propertyFilePath = config.get("property-file-path");
...
.
, :
private void initDataSource() {
String driverClassName = getDataSourceDriverClassName();
String url = getDataSourceUrl();
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
try {
dataSource.setDriverClass((Class<? extends Driver>) Class.forName(driverClassName));
dataSource.setUrl(url);
dataSource.setUsername(getDataSourceUsername());
dataSource.setPassword(getDataSourcePassword());
this.dataSource = dataSource;
log.debugv("Data source to connect with database \"{0}\" is created", url);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("JDBC driver class \"" + driverClassName + "\" is not found", e);
}
}
: WAR- , Keycloak. , , . , . Keycloak.
org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
.
- Factory Finder', META-INF/services
org.keycloak.storage.UserStorageProviderFactory
, .
前述のように、プラグインは特定のKeycloakモジュールからデータベースドライバーをロードします。このモジュールに依存していることをKeycloakに伝えるにjboss-deployment-structure.xml
は、ディレクトリにファイルを追加で作成する必要がありますMETA-INF
。
<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure>
<deployment>
<dependencies>
<module name="org.postgresql"/>
</dependencies>
</deployment>
</jboss-deployment-structure>
Keycloakがプラグインを取得するには、プラグイン(プラグイン)をディレクトリに配置する必要があります$KEYCLOAK_HOME/standalone/deployments
。プラグインがKeycloak管理パネルの[ユーザーフェデレーション]セクションで正常にデプロイされた場合、識別子を持つプロバイダーを追加habr.legacy-database
できます。その後、トークンの発行を開始できます。
それで全部です。清聴ありがとうございました!