OAuth2AuthenticatedPrincipal не загружается после выполнения самоанализа

Я создал проект Spring Security 6 с непрозрачной конфигурацией токена, реализованной на сервере ресурсов. У меня уже есть сервер авторизации, развернутый как в тестовых, так и в производственных средах. Мой интроспектор вызывается из пользовательского класса ниже:

@Slf4j
public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private OpaqueTokenIntrospector delegate;

    public CustomAuthoritiesOpaqueTokenIntrospector(String oauthServerUrl, String clientId, String clientSecret) {
        this.delegate = new NimbusOpaqueTokenIntrospector(oauthServerUrl, clientId, clientSecret);
    }

    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
        return new DefaultOAuth2AuthenticatedPrincipal(
                principal.getName(), principal.getAttributes(), extractAuthorities(principal));
    }

    private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
        List<String> scopes = principal.getAttribute("scope");
        return scopes.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

который я скопировал из этой документации

Вот конфигурация моего сервера ресурсов:

@Configuration
@EnableWebSecurity
public class ResourceServerConfiguration {
    @Value("${spring.profiles.active}")
    private String activeProfile;

    @Value("${root.url.osb.basic.auth}")
    private String osbBasicAuth;

    @Value("${auth.server.url}")
    private String oauthServerUrl;

    @Value("${auth.server.clientId}")
    private String clientId;

    @Value("${auth.server.clientsecret}")
    private String clientSecret;

    public static final String BEARER_PREFIX = "Bearer ";
    public static final String HEADER_NAME = "Authorization";
    private final UserService userService;

    public ResourceServerConfiguration(UserService userService) {
        this.userService = userService;
    }

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain filterChain)
                    throws ServletException, IOException {
                // We don't want to allow access to a resource with no token so clear
                // the security context in case it is actually an OAuth2Authentication
                var authHeader = request.getHeader(HEADER_NAME);
                if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWith(authHeader, BEARER_PREFIX)) {
                    SecurityContextHolder.clearContext();
                }
                filterChain.doFilter(request, response);
            }
        }, AbstractPreAuthenticatedProcessingFilter.class);
//        http.csrf().disable();
        http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/", "/v1/open/**").permitAll());

        http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/pension/**").authenticated())
                .httpBasic(Customizer.withDefaults());

        if (StringUtils.equalsIgnoreCase(activeProfile, "dev")) {
            http.authorizeHttpRequests(authorize ->
                authorize.requestMatchers("/webjars/**", "/resources/**", "/swagger-ui.html", "/swagger-resources/**", "/v2/api-docs", "index.html")
                .permitAll());
        }

        http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                .oauth2ResourceServer(oauth2 -> oauth2
                        .opaqueToken(Customizer.withDefaults())
                );
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        String[] authParts = osbBasicAuth.split(":");
        String username = authParts[0];
        String password = authParts[1];

        UserDetails userDetails = User.builder()
                .username(username)
                .password(new BCryptPasswordEncoder().encode(password)).roles()
                .build();
        return new InMemoryUserDetailsManager(userDetails);
    }

    @Bean
    public OpaqueTokenIntrospector introspector() {
        return new CustomAuthoritiesOpaqueTokenIntrospector(oauthServerUrl + "check_token", clientId, clientSecret);
    }
}

Для этого процесса токен извлекается с сервера авторизации по ссылке http://localhost:9696/oauth/token. который вызывается отдельно, в моем случае через Postman. После получения токена я вызываю другой сервис, вставляя этот токен в заголовок авторизации.

Например, я вызываю эту конечную точку: http://localhost:9090/v1/lead-management/get-details/18410, и эта конечная точка является частной и выполняется только с токенами. После того, как я его выполню, первое, что происходит, это то, что мой самоанализ запускается при вызове http://localhost:9696/oauth/http://localhost:9696/oauth/check_token для проверки моего токена, а затем он вызывает финальный конечная точка. Проверка токена прошла успешно, я получил все данные, необходимые для подтверждения подлинности моего пользователя: здесь

Однако конечная конечная точка выдает ошибку 500. И здесь я понятия не имею, в чем проблема, и мне кажется, что Principal был неправильно сопоставлен или загружен.

Я имею дело с этим беспорядком уже около 1 месяца и до сих пор не могу найти решение. Я надеюсь, что вы можете помочь решить эту проблему. Спасибо!

-- ОБНОВЛЯТЬ --

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

Securing GET /v1/lead-management/get-details/18410
07-11-2024 09:29:55.723 http-nio-9090-exec-1 [correlationId: ] [DEBUG] CompositeLog - HTTP POST http://localhost:9696/oauth/check_token
07-11-2024 09:29:55.732 http-nio-9090-exec-1 [correlationId: ] [DEBUG] InternalLoggerFactory - Using SLF4J as the default logging framework
07-11-2024 09:29:55.734 http-nio-9090-exec-1 [correlationId: ] [DEBUG] CompositeLog - Accept=[text/plain, application/json, application/*+json, */*] 07-11-2024 09:29:55.734 http-nio-9090-exec-1 [correlationId: ] [DEBUG]  CompositeLog - Writing [{token=[3055687b-6c25-4a59-894e-3a209ce3f1d7]}] with org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter 07-11-2024 09:29:55.748 http-nio-9090-exec-1 [correlationId: ] [DEBUG] LoggingProviderImpl$JULWrapper - sun.net.www.MessageHeader@33d105118 pairs: {POST /oauth/check_token HTTP/1.1: null}{Accept: application/json}{Content-Type: application/x-www-form-urlencoded;charset=UTF-8}{Authorization: Basic VVNFUl9DTElFTlRfUlM6dGVzdA==}{User-Agent: Java/17.0.7}{Host: localhost:9696}{Connection: keep-alive}{Content-Length: 42}
07-11-2024 09:29:55.935 http-nio-9090-exec-1 [correlationId: ] [DEBUG] LoggingProviderImpl$JULWrapper -
                    sun.net.www.MessageHeader@4978153910 pairs: {null: HTTP/1.1 200}{X-Content-Type-Options: nosniff}{X-XSS-Protection: 1; mode=block}{Cache-Control: no-cache, no-store, max-age=0, must-revalidate}{Pragma: no-cache}{Expires: 0}{X-Frame-Options: DENY}{Content-Type: application/json;charset=UTF-8}{Transfer-Encoding: chunked}{Date: Thu, 11 Jul 2024 04:29:55 GMT}
07-11-2024 09:29:55.936 http-nio-9090-exec-1 [correlationId: ] [DEBUG] CompositeLog -
                    Response 200 OK
07-11-2024 09:29:55.937 http-nio-9090-exec-1 [correlationId: ] [DEBUG] CompositeLog -
                    Reading to [java.lang.String] as "application/json;charset=UTF-8"
07-11-2024 09:29:56.002 http-nio-9090-exec-1 [correlationId: ] [DEBUG] OpaqueTokenAuthenticationProvider -
                    Authenticated token
07-11-2024 09:29:56.002 http-nio-9090-exec-1 [correlationId: ] [DEBUG] BearerTokenAuthenticationFilter -
                    Set SecurityContextHolder to BearerTokenAuthentication [Principal=org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal@4879b039, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[role_admin, role_user]]
07-11-2024 09:29:56.006 http-nio-9090-exec-1 [correlationId: ] [DEBUG] FilterChainProxy -
                    Secured GET /v1/lead-management/get-details/18410
07-11-2024 09:29:56.016 http-nio-9090-exec-1 [correlationId: ] [DEBUG] FilterChainProxy -
                    Securing GET /error
07-11-2024 09:29:56.017 http-nio-9090-exec-1 [correlationId: ] [DEBUG] FilterChainProxy -
                    Secured GET /error
07-11-2024 09:29:56.019 http-nio-9090-exec-1 [correlationId: ] [DEBUG] LogFormatUtils -
                    "ERROR" dispatch for GET "/error", parameters = {}
07-11-2024 09:29:56.021 http-nio-9090-exec-1 [correlationId: ] [DEBUG] AbstractHandlerMapping -
                    Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
07-11-2024 09:29:56.022 http-nio-9090-exec-1 [correlationId: ] [DEBUG] OpenEntityManagerInViewInterceptor -
                    Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
07-11-2024 09:29:56.052 http-nio-9090-exec-1 [correlationId: ] [DEBUG] AbstractMessageConverterMethodProcessor -
                    Using 'application/json', given [*/*] and supported [application/json, application/*+json]
07-11-2024 09:29:56.054 http-nio-9090-exec-1 [correlationId: ] [DEBUG] LogFormatUtils -
                    Writing [{timestamp=Thu Jul 11 09:29:56 ORAT 2024, status=500, error=Internal Server Error, path=/v1/lead-man (truncated)...]
07-11-2024 09:29:56.098 http-nio-9090-exec-1 [correlationId: ] [DEBUG] OpenEntityManagerInViewInterceptor -
                    Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
07-11-2024 09:29:56.099 http-nio-9090-exec-1 [correlationId: ] [DEBUG] FrameworkServlet -
                    Exiting from "ERROR" dispatch, status 500

500 означает, что есть журналы ошибок.

J Asgarov 10.07.2024 17:18

А что с логами сервера авторизации? Вы настраиваете собственный интроспектор, который мало что дает (авторитеты карт из scope утверждают, что они используются по умолчанию, и не регистрируют, что происходит). Возможно, вам стоит установить там несколько точек останова и проверить, что происходит.

ch4mp 11.07.2024 20:07

Журналы сервера авторизации не дают много информации, только несколько простых журналов отладки, выводимых на экран. Кстати, на данный момент я запустил этот сервер аутентификации локально, потому что обращение к тестовому серверу через конфигурацию локального ресурса дало мне ошибку PKIX. Я пытался поставить точки останова для самоанализа, но все равно не смог отследить никаких проблем. Интроспекция обращается к check_token и успешно проверяет его, а позже просто не сопоставляет полномочия моему конечному пользователю ouath2. Однако после реализации подхода без специального класса и реализации права на настройку свойств, к моему удивлению, проблема была решена.

Muratbek Bauyrzhan 12.07.2024 07:21
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
57
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Есть несколько вещей, которые необходимо изменить.

Общие Соображения

Как указано в комментариях @j-asgarov, вам не следует сообщать о 500 и опускать журналы.

Единственным обоснованным случаем платы за неэффективность самоанализа было сохранение некоторого контроля над состоянием пользователя («сессиями»), когда это состояние выгружалось во фронтенд (токен отправлялся в одностраничные или мобильные приложения). Но поскольку сейчас это не рекомендуется в пользу шаблона OAuth2 BFF, не остается веских причин предпочитать самоанализ декодированию JWT.

Сломанная конфигурация безопасности

У вас должен быть один bean-компонент SecurityFilterChain для каждого механизма авторизации. Здесь вы, кажется, хотите, чтобы некоторые запросы были авторизованы с помощью Basic аутентификации, а другие - с Bearer аутентификацией. Вам нужны разные цепочки фильтров (все с разными @Order и все, кроме последней по порядку, с securityMatcher).

Вам не нужен специальный фильтр для запрета анонимных запросов. authenticated() в каждой цепочке фильтров достаточно.

Какой смысл permitAll(), когда вы высказываете требование запретить анонимные запросы (и даже создать для этого фильтр)?

Что делать дальше

Если то, что потребляет ваш сервер ресурсов, не отображается на стороне сервера (Thymeleaf, JSF и т. д.), изучите шаблон OAuth2 BFF.

Переключите серверы ресурсов с самоанализа на декодирование JWT (и настройте сервер авторизации для обслуживания токенов доступа JWT, если он еще этого не делает).

Отбросьте свой собственный фильтр (эта функция включена в авторизацию запросов Spring, и вам, вероятно, понадобится анонимный доступ к некоторым ресурсам).

Создайте отдельные компоненты цепочки фильтров безопасности для авторизаций Basic и Bearer.

В моем случае я, скорее всего, откажусь от перехода на декодирование JWT, потому что этот сервер авторизации действительно был развернут сотрудниками компании в прошлом, и мой тимлид говорит мне, что мне придется использовать непрозрачный токен для этого проекта. На данный момент я временно закомментировал все конфигурации, связанные с базовой аутентификацией, и когда я пытался опубликовать запрос, он по-прежнему вызывает ту же ошибку.

Muratbek Bauyrzhan 11.07.2024 07:14
Ответ принят как подходящий

После некоторых изменений моего руководителя команды это наконец заработало. Использование пользовательского класса интроспектора было удалено, а конфигурация сервера ресурсов была изменена следующим образом:

http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
    .oauth2ResourceServer(oauth2 -> oauth2
    .opaqueToken(opaqueTokenConfigurer -> opaqueTokenConfigurer
    .introspectionUri(oauthServerUrl + "check_token")
    .introspectionClientCredentials(clientId, clientSecret)
  )
);

Компонент OpaqueTokenIntrospector также был удален.

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