Spring Session / Redis и Oauth2 не работают вместе

Oauth2 и Redis не будут работать вместе. Как только я включаю Spring Session, после аутентификации (OIDC) создаются два идентификатора сеанса и отправляются обратно в приложение - один JSESSIONID из Redis, а другой из Spring Security Oauth. Как только я отключаю Redis / Spring Session, все работает очень хорошо.

Я создал очень маленькое приложение Maven, которое можно загрузить с: http://folk.uio.no/erlendfg/oidc/oidc.zip

Если я запускаю приложение Jetty и Redis на локальном хосте, я могу воспроизвести проблему локально. Как показано на скриншоте из Firefox, создаются два файла cookie сеанса: http://folk.uio.no/erlendfg/oidc/two-sessions.png

Я следил за руководством Баелдунга, но внес некоторые небольшие изменения, чтобы сделать приложение совместимым с нашим поставщиком OIDC. https://www.baeldung.com/spring-security-openid-connect

Все эти классы доступны в zip-архиве (см. Ссылку выше). Наиболее важные из них:

RedisConfiguration.java

@Configuration
@EnableRedisHttpSession(redisNamespace = "oidc", maxInactiveIntervalInSeconds = 10800)
public class RedisConfiguration {

  @Bean
  public LettuceConnectionFactory connectionFactory() {
    return new LettuceConnectionFactory("localhost", 6379);
  }

}

FeideOpenIdConnectConfig.java

@Configuration
@EnableOAuth2Client
public class FeideOpenIdConnectConfig {

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

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

  @Value("${feide.auth.accessTokenUri}")
  private String accessTokenUri;

  @Value("${feide.auth.userAuthorizationUri}")
  private String userAuthorizationUri;

  @Value("${feide.auth.preEstablishedRedirectUri}")
  private String preEstablishedRedirectUri;


  @Bean
  public OAuth2ProtectedResourceDetails feideOpenId() {
    AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
    details.setClientId(clientId);
    details.setClientSecret(clientSecret);
    details.setAccessTokenUri(accessTokenUri);
    details.setUserAuthorizationUri(userAuthorizationUri);
    details.setScope(Arrays.asList("openid", "email", "userid-feide", "profile", "groups"));
    details.setPreEstablishedRedirectUri(preEstablishedRedirectUri);
    details.setUseCurrentUri(false);
    details.setGrantType("authorization_code");
    return details;
  }


  @Bean
  public OAuth2RestTemplate feideOpenIdTemplate(OAuth2ClientContext clientContext) {
    return new OAuth2RestTemplate(feideOpenId(), clientContext);
  }

}

FeideConnectFilter.java

public class FeideConnectFilter extends OAuth2ClientAuthenticationProcessingFilter {

  public FeideConnectFilter(String defaultFilterProcessesUrl) {
    super(defaultFilterProcessesUrl);
  }


  @Override
  public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response) throws AuthenticationException, IOException, ServletException {

    OAuth2AccessToken accessToken;
    try {
      accessToken = restTemplate.getAccessToken();
    } catch (OAuth2Exception e) {
      throw new BadCredentialsException("Could not obtain access token", e);
    }
    try {
      String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
      Jwt tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier("https://auth.dataporten.no/openid/jwks"));

      @SuppressWarnings("unchecked")
      Map<String, String> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);

      verifyClaims(authInfo, "https://auth.dataporten.no");

      request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
      request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());

      OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
      return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
    } catch (Exception e) {
      throw new BadCredentialsException("Could not obtain user details from token", e);
    }
  }


  @Override
  protected boolean requiresAuthentication(final HttpServletRequest request, final HttpServletResponse response) {
    if (super.requiresAuthentication(request, response)) {
      return true;
    }

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    // Already authenticated:
    if (authentication != null) {
      return false;
    }
    OAuth2AccessToken accessToken = restTemplate.getAccessToken();
    if (accessToken == null) {
        return true;
    }
    return true;
  }


  @SuppressWarnings("rawtypes")
  protected void verifyClaims(final Map claims, final String issuer) {
    int exp = (Integer) claims.get("exp");
    Date expireDate = new Date(exp * 1000L);
    Date now = new Date();
    if (expireDate.before(now) || !claims.get("iss").equals(issuer) ||
            !claims.get("aud").equals(restTemplate.getResource().getClientId())) {
      throw new RuntimeException("Invalid claims");
    }
  }


  protected RsaVerifier verifier(final String jwkSigningUri) throws Exception {
    CustomUrlJwkProvider provider = new CustomUrlJwkProvider(new URL(jwkSigningUri));
    Jwk jwk = provider.getJwk();
    return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
  }


  protected HttpHeaders getHttpHeaders() {
    HttpHeaders headers = new HttpHeaders();
    headers.set("Authorization", "Bearer " + restTemplate.getAccessToken());
    return headers;
  }

}

SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Inject
  private OAuth2RestTemplate restTemplate;


  @Bean
  public FeideConnectFilter feideConnectFilter() {
    FeideConnectFilter filter = new FeideConnectFilter("/oauth/login");
    filter.setRestTemplate(restTemplate);
    return filter;
  }


  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .addFilterAfter(new OAuth2ClientContextFilter(), AbstractPreAuthenticatedProcessingFilter.class)
        .addFilterAfter(feideConnectFilter(), OAuth2ClientContextFilter.class)
        .httpBasic()
        .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/oauth2/login"))
        .and()
        .authorizeRequests()
        .anyRequest().authenticated();
  }

}

Фильтры (в WebInitializer.java) добавляются в таком порядке:

private void addFilters(final ServletContext container, final WebApplicationContext applicationContext) {
  container.addFilter("springSessionRepositoryFilter", DelegatingFilterProxy.class).addMappingForUrlPatterns(null, false, "/*");
  container.addFilter("springSecurityFilterChain", DelegatingFilterProxy.class).addMappingForUrlPatterns(null, false, "/*");
}
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
1 372
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

OIDC работает с Spring Session, если я использую собственную реализацию OIDC Spring Security, которая была представлена ​​в версии 5. Между прочим, это было на удивление легко реализовать, почти без кода. Команда Spring Security проделала большую работу, добавив поддержку OIDC, а также SAML2.0 в Spring Security. Другими словами, я нашел решение.

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