#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
«, используя данные, которые у меня уже есть из сеанса (имя пользователя, идентификатор и прочее). Затем это позже будет подхвачено и автоматически обработано как SecurityContextHttpSessionSecurityContextRepository
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