Интеграция весеннего сеанса в Spring Security — как заполнить аутентификацию

#spring-boot #spring-security #spring-session #spring-security-rest

Вопрос:

При интеграции spring session с spring security я не уверен, как SecurityContextImpl#Authentication предполагается заполнять, когда сеанс идентифицируется spring session.

Контекст: spring-boot Приложение фактически не обрабатывает вход в систему,выход из системы или создание самого сеанса. Сеанс создается во внешнем микросервисе, не являющемся spring, и используется совместно через MongoDB. Совместное использование и отображение информации о сеансе работает и использовалось без проблем до весенней безопасности.

Что работает:

  • Весенняя сессия правильно разрешает идентификатор сеанса
  • Весенний сеанс извлекает сеанс (с использованием идентификатора сеанса) из репозитория сеанса(монго), и атрибуты заполняются
  • Запрос содержит заполненный объект сеанса, включающий все атрибуты

Что не работает:

  • Использование http.authorizeRequests().antMatchers("admin/**").authenticated() , а затем запрос и конечная точка (с сеансовым файлом cookie) никоим образом не заполняют SecurityContext#Authenticated

Возможные варианты

а) Я понимаю, я мог бы реализовать обычай CustomUserNameFromSessionFilter для предварительного заполнения Authenticated в SecureContext (еще не аутентифицированный=false) и поместить его в начале цепочки фильтров безопасности. Кроме того, я реализую пользовательский поставщик CustomFromSessionAuthenticationProvider проверки подлинности, который затем подбирает Authenticated и в основном устанавливает authenticated=true , является ли сеанс действительным (что на данный момент всегда верно).

б) Используйте RememberMeAuthenticationFilter , но я не уверен, как эта документация подходит для этой цели

c) Каким-то образом использовать AbstractPreAuthenticatedProcessingFilter , но, скорее всего, он используется для внешних запросов аутентификации

Каждый вариант кажется неправильным, и необходимость в такой реализации кажется слишком распространенной, чтобы не существовало/лучшего решения. Каков правильный подход?

Фрагменты кода

   @Override
  protected void configure(HttpSecurity http) throws Exception
  {
    
    http.csrf().disable();

    // Logout is yet handled by PHP Only, we yet cannot delete/write sessions here
    http.logout().disable();
    http.formLogin().disable();
    
 
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);

    http.authorizeRequests()
      .antMatchers("/admin").authenticated();
  }
 

Комментарии:

1. Можете ли вы поделиться своей конфигурацией безопасности spring? Использует ли ваше приложение a HttpSessionSecurityContextRepository ?

2. Что вы определяете как конфигурацию безопасности void configure(HttpSecurity http) ? Если да, то я добавил его. HttpSessionSecurityContextRepository#loadContext вызывается (контекст есть null , и он создает новый). Правильно ли я понимаю, что это сохраняет предыдущий аутентифицированный контекст SecurityContext , HttpSession чтобы его можно было позже извлечь при загрузке того же сеанса (используя идентификатор сеанса). Таким образом, следующий вызов загружает объект SecurityContext с предварительно заполненным Authenticated объектом. Это никогда не сработало бы для меня, так как вход в систему (аутентификация) происходит на другом микросервисе.

3. Покопавшись в коде, я понимаю, что именно здесь происходит волшебство. Когда предыдущий запрос был аутентифицирован, HttpSession сохраненный (пока пустой) SecurityContext заполняется Authenticated объектом и всей информацией. Затем это сохраняется в хранилище сеансов с ключом атрибута SPRING_SECURITY_CONTEXT . Следующий запрос, если файл cookie сеанса найден, используется для загрузки HttpSession и, следовательно, старого атрибута, который затем восстанавливается HttpSessionSecurityContextRepository в текущий SecurityContext .. таким образом, пользователь будет аутентифицирован …

4. .. когда запускается диспетчер аутентификации. Все это говорит о том, что у меня это никогда не сработает. Весь сеанс создается извне, так что атрибут SecurityContext не сериализуется в сеанс (и это невозможно сделать из внешней системы) — поэтому он никогда не будет работать из коробки. ИМХО, мне нужен пользовательский фильтр, восстанавливающий SecurityContext атрибут «В сеанс SPRING_SECURITY_CONTEXT «, используя данные, которые у меня уже есть из сеанса (имя пользователя, идентификатор и прочее). Затем это позже будет подхвачено и автоматически обработано как SecurityContext HttpSessionSecurityContextRepository

5. Или я мог бы просто переопределить HttpSessionSecurityContextRepository и добавить пользовательскую логику, чтобы восстановить контекст непосредственно из сеанса (что для меня звучит лучше)

Ответ №1:

Спасибо @Steve Riesenberg за то, что дал мне достаточно подсказок, чтобы найти правильное решение!

Чтобы понять мое решение и понять, когда нужно пройти этот дополнительный маршрут, сначала объясните интеграцию по умолчанию:

Классическая интеграция весенней сессии в spring-безопасность

Когда вы используете spring-security, включая аутентификацию через приложение spring-boot, интеграция spring-session и spring-security станет естественной без каких-либо дополнительных требований.

Когда вы авторизуете (входите в систему) своего пользователя изначально в первый раз с помощью аутентификации через spring-security, это будет:

  • Сохраните Authentication объект в SecurityContext этом запросе. — Затем SecurityContext он будет сохранен в HttpSession (если таковой существует), с весенней сессией, для которой вы когда-либо настроили весеннюю сессию (redis/mongo).
  • Хранится SecurityContext с использованием атрибута сеанса SPRING_SECURITY_CONTEXT в правом ключе в общих (сериализованных) данных сеанса.

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

  • весенняя сессия загружает HttpSession данные из вашего хранилища (включая атрибут SecurityContext in the session с ключом SPRING_SECURITY_CONTEXT
  • spring security позвонит HttpSessionSecurityContextRepository в самом начале SecurityFilter цепочки и проверит HttpSession наличие атрибута сеанса SPRING_SECURITY_CONTEXT и наличие SecurityContext найденного. Если да, он использует это SecurityContext и загружает в качестве current запроса SecurityContext . Поскольку этот контекст включает в себя уже аутентифицированный Authentication объект из предыдущей проверки подлинности, AuthenticationManager/Provider аутентификация будет пропущена, поскольку все это сделано, и ваш запрос рассматривается как аутентифицированный.

Это ванильный способ, и у него есть одно требование — процесс аутентификации (входа в систему) должен записывать SecurityContext данные в HttpSession объект во время процесса входа в систему.


Мой случай — внешний процесс входа в систему

В моем случае весь процесс входа в систему обрабатывается внешним микросервисом, не использующим пружинную загрузку. Тем не менее, он хранит сеанс в (внешнем) хранилище сеансов, которым в моем случае является MongoDB.

Весенняя сессия правильно настроена и может считывать этот сеанс, используя файл cookie сеанса/идентификатор сеанса, и загружать сеанс, созданный извне.

Большое » но » заключается в том, что этот внешний логин не будет хранить какие SecurityContext -либо данные сеанса, поскольку он не может этого сделать. На этом этапе — если у вас есть внешняя служба входа в систему, создающая сеанс, и это служба весенней загрузки, убедитесь, что вы написали SecurityContext ее правильно. Таким образом, все остальные микросервисы могут правильно загрузить этот сеанс (аутентифицированный), просто используя идентификатор сеанса и интеграцию spring-session/security по умолчанию.

Поскольку это не вариант для меня, и если это не для вас, следующее решение, по-видимому, является способом «по дизайну» в spring-boot/security IMHO:

Вы реализуете свой собственный CustomHttpSessionSecurityContextRepository и регистрируете его в конфигурации безопасности с помощью

 public class ApplicationSecurity extends WebSecurityConfigurerAdapter
{
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.securityContext()
    .securityContextRepository(new FromExternalSessionSecurityContextRepository());
  }
}
 

Это гарантирует, что мы заменим запас HttpSessionSecurityContextRepository нашей собственной реализацией.

Теперь наша пользовательская реализация

 public class CustomExternalSessionSecurityContextRepository implements SecurityContextRepository
{
  @Override
  public SecurityContext loadContext(final HttpRequestResponseHolder requestResponseHolder)
  {
    HttpServletRequest request = requestResponseHolder.getRequest();
    HttpSession httpSession = request.getSession(false);

    // No session yet, thus we cannot load an authentication-context from the session. Create a new, blanc
    // Authentication context and let others AuthenticationProviders deal with it.
    if (httpSession == null) {
      return generateNewSecurityContext();
    }

    Optional<Long> userId = Optional.ofNullable(httpSession.getAttribute(Attribute.SUBJECT_ID.attributeName))

    SecurityContext sc = generateNewSecurityContext();

    if (userId.isEmpty()) {
      // Return a emtpy context if the session has neither no subjectId
      return sc;
    }

    // This is an session of an authenticated user. Create the security context with the principal we know from
    // the session and mark the user authenticated
    // OurAuthentication uses userId.get() as principal and implements Authentication
    var authentication = new OurAuthentication(userId.get());
    authentication.setAuthenticated(true);
    sc.setAuthentication(authentication);
    httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, sc);

    return sc;
  }

  @Override
  public void saveContext(
    final SecurityContext context, final HttpServletRequest request, final HttpServletResponse response
  )
  {
    // do implement storage back into HttpSession if you want spring-boot to be
    // able to write it. 
  }

  @Override
  public boolean containsContext(final HttpServletRequest request)
  {
    HttpSession session = request.getSession(false);
    if (session == null) {
      return false;
    }
    return session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null;
  }

  private SecurityContext generateNewSecurityContext()
  {
    return SecurityContextHolder.createEmptyContext();
  }
}
 

Итак, теперь изменилось поведение того, как spring-security загружает a SecurityContext из сеанса. Вместо того, чтобы ожидать SecurityContext , что вы уже будете присутствовать, мы проверяем правильность сеанса, создаем SecurityContext его из заданных данных и сохраняем, возвращаем его. Это заставит всю следующую цепочку использовать и уважать это SecurityContext