Spring Security — ошибка аутентификации «Запомнить меня»

Мы используем Spring MVC и сталкиваемся со следующей проблемой, связанной с аутентификацией «Запомнить меня»:

  1. Пользователь входит в систему с установленным флажком «Запомнить меня», работает правильно, таблица persist_login обновляется, как и ожидалось.
  2. Мы перезапускаем сервер приложений, возможно, после развертывания и т. д.
  3. Пользователь обновляет страницу, мы видим сообщение об ошибке на рисунке 1, пользователь перенаправляется на страницу входа (не видит ошибку)
  4. Несмотря на ошибку, токен записи persist_login обновился (серия осталась такой же, как и до обновления), токен spring Remember Me также остался прежним.
  5. Пользователь обновляет страницу во второй раз, он входит в систему, как будто ничего не произошло

Рисунок 1. Сообщение об ошибке

Apr 24, 2014 9:29:15 AM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [workmarket] in context with path [] threw exception
java.lang.ClassCastException: org.springframework.security.web.firewall.FirewalledResponse cannot be cast to org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper
at org.springframework.security.web.context.HttpSessionSecurityContextRepository.saveContext(HttpSessionSecurityContextRepository.java:99)
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:87)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:113)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:139)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:54)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:45)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:167)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:182)
at com.workmarket.web.authentication.CustomLinkedInLoginFilter.doFilter(CustomLinkedInLoginFilter.java:100)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
at com.workmarket.web.authentication.CustomLoginFilter.doFilter(CustomLoginFilter.java:100)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:105)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
at org.springframework.security.saml.metadata.MetadataGeneratorFilter.doFilter(MetadataGeneratorFilter.java:86)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:323)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:173)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:343)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:260)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:936)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1004)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:589)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)

Рисунок 2. Фильтры безопасности

<sec:custom-filter before="FIRST" ref="metadataGeneratorFilter"/>
<sec:custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter"/>
<sec:custom-filter ref="customLoginFilter" position="FORM_LOGIN_FILTER"/>
<sec:custom-filter ref="customLinkedInLoginFilter" after="FORM_LOGIN_FILTER"/>
<sec:custom-filter ref="rememberMeFilter" position="REMEMBER_ME_FILTER"/>
<sec:custom-filter ref="switchUserProcessingFilter" position="SWITCH_USER_FILTER"/>
<sec:custom-filter ref="authenticatedUserInitializer" before="FILTER_SECURITY_INTERCEPTOR"/>
<sec:custom-filter ref="publicWorkRequestFilter" after="FILTER_SECURITY_INTERCEPTOR"/>
<sec:custom-filter ref="securityContextCleanupFilter" after="SESSION_MANAGEMENT_FILTER"/>
<sec:custom-filter ref="customLogoutFilter" position="LOGOUT_FILTER"/>

Рис. 3. Настройка функции «Запомнить меня»

<!-- Remember me -->
<bean id="rememberMeFilter" class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
    <constructor-arg index="0" ref="org.springframework.security.authenticationManager"/>
    <constructor-arg index="1" ref="rememberMeServices"/>
</bean>

<bean id="rememberMeAuthenticationProvider" class="org.springframework.security.authentication.RememberMeAuthenticationProvider">
    <constructor-arg index="0" value="[REMOVED]"/>
</bean>

<bean id="rememberMeServices" class="org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices">
    <constructor-arg index="0" value="[REMOVED]"/>
    <constructor-arg index="1" ref="userDetailsService"/>
    <constructor-arg index="2" ref="persistentTokenRepository"/>
</bean>

Версии

  1. пружинный каркас -- 3.2.4.RELEASE
  2. пружинная безопасность -- 3.1.0.RELEASE
  3. весна-безопасность-saml2-ядро -- 1.0.0.RC2
  4. опенсамл -- 2.5.3

====== ОБНОВЛЕНИЕ ======

Мы заметили, что удаление этих двух фильтров, связанных с SAML, решает эту проблему, однако нам нужно, чтобы они работали...

<sec:custom-filter before="FIRST" ref="metadataGeneratorFilter"/>
<sec:custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter"/>

====== ОБНОВЛЕНИЕ 2 ======

Детали определения samlFilter.

<bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
  <security:filter-chain-map request-matcher="ant">
  <security:filter-chain pattern="/saml/login/**" filters="samlEntryPoint"/>
  <security:filter-chain pattern="/saml/SSO/**" filters="samlWebSSOProcessingFilter"/>
  </security:filter-chain-map>
</bean>

<bean id="samlEntryPoint" class="org.springframework.security.saml.SAMLEntryPoint">
  <property name="defaultProfileOptions">
    <bean class="org.springframework.security.saml.websso.WebSSOProfileOptions">
      <property name="includeScoping" value="false"/>
    </bean>
  </property>
</bean>

<bean id="samlWebSSOProcessingFilter" class="org.springframework.security.saml.SAMLProcessingFilter">
  <property name="authenticationManager" ref="samlAuthenticationManager"/>
  <property name="authenticationSuccessHandler">
    <bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
      <property name="defaultTargetUrl" value="/"/>
    </bean>
  </property>
</bean>

Заранее спасибо.


person Steve N    schedule 24.04.2014    source источник
comment
Можете ли вы попробовать использовать Spring Security 3.1.6.RELEASE? ЕСЛИ это не сработает, попробуйте версию 3.1.2.RELEASE, которая используется в примере приложения.   -  person Rob Winch    schedule 25.04.2014
comment
Пробовал с обеими версиями и все еще сталкивается с проблемой. Это должно быть что-то связанное с фильтрами SAML.   -  person Steve N    schedule 28.04.2014
comment
Пробовали ли вы удалить только один из двух фильтров и посмотреть, вызывает ли это ошибку? Можете ли вы предоставить свои определения bean-компонентов для двух фильтров, которые вызывают проблемы.   -  person Rob Winch    schedule 28.04.2014
comment
Это определенно samlFilter, удалите его, и все снова заработает. Я добавлю определение bean-компонента samlFilter выше.   -  person Steve N    schedule 29.04.2014


Ответы (2)


Это вызвано тем, что FilterChainProxy используется после SecurityContextPersistenceFilter. В частности, HttpFirewall FilterChainProxy заменяет HttpServletResponse на DefaultHttpFirewall, который больше не реализует SavedRequest. Чтобы обойти это, вы можете внедрить пользовательский HttpFirewall в samlFilter FilterChainProxy, который возвращает тот же HttpServletResponse, который передается в него. Например:

public class DoNothingHttpFirewall implements HttpFirewall {

    public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
        return new MyFirewalledRequest(request);
    }

    public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
        return response;
    }

    private static class MyFirewalledRequest extends FirewalledRequest {
         MyFirewalledRequest(HttpServletRequest r) {
             super(r);
         }
         public void reset() {}
    }
}

Затем вы можете подключить его, используя:

<bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
  <security:filter-chain-map request-matcher="ant">
    <security:filter-chain pattern="/saml/login/**" filters="samlEntryPoint"/>
    <security:filter-chain pattern="/saml/SSO/**" filters="samlWebSSOProcessingFilter"/>
  </security:filter-chain-map>
  <property name="firewall">
    <bean class="DoNothingHttpFirewall"/>
  </property>
</bean>

Я зарегистрировал тикет, чтобы сделать эту работу прозрачной в будущем https://jira.spring.io/browse/SEC-2578

person Rob Winch    schedule 29.04.2014
comment
Это действительно решает проблему. Большое спасибо @RobWinch. - person Steve N; 29.04.2014

Я тоже столкнулся с этой проблемой. Мое решение основано на ответе @RobWinch, но с использованием, возможно, более безопасной реализации:

  1. создайте класс, который расширяет DefaultHttpFirewall
  2. Переопределите метод getFirewalledResponse(HttpResponse response), заменив его на тот, который проверяет тип ответа; если это instanceof SaveContextOnUpdateOrErrorResponseWrapper, то тривиально вернуть предоставленный ответ, а в противном случае вернуть return super.getFirewalledResponse().
  3. Внедрите bean-компонент этого класса, используя внедрение свойств, описанное в ответе @RobWinch.

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

person acidnbass    schedule 27.04.2016