В этом разделе обсуждается высокоуровневая архитектура Spring Security в приложениях на базе сервлетов.

Краткое описание фильтров

Поддержка сервлетов в Spring Security основана на экземплярах Filter сервлетов, поэтому сначала полезно рассмотреть роль Filter в целом. На рисунке ниже показана типичная уровневая структура обработчиков для одного HTTP-запроса.

Клиент отправляет запрос приложению, а контейнер создает FilterChain, который содержит экземпляры Filter и Servlet, который должен обработать HttpServletRequest на основе пути URI запроса. В приложении Spring MVC Servlet является экземпляром DispatcherServlet. Только один Servlet может обрабатывать один HttpServletRequest и HttpServletResponse. Однако при этом можно использовать более одного Filter для:

  • Предотвращения вызова нижестоящих Filter или Servlet. В этом случае Filter обычно записывает HttpServletResponse.

  • Изменения HttpServletRequest или HttpServletResponse, используемых последующими Filter и Servlet

Мощность Filter зависит от FilterChain, который передается в него.

Пример использования FilterChain
Java
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	// делаем что-нибудь перед выполнением остальной части приложения
    chain.doFilter(request, response); // invoke the rest of the application
    // делаем что-нибудь после выполнения остальной части приложения
}
Kotlin
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
    // делаем что-нибудь перед выполнением остальной части приложения
    chain.doFilter(request, response) // invoke the rest of the application
    // делаем что-нибудь после выполнения остальной части приложения
}

Поскольку Filter влияет только на нижестоящие Filter и Servlet, порядок вызова каждого Filter чрезвычайно важен.

DelegatingFilterProxy

Spring предусматривает реализацию Filter под названием DelegatingFilterProxy, которая позволяет установить мост между жизненным циклом контейнера сервлетов и ApplicationContext из Spring. Контейнер сервлетов позволяет регистрировать экземпляры Filter, используя свои собственные стандарты, но ему неизвестно о бинах, определенных Spring. DelegatingFilterProxy можно зарегистрировать через стандартные механизмы контейнера сервлетов, но при этом делегировать всю работу бину Spring, реализующему Filter.

Здесь показано, как DelegatingFilterProxy вписывается в схему с экземплярами Filter и FilterChain.

DelegatingFilterProxy осуществляет поиск Bean Filter0 в ApplicationContext и затем вызывает Bean Filter0. Псевдокод DelegatingFilterProxy показан ниже.

Псевдокод DelegatingFilterProxy
Java
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	// В отложенном режиме получаем фильтр, который был зарегистрирован как бин Spring
	// Для примера в DelegatingFilterProxy делегат является экземпляром Bean Filter0
	Filter delegate = getFilterBean(someBeanName);
	// делегируем работу бину Spring
	delegate.doFilter(request, response);
}
Kotlin
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
	// В отложенном режиме получаем фильтр, который был зарегистрирован как бин Spring
	// Для примера в DelegatingFilterProxy делегат является экземпляром Bean Filter0
	val delegate: Filter = getFilterBean(someBeanName)
	// делегируем работу бину Spring
	delegate.doFilter(request, response)
}

Еще одним преимуществом DelegatingFilterProxy является то, что он позволяет отложить поиск экземпляров бинов Filter. Это важно, поскольку контейнеру необходимо зарегистрировать экземпляры Filter перед запуском контейнера. Однако Spring обычно использует ContextLoaderListener для загрузки бинов Spring, что происходит только после регистрации экземпляров Filter.

FilterChainProxy

Средства поддержки сервлетов в Spring Security содержатся в FilterChainProxy. FilterChainProxy – это специализированный Filter, предусматриваемый Spring Security, который позволяет делегировать полномочия многим экземплярам Filter через <SecurityFilterChain. Поскольку FilterChainProxy является бином, он обычно обёрнут в DelegatingFilterProxy.

SecurityFilterChain

SecurityFilterChain используется FilterChainProxy для определения того, какие Filter из Spring Security необходимо вызвать для данного запроса.

Фильтры безопасности в SecurityFilterChain обычно являются бинами, но они зарегистрированы в FilterChainProxy вместо DelegatingFilterProxy. FilterChainProxy предусматривает ряд преимуществ по сравнению с регистрацией непосредственно в контейнере сервлетов или DelegatingFilterProxy. Во-первых, он обеспечивает отправную точку для всех средств поддержки сервлетов в Spring Security. По этой причине, если вы будете пробовать устранить неполадки в средствах поддержки сервлетов Spring Security, начать лучше всего будет с добавления точки отладки в FilterChainProxy.

Во-вторых, поскольку FilterChainProxy занимает центральное место при использовании Spring Security, он может выполнять задачи, которые не рассматриваются в качестве опциональных. Например, он очищает SecurityContext, чтобы избежать утечки памяти. Он также применяет HttpFirewall из Spring Security для защиты приложений от определенных типов атак.

Кроме того, он обеспечивает большую гибкость при определении момента, когда необходимо вызывать SecurityFilterChain. В контейнере сервлетов экземпляры Filter вызываются только на основе URL-адреса. Однако FilterChainProxy может определить вызов на основе чего угодно в HttpServletRequest, используя интерфейс RequestMatcher.

Фактически, FilterChainProxy можно использовать для определения того, какой SecurityFilterChain следует использовать. Это позволяет обеспечить совершенно отдельную конфигурацию для различных фрагментов вашего приложения.

На рисунке c несколькими SecurityFilterChain экземпляр FilterChainProxy решает, какую SecurityFilterChain следует использовать. Будет вызвана только первая совпадающая SecurityFilterChain. Если запрашивается URL-адрес /api/messages/, он сначала будет сопоставлен с шаблоном SecurityFilterChain0 через /api/**, поэтому будет вызван только SecurityFilterChain0, даже если он также будет соответствовать SecurityFilterChainn. Если будет запрошен URL-адрес /messages/, он не будет сопоставлен с шаблоном SecurityFilterChain0 через /api/**, поэтому FilterChainProxy будет продолжать перебирать каждую SecurityFilterChain. Предполагается, что другие экземпляры SecurityFilterChain, совпадающие с SecurityFilterChainn, не будут вызваны.

Обратите внимание, что в SecurityFilterChain0 сконфигурированы только три экземпляра Filter. Однако в SecurityFilterChainn сконфигурированы четыре Filter. Важно отметить, что каждая SecurityFilterChain может быть уникальной и сконфигурированной изолированно. На самом деле SecurityFilterChain может иметь ноль экземпляров Filter, если приложению нужно, чтобы Spring Security игнорировал определенные запросы.

Фильтры безопасности

Фильтры Spring Security добавляются в FilterChainProxy через API для SecurityFilterChain. Порядок экземпляров Filter имеет значение. Обычно необходимости знать порядок следования Filter в Spring Security нет. Тем не менее, в некоторых случаях полезно знать, в каком порядке они расположены.

Ниже приведен полный упорядоченный список фильтров Spring Security:

  • ForceEagerSessionCreationFilter

  • ChannelProcessingFilter

  • WebAsyncManagerIntegrationFilter

  • SecurityContextPersistenceFilter

  • HeaderWriterFilter

  • CorsFilter

  • CsrfFilter

  • LogoutFilter

  • OAuth2AuthorizationRequestRedirectFilter

  • Saml2WebSsoAuthenticationRequestFilter

  • X509AuthenticationFilter

  • AbstractPreAuthenticatedProcessingFilter

  • CasAuthenticationFilter

  • OAuth2LoginAuthenticationFilter

  • Saml2WebSsoAuthenticationFilter

  • UsernamePasswordAuthenticationFilter

  • OpenIDAuthenticationFilter

  • DefaultLoginPageGeneratingFilter

  • DefaultLogoutPageGeneratingFilter

  • ConcurrentSessionFilter

  • DigestAuthenticationFilter

  • BearerTokenAuthenticationFilter

  • BasicAuthenticationFilter

  • RequestCacheAwareFilter

  • SecurityContextHolderAwareRequestFilter

  • JaasApiIntegrationFilter

  • RememberMeAuthenticationFilter

  • AnonymousAuthenticationFilter

  • OAuth2AuthorizationCodeGrantFilter

  • SessionManagementFilter

  • ExceptionTranslationFilter

  • FilterSecurityInterceptor

  • SwitchUserFilter

Обработка исключений безопасности

ExceptionTranslationFilter позволяет преобразовывать AccessDeniedException и AuthenticationException в HTTP-ответы.

ExceptionTranslationFilter добавляется в FilterChainProxy в качество одного из фильтров безопасности.

  • 1 Сначала ExceptionTranslationFilter обращается к FilterChain.doFilter(request, response), чтобы вызвать остальную часть приложения.

  • 2 Если пользователь не аутентифицирован или возникает AuthenticationException, то запустите процедуру аутентификации.

    • TheSecurityContextHolder очищается

    • HttpServletRequest сохраняется в RequestCache. После того, как пользователь успешно пройдет аутентификацию, RequestCache будет использован для воспроизведения исходного запроса.

    • AuthenticationEntryPoint используется для запроса учетных данных у клиента. Например, он может перенаправить на страницу входа в систему или отправить заголовок WWW-Authenticate.

  • 3 Иначе, если возникнет AccessDeniedException, то в доступе будет отказано. AccessDeniedHandler вызывается для обработки отказа в доступе.

Если приложение не генерирует AccessDeniedException или AuthenticationException, то ExceptionTranslationFilter ничего не делает.

Псевдокод для ExceptionTranslationFilter выглядит примерно так:

Псевдокод ExceptionTranslationFilter
try {
	filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		startAuthentication();
	} else {
		accessDenied();
	}
}
  1. Из краткого описания экземпляров Filter можно вспомнить, что вызов FilterChain.doFilter(request, response) эквивалентен вызову остальной части приложения. Это означает, что если другая часть приложения (например, FilterSecurityInterceptor или метод безопасности) сгенерирует AuthenticationException или AccessDeniedException, то эти исключения будут перехвачены и обработаны на этом этапе.
  2. Если пользователь не аутентифицирован или появляется AuthenticationException, то запустите процедуру аутентификации.
  3. В противном случае в доступе будет отказано