Уже пару дней я пытаюсь выяснить, почему Spring OAuth2 не использует токен обновления, возвращаемый для предоставления кода авторизации, когда срок действия токена доступа истекает.
Настройка
По сути, я следовал этому руководству, чтобы настроить все необходимое. Мой Keycloak работает на локальном хосте: 8090, и я настроил его следующим образом:
Код: Выделить всё
package com.my.project.config.security;
import com.my.project.security.authentication.CustomOidcClientInitiatedLogoutSuccessHandler;
import jakarta.servlet.http.Cookie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
@Configuration
public class WebSecurityConfig {
public static String[] PUBLIC_PATHS = {
"/api/csrf",
"/oauth2/login",
"/oauth2/logout",
"/oauth2/error",
"/api/contact-form"
};
@Bean
public SecurityFilterChain securityFilterChain(final HttpSecurity http,
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
final ClientRegistrationRepository clientRegistrationRepository) throws Exception {
final CustomOidcClientInitiatedLogoutSuccessHandler clientInitiatedLogoutSuccessHandler =
new CustomOidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
clientInitiatedLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/oauth2/login?redirect=true");
final DefaultOAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository,
OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
oAuth2AuthorizationRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
return http
.csrf(Customizer.withDefaults())
.cors(Customizer.withDefaults())
.authorizeHttpRequests((customizer) -> customizer
.requestMatchers(PUBLIC_PATHS).permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer((oauth2) -> oauth2
.jwt(Customizer.withDefaults()))
.oauth2Client(configurer -> {
//override behaviour of authentication: don't redirect but change status and add location header.
//it's a bit hacky, but otherwise we get CORS errors on client side, because through the redirect we're running into cross-origin issues
//and keycloak is just not setting correct CORS headers :/
//delete this hack when bug https://github.com/keycloak/keycloak/pull/27334 is fixed
configurer.authorizationCodeGrant(customizer -> {
customizer.authorizationRedirectStrategy((request, response, url) -> {
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, HttpHeaders.LOCATION);
response.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.LOCATION);
response.addCookie(new Cookie("test", System.currentTimeMillis() + ""));
response.setHeader(HttpHeaders.LOCATION, url);
if ("true".equals(request.getParameter("redirect"))) {
response.setStatus(302);
} else {
response.setStatus(201);
}
});
customizer.authorizationRequestResolver(oAuth2AuthorizationRequestResolver);
});
})
.oauth2Login(configurer -> {
configurer.userInfoEndpoint(customizer -> customizer.userAuthoritiesMapper(customGrantedAuthoritiesMapper()));
configurer.loginPage("/oauth2/login");
configurer.defaultSuccessUrl("/oauth2/success");
configurer.failureUrl("/oauth2/error");
})
.logout(configurer -> {
configurer.invalidateHttpSession(true);
configurer.clearAuthentication(true);
configurer.deleteCookies("JSESSIONID");
configurer.logoutUrl("/oauth2/logout");
configurer.logoutSuccessHandler(clientInitiatedLogoutSuccessHandler);
})
//.addFilterBefore(new TokenExpiredFilter(), AnonymousAuthenticationFilter.class)
.build();
}
@Bean
public GrantedAuthoritiesMapper customGrantedAuthoritiesMapper() {
return new CustomGrantedAuthoritiesMapper();
}
}
Мой файл application.yml выглядит так:
Код: Выделить всё
service:
mock: false
keycloak:
realm-id: my-realm-id
base-uri: http://localhost:8090
base-rest-uri: ${keycloak.base-uri}/admin/realms/${keycloak.realm-id}
token-uri: ${keycloak.base-uri}/realms/${keycloak.realm-id}/protocol/openid-connect/token
logging:
level:
root: INFO
server:
port: 8080
servlet:
session:
timeout: 15s
cookie:
same-site: none
http-only: true
secure: true
error:
include-message: never
include-binding-errors: never
include-stacktrace: never
include-exception: false
spring:
datasource:
url: jdbc:postgresql://localhost:5432/db-name
driverClassName: org.postgresql.Driver
username: my-user
password: my-password
jpa:
open-in-view: false
hibernate:
ddl-auto: none
properties:
database-platform: org.hibernate.dialect.PostgreSQL10Dialect
show-sql: false
security:
oauth2:
client:
registration:
keycloak:
client_id: my-client-id
client_secret: my-client-secret
authorization-grant-type: authorization_code
scope: openid
provider:
keycloak:
issuerUri: ${keycloak.base-uri}/realms/${keycloak.realm-id}
user-name-attribute: preferred_username
resourceserver:
jwt:
issuer-uri: ${keycloak.base-uri}/realms/${keycloak.realm-id}
flyway:
url: ${spring.datasource.url}
user: ${spring.datasource.username}
password: ${spring.datasource.password}
locations: classpath:/db/migration/ddl
Когда я пытаюсь запросить защищенный ресурс в своем бэкэнде Spring, я получаю перенаправление на /oauth2/login, который перенаправляет на /oauth2/authorization/keycloak, который, наконец, «перенаправляет» на страницу входа в Keycloak. Это работает хорошо, я никогда не связываюсь с учетными данными пользователя, а интерфейс знает только JSESSIONID. Кроме того, выход из системы работает нормально, сеанс в области Keycloak успешно уничтожается, и сеанс Spring тоже уничтожается.
Мои проблемы начинаются, когда дело доходит до токена доступа с истекшим сроком действия. В целях тестирования я установил продолжительность жизни токена доступа на 10 секунд в Keycloak, а продолжительность сеанса Spring — на 15 секунд. Всякий раз, когда я запрашиваю что-то из своего защищенного бэкэнда и срок действия сеанса истекает, процесс входа в систему запускается снова, перенаправляясь на /oauth2/login, который перенаправляется на /oauth2/authorization/keycloak и, наконец, на настроенный URL-адрес успешного входа в систему oauth2, не видя входа в систему. сформировать заново. В пользовательских событиях Keycloak я вижу событие входа другого пользователя и код для токена с новым токеном доступа в качестве результата.
Когда я отлаживаю код Spring, я вижу, что грант авторизации_кода возвращает оба токен доступа и токен обновления правильно, но кажется, что токен обновления больше никогда не используется. К сожалению, я не могу понять, в чем дело, но я читал, что Spring должен обрабатывать обновление токена доступа, используя токен обновления из коробки. Но
org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider#authorize
никогда не вызывается и не вызывается
org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient#getTokenResponse
Я вижу это
org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter#attemptAuthentication
вызывает
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient,
oauth2Authentication, request , ответ)
Похоже, это последний след токена обновления, поскольку его не существует в OAuth2AuthenticationToken, возвращенном из этого метода.
К сожалению, ни в одной реализации нет точки останова
org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository#loadAuthorizedClient< /p>
когда-либо вызывается для повторного получения токена обновления.
Любая помощь приветствуется, так как я действительно застрял здесь. Большое спасибо за любой ответ, вопросы и помощь!
Подробнее здесь: https://stackoverflow.com/questions/783 ... en-expires