Я создал проект 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
А что с логами сервера авторизации? Вы настраиваете собственный интроспектор, который мало что дает (авторитеты карт из scope
утверждают, что они используются по умолчанию, и не регистрируют, что происходит). Возможно, вам стоит установить там несколько точек останова и проверить, что происходит.
Журналы сервера авторизации не дают много информации, только несколько простых журналов отладки, выводимых на экран. Кстати, на данный момент я запустил этот сервер аутентификации локально, потому что обращение к тестовому серверу через конфигурацию локального ресурса дало мне ошибку PKIX. Я пытался поставить точки останова для самоанализа, но все равно не смог отследить никаких проблем. Интроспекция обращается к check_token и успешно проверяет его, а позже просто не сопоставляет полномочия моему конечному пользователю ouath2. Однако после реализации подхода без специального класса и реализации права на настройку свойств, к моему удивлению, проблема была решена.
Есть несколько вещей, которые необходимо изменить.
Как указано в комментариях @j-asgarov, вам не следует сообщать о 500 и опускать журналы.
Единственным обоснованным случаем платы за неэффективность самоанализа было сохранение некоторого контроля над состоянием пользователя («сессиями»), когда это состояние выгружалось во фронтенд (токен отправлялся в одностраничные или мобильные приложения). Но поскольку сейчас это не рекомендуется в пользу шаблона OAuth2 BFF, не остается веских причин предпочитать самоанализ декодированию JWT.
У вас должен быть один bean-компонент SecurityFilterChain
для каждого механизма авторизации. Здесь вы, кажется, хотите, чтобы некоторые запросы были авторизованы с помощью Basic
аутентификации, а другие - с Bearer
аутентификацией. Вам нужны разные цепочки фильтров (все с разными @Order
и все, кроме последней по порядку, с securityMatcher
).
Вам не нужен специальный фильтр для запрета анонимных запросов. authenticated()
в каждой цепочке фильтров достаточно.
Какой смысл permitAll()
, когда вы высказываете требование запретить анонимные запросы (и даже создать для этого фильтр)?
Если то, что потребляет ваш сервер ресурсов, не отображается на стороне сервера (Thymeleaf, JSF и т. д.), изучите шаблон OAuth2 BFF.
Переключите серверы ресурсов с самоанализа на декодирование JWT (и настройте сервер авторизации для обслуживания токенов доступа JWT, если он еще этого не делает).
Отбросьте свой собственный фильтр (эта функция включена в авторизацию запросов Spring, и вам, вероятно, понадобится анонимный доступ к некоторым ресурсам).
Создайте отдельные компоненты цепочки фильтров безопасности для авторизаций Basic
и Bearer
.
В моем случае я, скорее всего, откажусь от перехода на декодирование JWT, потому что этот сервер авторизации действительно был развернут сотрудниками компании в прошлом, и мой тимлид говорит мне, что мне придется использовать непрозрачный токен для этого проекта. На данный момент я временно закомментировал все конфигурации, связанные с базовой аутентификацией, и когда я пытался опубликовать запрос, он по-прежнему вызывает ту же ошибку.
После некоторых изменений моего руководителя команды это наконец заработало. Использование пользовательского класса интроспектора было удалено, а конфигурация сервера ресурсов была изменена следующим образом:
http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaqueTokenConfigurer -> opaqueTokenConfigurer
.introspectionUri(oauthServerUrl + "check_token")
.introspectionClientCredentials(clientId, clientSecret)
)
);
Компонент OpaqueTokenIntrospector также был удален.
500 означает, что есть журналы ошибок.