Initial commit
This commit is contained in:
14
.env.example
Normal file
14
.env.example
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
|
||||||
|
KC_DB_USERNAME=keycloak_user
|
||||||
|
KC_DB_PASSWORD=''
|
||||||
|
|
||||||
|
KC_BOOTSTRAP_ADMIN_USERNAME=admin
|
||||||
|
KC_BOOTSTRAP_ADMIN_PASSWORD=''
|
||||||
|
|
||||||
|
KC_HOSTNAME=https://account.example.com
|
||||||
|
KC_HTTP_ENABLED=true
|
||||||
|
KC_PROXY_HEADERS=xforwarded
|
||||||
|
|
||||||
|
#JAVA_OPTS_KC_HEAP='-XX:MaxHeapFreeRatio=30 -XX:MaxRAMPercentage=65'
|
||||||
|
JAVA_OPTS_KC_HEAP='-Xms512m -Xmx768m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC'
|
||||||
|
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
target/
|
||||||
|
.env
|
||||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
FROM quay.io/keycloak/keycloak:26.4.0 AS builder
|
||||||
|
|
||||||
|
ENV KC_DB=postgres
|
||||||
|
ENV KC_FEATURES=hostname:v2
|
||||||
|
ENV KC_HEALTH_ENABLED=true
|
||||||
|
ENV KC_METRICS_ENABLED=true
|
||||||
|
|
||||||
|
WORKDIR /opt/keycloak
|
||||||
|
|
||||||
|
# MySQL JDBC driver
|
||||||
|
ADD --chown=keycloak:keycloak --chmod=644 \
|
||||||
|
https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/8.4.0/mysql-connector-j-8.4.0.jar \
|
||||||
|
/opt/keycloak/providers/
|
||||||
|
|
||||||
|
# Providers
|
||||||
|
COPY --chown=keycloak:keycloak --chmod=644 \
|
||||||
|
./meta-user-provider/target/meta-user-provider-1.0.0.jar \
|
||||||
|
/opt/keycloak/providers/
|
||||||
|
|
||||||
|
RUN /opt/keycloak/bin/kc.sh build
|
||||||
|
|
||||||
|
FROM quay.io/keycloak/keycloak:26.4.0
|
||||||
|
COPY --from=builder /opt/keycloak/ /opt/keycloak/
|
||||||
|
|
||||||
|
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
|
||||||
|
|
||||||
18
LICENSE
Normal file
18
LICENSE
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 y9938
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
21
README.md
Normal file
21
README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# keycloak-setup
|
||||||
|
|
||||||
|
> Docker setup + SPI (connect to MySQL)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reverse proxy (Caddy)
|
||||||
|
|
||||||
|
`Caddyfile`
|
||||||
|
|
||||||
|
```
|
||||||
|
account.yourdomain.com {
|
||||||
|
reverse_proxy keycloak:8080
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
26
compose.yaml
Normal file
26
compose.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
services:
|
||||||
|
keycloak:
|
||||||
|
build: .
|
||||||
|
container_name: keycloak
|
||||||
|
restart: unless-stopped
|
||||||
|
command: start --optimized
|
||||||
|
env_file: .env
|
||||||
|
# mem_limit: 768M
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
expose:
|
||||||
|
- "8080"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:9000:9000" # Health/Metrics
|
||||||
|
networks:
|
||||||
|
- db
|
||||||
|
- proxify
|
||||||
|
|
||||||
|
networks:
|
||||||
|
db:
|
||||||
|
name: db
|
||||||
|
external: true
|
||||||
|
proxify:
|
||||||
|
name: proxify
|
||||||
|
external: true
|
||||||
|
|
||||||
1
meta-user-provider/.gitignore
vendored
Normal file
1
meta-user-provider/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
19
meta-user-provider/README.md
Normal file
19
meta-user-provider/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Meta User Storage Provider
|
||||||
|
|
||||||
|
[Keycloak](https://www.keycloak.org/) ([User Storage SPI](https://www.keycloak.org/docs/latest/server_development/index.html#_user-storage-spi))
|
||||||
|
|
||||||
|
## Читать
|
||||||
|
|
||||||
|
[DOCS](https://www.keycloak.org/documentation)
|
||||||
|
|
||||||
|
[Quickstarts](https://github.com/keycloak/keycloak-quickstarts#)
|
||||||
|
|
||||||
|
[Maven Repository](https://mvnrepository.com/)
|
||||||
|
|
||||||
|
## Сборка (JAR-файл)
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -it --rm -v "$(pwd)":/app -w /app maven:3.9.11-eclipse-temurin-17-alpine mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
`target/meta-user-provider-{version}.jar`
|
||||||
65
meta-user-provider/pom.xml
Normal file
65
meta-user-provider/pom.xml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>com.example</groupId>
|
||||||
|
<artifactId>meta-user-provider</artifactId>
|
||||||
|
<name>Meta User Storage Provider</name>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<description>Custom User Storage Provider for Keycloak</description>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>17</source>
|
||||||
|
<target>17</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.4.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-core</artifactId>
|
||||||
|
<version>26.4.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-server-spi</artifactId>
|
||||||
|
<version>26.4.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-server-spi-private</artifactId>
|
||||||
|
<version>26.4.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-services</artifactId>
|
||||||
|
<version>26.4.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<keycloak.version>26.4.0</keycloak.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.example.keycloak;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.storage.ReadOnlyException;
|
||||||
|
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
|
||||||
|
|
||||||
|
public class MetaUserModel extends AbstractUserAdapterFederatedStorage {
|
||||||
|
|
||||||
|
private final String userId;
|
||||||
|
private final String username;
|
||||||
|
private final String email;
|
||||||
|
private final String fullName;
|
||||||
|
private final boolean enabled;
|
||||||
|
|
||||||
|
public MetaUserModel(KeycloakSession session, RealmModel realm, ComponentModel storageProviderModel,
|
||||||
|
String userId, String username, String email, String fullName, boolean enabled) {
|
||||||
|
super(session, realm, storageProviderModel);
|
||||||
|
this.userId = userId;
|
||||||
|
this.username = username;
|
||||||
|
this.email = email;
|
||||||
|
this.fullName = fullName;
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFirstName() {
|
||||||
|
// Извлекаем первое слово из full_name как имя
|
||||||
|
if (fullName != null && !fullName.trim().isEmpty()) {
|
||||||
|
String[] names = fullName.split("\\s+");
|
||||||
|
return names.length > 0 ? names[0] : "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLastName() {
|
||||||
|
// Извлекаем все кроме первого слова как фамилию
|
||||||
|
if (fullName != null && !fullName.trim().isEmpty()) {
|
||||||
|
String[] names = fullName.split("\\s+");
|
||||||
|
if (names.length > 1) {
|
||||||
|
StringBuilder lastName = new StringBuilder();
|
||||||
|
for (int i = 1; i < names.length; i++) {
|
||||||
|
if (i > 1) lastName.append(" ");
|
||||||
|
lastName.append(names[i]);
|
||||||
|
}
|
||||||
|
return lastName.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEmail(String email) {
|
||||||
|
throw new ReadOnlyException("User is read-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUsername(String username) {
|
||||||
|
throw new ReadOnlyException("User is read-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFirstName(String firstName) {
|
||||||
|
throw new ReadOnlyException("User is read-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLastName(String lastName) {
|
||||||
|
throw new ReadOnlyException("User is read-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
throw new ReadOnlyException("User is read-only");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
package com.example.keycloak;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.credential.CredentialInput;
|
||||||
|
import org.keycloak.credential.CredentialInputValidator;
|
||||||
|
import org.keycloak.models.*;
|
||||||
|
import org.keycloak.storage.StorageId;
|
||||||
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
|
import org.keycloak.storage.user.UserLookupProvider;
|
||||||
|
import org.keycloak.storage.user.UserQueryProvider;
|
||||||
|
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||||
|
import org.keycloak.models.UserCredentialModel;
|
||||||
|
import org.mindrot.jbcrypt.BCrypt;
|
||||||
|
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class MetaUserStorageProvider implements
|
||||||
|
UserStorageProvider,
|
||||||
|
UserLookupProvider,
|
||||||
|
CredentialInputValidator,
|
||||||
|
UserQueryProvider {
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final ComponentModel model;
|
||||||
|
|
||||||
|
public MetaUserStorageProvider(KeycloakSession session, ComponentModel model) {
|
||||||
|
this.session = session;
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// Clean up resources if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserLookupProvider methods
|
||||||
|
@Override
|
||||||
|
public UserModel getUserById(RealmModel realm, String id) {
|
||||||
|
StorageId storageId = new StorageId(id);
|
||||||
|
String userId = storageId.getExternalId();
|
||||||
|
return getUserByExternalId(realm, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserModel getUserByExternalId(RealmModel realm, String userId) {
|
||||||
|
try (Connection connection = MetaUserStorageProviderFactory.getDatabaseConnection(model)) {
|
||||||
|
String sql = "SELECT * FROM keycloak_users WHERE user_id = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
stmt.setString(1, userId);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return createUserModel(realm, rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
System.err.println("Ошибка при поиске пользователя по ID: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserModel getUserByUsername(RealmModel realm, String username) {
|
||||||
|
try (Connection connection = MetaUserStorageProviderFactory.getDatabaseConnection(model)) {
|
||||||
|
String sql = "SELECT * FROM keycloak_users WHERE username = ? OR email = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
stmt.setString(1, username);
|
||||||
|
stmt.setString(2, username);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return createUserModel(realm, rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
System.err.println("Ошибка при поиске пользователя по username: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserModel getUserByEmail(RealmModel realm, String email) {
|
||||||
|
// Используем тот же метод, что и для username, так как email тоже уникален
|
||||||
|
return getUserByUsername(realm, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialInputValidator methods
|
||||||
|
@Override
|
||||||
|
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
|
||||||
|
return PasswordCredentialModel.TYPE.equals(credentialType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsCredentialType(String credentialType) {
|
||||||
|
return PasswordCredentialModel.TYPE.equals(credentialType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
|
||||||
|
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserCredentialModel cred = (UserCredentialModel) input;
|
||||||
|
String inputPassword = cred.getChallengeResponse();
|
||||||
|
|
||||||
|
try (Connection connection = MetaUserStorageProviderFactory.getDatabaseConnection(model)) {
|
||||||
|
String sql = "SELECT password_hash FROM keycloak_users WHERE username = ? OR email = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
stmt.setString(1, user.getUsername());
|
||||||
|
stmt.setString(2, user.getUsername());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
String hashedPassword = rs.getString("password_hash");
|
||||||
|
if (hashedPassword == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Заменяем $2y$ на $2a$ для совместимости с jbcrypt
|
||||||
|
if (hashedPassword.startsWith("$2y$")) {
|
||||||
|
hashedPassword = "$2a$" + hashedPassword.substring(4);
|
||||||
|
}
|
||||||
|
// Проверяем пароль с помощью BCrypt (как в Laravel)
|
||||||
|
return BCrypt.checkpw(inputPassword, hashedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
System.err.println("Ошибка при проверке пароля: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Ошибка при проверке BCrypt: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserQueryProvider methods
|
||||||
|
@Override
|
||||||
|
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
|
||||||
|
try (Connection connection = MetaUserStorageProviderFactory.getDatabaseConnection(model)) {
|
||||||
|
String sql = "SELECT * FROM keycloak_users WHERE username LIKE ? OR email LIKE ? OR full_name LIKE ? " +
|
||||||
|
"ORDER BY username LIMIT ? OFFSET ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
String searchPattern = "%" + search + "%";
|
||||||
|
stmt.setString(1, searchPattern);
|
||||||
|
stmt.setString(2, searchPattern);
|
||||||
|
stmt.setString(3, searchPattern);
|
||||||
|
stmt.setInt(4, maxResults != null ? maxResults : 50);
|
||||||
|
stmt.setInt(5, firstResult != null ? firstResult : 0);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
return createUserModelStream(realm, rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
System.err.println("Ошибка при поиске пользователей: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults) {
|
||||||
|
// Поддержка поиска по конкретным параметрам
|
||||||
|
String search = params.get(UserModel.SEARCH);
|
||||||
|
if (search != null && !search.trim().isEmpty()) {
|
||||||
|
return searchForUserStream(realm, search, firstResult, maxResults);
|
||||||
|
}
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) {
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) {
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getUsersCount(RealmModel realm) {
|
||||||
|
try (Connection connection = MetaUserStorageProviderFactory.getDatabaseConnection(model)) {
|
||||||
|
String sql = "SELECT COUNT(*) as count FROM keycloak_users";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getInt("count");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
System.err.println("Ошибка при подсчете пользователей: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getUsersCount(RealmModel realm, boolean includeServiceAccount) {
|
||||||
|
return getUsersCount(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательные методы
|
||||||
|
private UserModel createUserModel(RealmModel realm, ResultSet rs) throws SQLException {
|
||||||
|
String userId = rs.getString("user_id");
|
||||||
|
String username = rs.getString("username");
|
||||||
|
String email = rs.getString("email");
|
||||||
|
String fullName = rs.getString("full_name");
|
||||||
|
boolean enabled = rs.getBoolean("enabled");
|
||||||
|
|
||||||
|
return new MetaUserModel(session, realm, model, userId, username, email, fullName, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<UserModel> createUserModelStream(RealmModel realm, ResultSet rs) throws SQLException {
|
||||||
|
Stream.Builder<UserModel> streamBuilder = Stream.builder();
|
||||||
|
while (rs.next()) {
|
||||||
|
streamBuilder.add(createUserModel(realm, rs));
|
||||||
|
}
|
||||||
|
return streamBuilder.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.example.keycloak;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||||
|
import org.keycloak.storage.UserStorageProviderFactory;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MetaUserStorageProviderFactory
|
||||||
|
implements UserStorageProviderFactory<MetaUserStorageProvider> {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "meta-user-provider";
|
||||||
|
|
||||||
|
// Конфигурационные свойства
|
||||||
|
private static final List<ProviderConfigProperty> configProperties;
|
||||||
|
|
||||||
|
static {
|
||||||
|
configProperties = ProviderConfigurationBuilder.create()
|
||||||
|
.property().name("dbUrl")
|
||||||
|
.label("Database URL")
|
||||||
|
.type(ProviderConfigProperty.STRING_TYPE)
|
||||||
|
.defaultValue("jdbc:mysql://host.docker.internal:3306/app")
|
||||||
|
.helpText("JDBC URL для подключения к MySQL")
|
||||||
|
.add()
|
||||||
|
.property().name("dbUsername")
|
||||||
|
.label("Database Username")
|
||||||
|
.type(ProviderConfigProperty.STRING_TYPE)
|
||||||
|
.defaultValue("laravel")
|
||||||
|
.helpText("Пользователь с доступом к DB")
|
||||||
|
.add()
|
||||||
|
.property().name("dbPassword")
|
||||||
|
.label("Database Password")
|
||||||
|
.type(ProviderConfigProperty.PASSWORD)
|
||||||
|
.defaultValue("")
|
||||||
|
.helpText("Пароль пользователя")
|
||||||
|
.add()
|
||||||
|
.property().name("usersTable")
|
||||||
|
.label("Users Table")
|
||||||
|
.type(ProviderConfigProperty.STRING_TYPE)
|
||||||
|
.defaultValue("users")
|
||||||
|
.helpText("Название таблицы пользователей")
|
||||||
|
.add()
|
||||||
|
.property().name("membersTable")
|
||||||
|
.label("Members Table")
|
||||||
|
.type(ProviderConfigProperty.STRING_TYPE)
|
||||||
|
.defaultValue("members")
|
||||||
|
.helpText("Название вспомогательной таблицы с фио пользователей")
|
||||||
|
.add()
|
||||||
|
.property().name("viewName")
|
||||||
|
.label("View Name")
|
||||||
|
.type(ProviderConfigProperty.STRING_TYPE)
|
||||||
|
.defaultValue("keycloak_users")
|
||||||
|
.helpText("Название VIEW для пользователей")
|
||||||
|
.add()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MetaUserStorageProvider create(KeycloakSession session, ComponentModel model) {
|
||||||
|
return new MetaUserStorageProvider(session, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Meta User Storage Provider для Laravel MySQL";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return configProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательный метод для тестирования подключения
|
||||||
|
public static Connection getDatabaseConnection(ComponentModel config) {
|
||||||
|
try {
|
||||||
|
String url = config.getConfig().getFirst("dbUrl");
|
||||||
|
String username = config.getConfig().getFirst("dbUsername");
|
||||||
|
String password = config.getConfig().getFirst("dbPassword");
|
||||||
|
|
||||||
|
return DriverManager.getConnection(url, username, password);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Ошибка подключения к БД", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
com.example.keycloak.MetaUserStorageProviderFactory
|
||||||
Reference in New Issue
Block a user