Аутентификация на основе ролей не работает с Keycloak и Java Spring

Я следовал этому руководству: https://www.baeldung.com/spring-boot-keycloak, чтобы настроить свое приложение Spring с помощью Keycloak. Аутентификация работает нормально, но когда я хочу добавить аутентификацию на основе ролей, например

.requestMatchers(new AntPathRequestMatcher("/api/v1/user/**")).hasRole("user")

Я всегда получаю следующую ошибку

Bearer error = "insufficient_scope", error_description = "The request requires higher privileges than provided by the access token.", error_uri = "https://tools.ietf.org/html/rfc6750#section-3.1"

Я проверил авторитеты, используя

SecurityContextHolder.getContext().getAuthentication().getAuthorities();

и есть только те, у которых есть префикс SCOPE_. Я также проверил токен, и он возвращает это правильно.

 "realm_access": {
    "roles": [
      "offline_access",
      "uma_authorization",
      "default-master-realm",
      "user"
    ]
  },

помимо других полей.

Это мой KeycloakConfig класс:

package com.motus.core.shared.config.security;

import com.motus.auth.constants.Authority;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
@EnableWebSecurity
@Configuration
public class KeycloakConfig {

    private static final String GROUPS = "groups";
    private static final String REALM_ACCESS_CLAIM = "realm_access";
    private static final String ROLES_CLAIM = "roles";

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(sessionRegistry());
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, KeycloakLogoutHandler keycloakLogoutHandler) throws Exception {
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers(new AntPathRequestMatcher("/api/v1/public/**")).permitAll()
                .requestMatchers(new AntPathRequestMatcher("/api/v1/auth/**")).permitAll()
                .requestMatchers(new AntPathRequestMatcher("/api/v1/admin/**")).hasRole("admin")
                .requestMatchers(new AntPathRequestMatcher("/api/v1/user/**")).hasRole("user")
                .anyRequest().authenticated()
        );
        http.oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults()));
        http.oauth2Login(Customizer.withDefaults())
                .logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));

        http.csrf(AbstractHttpConfigurer::disable);
        http.cors(Customizer.withDefaults());

        return http.build();
    }

    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
        return authorities -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
            var authority = authorities.iterator().next();
            boolean isOidc = authority instanceof OidcUserAuthority;

            if (isOidc) {
                var oidcUserAuthority = (OidcUserAuthority) authority;
                var userInfo = oidcUserAuthority.getUserInfo();

                if (userInfo.hasClaim(REALM_ACCESS_CLAIM)) {
                    var realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM);
                    var roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                } else if (userInfo.hasClaim(GROUPS)) {
                    Collection<String> roles = userInfo.getClaim(
                            GROUPS);
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                }
            } else {
                var oauth2UserAuthority = (OAuth2UserAuthority) authority;
                Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

                if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) {
                    Map<String, Object> realmAccess = (Map<String, Object>) userAttributes.get(
                            REALM_ACCESS_CLAIM);
                    Collection<String> roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                }
            }
            return mappedAuthorities;
        };
    }

    Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
        return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(
                Collectors.toList());
    }
}

Похоже, что GrantedAuthoritesMapper не вызывается, знаете почему? Вот что я добавил в свой pom.xml:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>

и вот что я добавил в свой application.properties:

spring.security.oauth2.client.registration.keycloak.client-id=${KEYCLOAK_CLIENT_ID:test-backend}
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=${KEYCLOAK_AUTHORIZATION_GRANT_TYPE:password}
spring.security.oauth2.client.registration.keycloak.scope=${KEYCLOAK_SCOPE:openid}
spring.security.oauth2.client.provider.keycloak.issuer-uri=${KEYCLOAK_ISSUER_URI:http://localhost:8080/realms/master}
spring.security.oauth2.client.provider.keycloak.user-name-attribute=${KEYCLOAK_USER_NAME_ATTRIBUTE:username}
spring.security.oauth2.resourceserver.jwt.issuer-uri=${KEYCLOAK_RESOURCE_SERVER_JWT_ISSUER_URI:http://localhost:8080/realms/master}

Я получаю токен, используя http://localhost:8080/realms/master/protocol/openid-connect/token Затем я отправляю запрос в свое приложение с полученным токеном и всегда получаю упомянутую выше ошибку. Кто-нибудь знает, как я могу это исправить?

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

hasRole('user') требует полномочий ROLE_user

Или:

  • используйте hasAuthority('user')
  • добавьте префикс ROLE_ при сопоставлении ролей Keycloak с органами безопасности Spring.

Не используйте oauth2ResourceServer и oauth2Login в одной цепочке фильтров безопасности.

Помните, что клиент OAuth2 потребляет ресурсы, обслуживаемые серверами ресурсов (это разные субъекты). В архитектурах микросервисов часто встречаются приложения с обеими ролями, но для разных ресурсов (которые должны быть защищены разными Security(Web)FilterChain bean-компонентами).

Spring Boot не зря предоставляет разные стартеры. Я знаю, что учебник Baeldung, а также многие другие от авторов, которые копируют друг друга, не понимая, что они делают, используют oauth2ResourceServer и oauth2Login в одной и той же цепочке фильтров безопасности, но это ерунда. Давайте подробнее рассмотрим конфигурацию безопасности для этих двух:

  • oauth2ResourceServer настраивает сервер ресурсов OAuth2, который:
  • oauth2Login настраивает клиент OAuth2 с потоком кода авторизации, который:
    • авторизует запросы на основе файла cookie сеанса (а не токена Bearer в заголовке Authorization)
    • нужна защита от CSRF-атак
    • должен возвращать 302 Redirect to login на запросы без действительного сеанса
    • сопоставляет органы власти с помощью GrantedAuthoritiesMapper (или с помощью специального OAuth2UserService)

Эти требования слишком разные, чтобы стоять в одном Security(Web)FilterChain бобе. Также обратите внимание на вышеизложенное, что полномочия сопоставляются по-разному на oauth2ResourceServer с JWT-декодером, oauth2ResourceServer с (непрозрачным) интроспектором токенов и на клиентах OAuth2 с oauth2Login.

ПС

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

Дополнительный стартер , который я размещаю в том же репозитории, может помочь вам настроить ваши приложения Spring для Keycloak, используя только свойства (эти приложения Spring в основном являются клиентами OAuth2 с серверами ресурсов oauth2Login или OAuth2).

Другие вопросы по теме