Динамическое управление доступом: Роли пользователей на лету с Spring Boot и Keycloak.

Салимжанов Р.Д.

Введение

В современном приложении крайне важно организовать безопасный доступ пользователей к различным ресурсам в зависимости от их роли. В этой статье мы рассмотрим, как реализовать ролевую модель доступа, используя Spring Boot, PostgreSQL и Keycloak в качестве сервера авторизации.

В предыдущей статье я уже разбирал, как настроить Keycloak и Spring Security на Java. На основе того приложения мы добавим ролевую модель, так чтобы роли пользователей извлекались динамически из базы данных PostgreSQL, а не хранились статически в токенах. Такой подход обеспечит большую гибкость и безопасность, позволяя изменять роли пользователей без необходимости пересоздания токенов.

Предыдущая статья:

Настройка Keycloak, базы данных и приложения.

Итак, я не вносил изменений в файл docker-compose.yml, который мы будем использовать для запуска Keycloak. Как и в прошлый раз, запускаем контейнер с помощью команды docker-compose up . После успешного запуска контейнера вы сможете получить доступ к Keycloak по адресу http://localhost:8080. Далее мы можем продолжить настройку Keycloak в соответствии с требованиями.

Если вдруг он уже настроен, вы можете работать в нем, или удалить контейнеры, сети, образы и данные, чтобы очистить окружение. Выбор за вами. Я почистил и настроил заново:

Realm name: app_realm

Client ID: spring-app

Users: test_user

Password: test123

Подробную настройку можно почитать в моей предыдущей статье.

Далее Настройка Mappers (Мапперов токена).

Более подробно мы рассмотрим, как настроить мапперы токена в Keycloak для добавления свойства preferred_username в токен доступа. Это необходимо для того, чтобы приложение могло получать и использовать имя пользователя в удобном формате, что упрощает управление доступом и персонализацию пользовательского опыта.

1)В клиенте spring-app перейдите на вкладку "Client Scopes": Это позволит вам управлять областями доступа, которые определяют, какие данные будут включены в токены для вашего клиента.

Динамическое управление доступом: Роли пользователей на лету с Spring Boot и Keycloak.

2)Выберите "spring-app-dedicated" вашего клиента: Выбор конкретной области доступа гарантирует, что изменения будут применены только к нужному клиенту.

3)Нажмите "Add mapper(By configuration)": Это действие позволит вам создать новый маппер, который будет добавлять необходимые данные в токен.

Динамическое управление доступом: Роли пользователей на лету с Spring Boot и Keycloak.

4)Выберите "User Property": Этот тип маппера позволяет извлекать свойства пользователя из базы данных Keycloak.

5) Заполните поля и сохраните:

Динамическое управление доступом: Роли пользователей на лету с Spring Boot и Keycloak.

Эта настройка позволит вашему приложению получать имя пользователя в токене, что может быть полезно для аутентификации, авторизации и персонализации интерфейса пользователя.

Щя покажу, как это работает:

Для начала сгенерируем токен.

curl -X POST http://localhost:8080/realms/app_realm/protocol/openid-connect/token -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=spring-app" -d "client_secret=ВАШ_КЛИЕНТСКИЙ_СЕКРЕТ" -d "username=test_user" -d "password=test123" -d "grant_type=password"

Динамическое управление доступом: Роли пользователей на лету с Spring Boot и Keycloak.

Скопируйте access_token из ответа.

Вставьте на https://jwt.io

Если все правильно, то вы должны увидеть "preferred_username": "test_user".

Динамическое управление доступом: Роли пользователей на лету с Spring Boot и Keycloak.

Подготовка базы данных

Создадим базу данных для хранения пользователей и их ролей. Схема включает три таблицы:

roles — хранит роли (USER, ADMIN, MODERATOR).

users — хранит информацию о пользователях (username, email).

user_roles — связывает пользователей и их роли.

SQL-запросы для создания базы данных

-- 1. Создаем базу данных CREATE DATABASE security_roles; -- 2. Создаем таблицу пользователей CREATE TABLE users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL ); -- 3. Создаем таблицу ролей CREATE TABLE roles ( id SERIAL PRIMARY KEY, name VARCHAR(50) UNIQUE NOT NULL ); -- 4. Создаем таблицу связи пользователей и ролей CREATE TABLE user_roles ( user_id INT, role_id INT, PRIMARY KEY (user_id, role_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE ); -- 5. Добавляем тестовые данные INSERT INTO roles (name) VALUES ('USER'), ('ADMIN'), ('MODERATOR'); INSERT INTO users (username, email) VALUES ('user1', 'user1@example.com'), ('admin1', 'admin1@example.com'); INSERT INTO user_roles (user_id, role_id) VALUES (1, 1), -- user1 имеет роль USER (2, 2); -- admin1 имеет роль ADMIN INSERT INTO users (username, email) VALUES ('test_user', 'test@example.com'); INSERT INTO user_roles (user_id, role_id) VALUES (3, 1), -- test_user имеет роль USER (3, 3); -- test_user имеет роль MODERATOR

Конфигурация Spring Boot

В файле application.properties укажем параметры подключения к базе данных и Keycloak:

server.port=8081 spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/app_realm spring.datasource.url=jdbc:postgresql://localhost:5432/security_roles spring.datasource.username=postgres spring.datasource.password=admin spring.jpa.hibernate.ddl-auto=validate spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

Обновление зависимостей в pom.xml:

<!-- Добавим --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency>

Пишем код

Создание сущностей и репозитория

Сущность RoleEntity

@Entity @Table(name = "roles") public class RoleEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true, length = 50) private String name; @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY) private Set<UserEntity> users = new HashSet<>(); }

Файл RoleEntity — это как список всех ролей в нашей компании. Например, есть роли "Администратор", "Пользователь", "Модератор". Эти роли — как ярлыки, которые помогают нам понять, что каждый человек может или не может делать. Когда пользователь регистрируется или входит в систему, система смотрит, какая у него роль, чтобы знать, что ему разрешено делать.

Сущность UserEntity

@Entity @Table(name = "users") public class UserEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true, length = 50) private String username; @Column(nullable = false, unique = true, length = 100) private String email; @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id") ) private Set<RoleEntity> roles = new HashSet<>(); }

Файл UserEntity — это как список всех сотрудников. Каждый сотрудник — это отдельный пользователь в системе. У каждого пользователя есть ссылка на его роль через RoleId. Если у Анны RoleId = 1, значит, она — Администратор, потому что в таблице ролей под номером 1 написано "Администратор".

Репозиторий UserRepository

public interface UserRepository extends JpaRepository<UserEntity, Long> { // Стандартный метод поиска по имени Optional<UserEntity> findByUsername(String username); }

Это файл UserRepository посредник между кодом и базой данных. Когда системе нужно узнать что-то о пользователе, она обращается сюда.

Конфигурация безопасности KeycloakJwtAuthenticationConverter

public class KeycloakJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> { private final JwtGrantedAuthoritiesConverter defaultConverter = new JwtGrantedAuthoritiesConverter(); private final UserRepository userRepository; public KeycloakJwtAuthenticationConverter(UserRepository userRepository) { this.userRepository = userRepository; } @Override public AbstractAuthenticationToken convert(Jwt jwt) { Collection<GrantedAuthority> authorities = new ArrayList<>(defaultConverter.convert(jwt)); // Получаем username из токена String username = jwt.getClaimAsString("preferred_username"); // Ищем пользователя в БД и добавляем роли userRepository.findByUsername(username) .ifPresent(user -> user.getRoles().forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName())) ) ); return new JwtAuthenticationToken(jwt, authorities); } }

Когда пользователь присылает токен (например, при входе или доступе к странице), этот файл "читает" токен и вытаскивает важную информацию, как имя пользователя и другие данные.

После того как токен разобран, информация из него передаётся другим частям программы, чтобы те знали, кто пользователь и что ему разрешено делать.

SecurityConfig

@Configuration @EnableWebSecurity public class SecurityConfig { private final UserRepository userRepository; public SecurityConfig(UserRepository userRepository) { this.userRepository = userRepository; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(CsrfConfigurer::disable) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/public/**").permitAll() .requestMatchers("/admin").hasRole("ADMIN") .requestMatchers("/moderator").hasRole("MODERATOR") .requestMatchers("/user").hasRole("USER") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(new KeycloakJwtAuthenticationConverter(userRepository)) ) ); return http.build(); } }

Это файл охранник у ворот. Он решает, кто может пройти в систему, а кто нет. Здесь прописаны все правила безопасности.

А ну конечно не забудем про TestController

Это испытатель системы. Этот файл нужен для проверки работы системы и её функций.

@RestController public class TestController { @GetMapping("/public/hello") public String publicHello() { return "Public access!"; } @GetMapping("/user") public String userEndpoint() { return "User area!"; } @GetMapping("/admin") public String adminEndpoint() { return "Admin area!"; } @GetMapping("/moderator") public String moderatorEndpoint() { return "Moderator area!"; } }

Как всё это работает вместе?

Когда пользователь входит в систему:

  • Он присылает токен (как билет).
  • KeycloakJwtAuthenticationConverter читает токен и передаёт данные системе.

Система проверяет пользователя:

  • SecurityConfig определяет, может ли этот пользователь зайти на нужную страницу.
  • Например, если это страница для Администраторов, файл проверяет, есть ли у пользователя роль Администратора.

Когда система работает с пользователями:

  • Для добавления или получения данных о пользователе используется UserRepository.
  • Например, чтобы узнать роль пользователя, система найдёт его через UserRepository и посмотрит, какой у него RoleId.

Ну а далее тесты, над пользователем test_user:

# User endpoint (должен работать, так как у test_user есть роль USER) curl -H "Authorization: Bearer REAL_TOKEN" http://localhost:8081/user # Moderator endpoint (должен работать, так как у test_user есть роль MODERATOR) curl -H "Authorization: Bearer REAL_TOKEN" http://localhost:8081/moderator # Admin endpoint (не должен работать, так как прав нет) curl -H "Authorization: Bearer REAL_TOKEN" http://localhost:8081/admin
Динамическое управление доступом: Роли пользователей на лету с Spring Boot и Keycloak.

Вывод

Динамическое управление ролями через базу данных в интеграции с Keycloak обеспечивает гибкость и удобство. Вы можете легко обновлять роли пользователей и изменять их права доступа без необходимости обновлять токены. Такой подход подходит для масштабируемых приложений, где требуется централизованное управление правами.

1
Начать дискуссию