Как я могу использовать Spring Security без сеансов?

99

Я создаю веб-приложение с Spring Security, которое будет жить на Amazon EC2 и будет использовать Amazon Elastic Load Balancers. К сожалению, ELB не поддерживает липкие сеансы, поэтому мне нужно убедиться, что мое приложение работает правильно без сеансов.

Пока я настроил RememberMeServices для назначения токена через cookie, и это работает нормально, но я хочу, чтобы срок действия cookie истекал вместе с сеансом браузера (например, когда браузер закрывается).

Я должен представить, что я не первый, кто хочет использовать Spring Security без сессий ... есть предложения?

Джаррод Карлсон
источник

Ответы:

125

В Spring Security 3 с Java Config вы можете использовать HttpSecurity.sessionManagement () :

@Override
protected void configure(final HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
Бен Хатчисон
источник
2
Это правильный ответ для конфигурации Java, отражающий то, что @sappenin правильно указал для конфигурации xml в комментарии к принятому ответу. Мы используем этот метод, и действительно, наше приложение бессеансовое.
Пол
Это имеет побочный эффект. Контейнер Tomcat будет добавлять "; jsessionid = ..." к запросам изображений, таблиц стилей и т. Д., Потому что Tomcat не любит иметь состояние без состояния, и Spring Security затем блокирует эти ресурсы при первой загрузке, потому что "URL-адрес содержал потенциально вредоносная String ';' ".
workerjoe,
@workerjoe Итак, что вы пытаетесь сказать этой конфигурацией java, сеансы создаются не с помощью Spring Security, а с помощью tomcat?
Вишвас Атрей,
@VishwasAtrey Насколько я понимаю (что может быть неверно), Tomcat создает и поддерживает сеансы. Spring использует их, добавляя свои собственные данные. Я попытался создать веб-приложение без сохранения состояния, но оно не сработало, как я упоминал выше. См. Этот ответ на мой собственный вопрос, чтобы узнать больше.
workerjoe
28

В Spring Securitiy 3.0 это стало еще проще. Если вы используете конфигурацию пространства имен, вы можете просто сделать следующее:

<http create-session="never">
  <!-- config -->
</http>

Или вы можете настроить SecurityContextRepository как нуль, и ничто никогда не спасаются таким образом , как хорошо .

Джаррод Карлсон
источник
5
Это не сработало так, как я думал. Вместо этого ниже есть комментарий, в котором проводится различие между «никогда» и «без гражданства». Используя «никогда», мое приложение все еще создавало сеансы. Используя «без сохранения состояния», мое приложение фактически перешло без состояния, и мне не нужно было реализовывать какие-либо переопределения, упомянутые в других ответах. См. Проблему
sappenin
27

Сегодня мы работали над той же проблемой (внедрение настраиваемого SecurityContextRepository в SecurityContextPersistenceFilter) в течение 4-5 часов. Наконец, мы в этом разобрались. Прежде всего, в разделе 8.3 Spring Security исх. doc, есть определение bean-компонента SecurityContextPersistenceFilter

<bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
    <property name='securityContextRepository'>
        <bean class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'>
            <property name='allowSessionCreation' value='false' />
        </bean>
    </property>
</bean>

И после этого определения есть следующее объяснение: «В качестве альтернативы вы можете предоставить нулевую реализацию интерфейса SecurityContextRepository, которая предотвратит сохранение контекста безопасности, даже если сеанс уже был создан во время запроса».

Нам нужно было внедрить наш настраиваемый SecurityContextRepository в SecurityContextPersistenceFilter. Поэтому мы просто изменили приведенное выше определение bean-компонента на наш собственный impl и поместили его в контекст безопасности.

Когда мы запускали приложение, мы проследили журналы и увидели, что SecurityContextPersistenceFilter не использует наш настраиваемый impl, он использует HttpSessionSecurityContextRepository.

После того, как мы попробовали еще несколько вещей, мы выяснили, что должны предоставить нашему настраиваемому SecurityContextRepository имплицит с атрибутом «security-context-repository-ref» пространства имен «http». Если вы используете пространство имен «http» и хотите внедрить свой собственный SecurityContextRepository impl, попробуйте атрибут security-context-repository-ref.

Когда используется пространство имен «http», отдельное определение SecurityContextPersistenceFilter игнорируется. Как я скопировал выше, справочный документ. не говорит об этом.

Пожалуйста, поправьте меня, если я неправильно понял вещи.

Басри Кахвечи
источник
Спасибо, это ценная информация. Попробую в своем приложении.
Джефф Эванс,
Спасибо, это то, что мне нужно с Spring 3.0
Джастин Людвиг
1
Вы совершенно правы, когда говорите, что пространство имен http не позволяет использовать настраиваемый SecurityContextPersistenceFilter, мне потребовалось несколько часов отладки, чтобы понять это,
Хайме Хаблуцель
Большое спасибо за публикацию этого сообщения! Я собирался вырвать то немногое, что у меня есть. Мне было интересно, почему метод setSecurityContextRepository SecurityContextPersistenceFilter устарел (в документах говорится, что нужно использовать инъекцию конструктора, что тоже неправильно).
fool4jesus
10

Взгляните на SecurityContextPersistenceFilterкласс. Он определяет, как SecurityContextHolderзаполняется. По умолчанию он используется HttpSessionSecurityContextRepositoryдля хранения контекста безопасности в сеансе http.

Я довольно легко реализовал этот механизм с помощью custom SecurityContextRepository.

См. securityContext.xmlНиже:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
       http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
       http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd">

    <context:annotation-config/>

    <sec:global-method-security secured-annotations="enabled" pre-post-annotations="enabled"/>

    <bean id="securityContextRepository" class="com.project.server.security.TokenSecurityContextRepository"/>

    <bean id="securityContextFilter" class="com.project.server.security.TokenSecurityContextPersistenceFilter">
        <property name="repository" ref="securityContextRepository"/>
    </bean>

    <bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
        <constructor-arg value="/login.jsp"/>
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
            </list>
        </constructor-arg>
    </bean>

    <bean id="formLoginFilter"
          class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="authenticationSuccessHandler">
            <bean class="com.project.server.security.TokenAuthenticationSuccessHandler">
                <property name="defaultTargetUrl" value="/index.html"/>
                <property name="passwordExpiredUrl" value="/changePassword.jsp"/>
                <property name="alwaysUseDefaultTargetUrl" value="true"/>
            </bean>
        </property>
        <property name="authenticationFailureHandler">
            <bean class="com.project.server.modules.security.CustomUrlAuthenticationFailureHandler">
                <property name="defaultFailureUrl" value="/login.jsp?failure=1"/>
            </bean>
        </property>
        <property name="filterProcessesUrl" value="/j_spring_security_check"/>
        <property name="allowSessionCreation" value="false"/>
    </bean>

    <bean id="servletApiFilter"
          class="org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter"/>

    <bean id="anonFilter" class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
        <property name="key" value="ClientApplication"/>
        <property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
    </bean>


    <bean id="exceptionTranslator" class="org.springframework.security.web.access.ExceptionTranslationFilter">
        <property name="authenticationEntryPoint">
            <bean class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
                <property name="loginFormUrl" value="/login.jsp"/>
            </bean>
        </property>
        <property name="accessDeniedHandler">
            <bean class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                <property name="errorPage" value="/login.jsp?failure=2"/>
            </bean>
        </property>
        <property name="requestCache">
            <bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
        </property>
    </bean>

    <alias name="filterChainProxy" alias="springSecurityFilterChain"/>

    <bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
        <sec:filter-chain-map path-type="ant">
            <sec:filter-chain pattern="/**"
                              filters="securityContextFilter, logoutFilter, formLoginFilter,
                                        servletApiFilter, anonFilter, exceptionTranslator, filterSecurityInterceptor"/>
        </sec:filter-chain-map>
    </bean>

    <bean id="filterSecurityInterceptor"
          class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
        <property name="securityMetadataSource">
            <sec:filter-security-metadata-source use-expressions="true">
                <sec:intercept-url pattern="/staticresources/**" access="permitAll"/>
                <sec:intercept-url pattern="/index.html*" access="hasRole('USER_ROLE')"/>
                <sec:intercept-url pattern="/rpc/*" access="hasRole('USER_ROLE')"/>
                <sec:intercept-url pattern="/**" access="permitAll"/>
            </sec:filter-security-metadata-source>
        </property>
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="accessDecisionManager" ref="accessDecisionManager"/>
    </bean>

    <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
        <property name="decisionVoters">
            <list>
                <bean class="org.springframework.security.access.vote.RoleVoter"/>
                <bean class="org.springframework.security.web.access.expression.WebExpressionVoter"/>
            </list>
        </property>
    </bean>

    <bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
        <property name="providers">
            <list>
                <bean name="authenticationProvider"
                      class="com.project.server.modules.security.oracle.StoredProcedureBasedAuthenticationProviderImpl">
                    <property name="dataSource" ref="serverDataSource"/>
                    <property name="userDetailsService" ref="userDetailsService"/>
                    <property name="auditLogin" value="true"/>
                    <property name="postAuthenticationChecks" ref="customPostAuthenticationChecks"/>
                </bean>
            </list>
        </property>
    </bean>

    <bean id="customPostAuthenticationChecks" class="com.project.server.modules.security.CustomPostAuthenticationChecks"/>

    <bean name="userDetailsService" class="com.project.server.modules.security.oracle.UserDetailsServiceImpl">
        <property name="dataSource" ref="serverDataSource"/>
    </bean>

</beans>
Лукас Герман
источник
1
Привет, Лукас, не могли бы вы подробнее рассказать о реализации репозитория контекста безопасности?
Джим Даунинг,
1
Класс TokenSecurityContextRepository содержит HashMap <String, SecurityContext> contextMap. В методе loadContext () проверяется, существует ли SecurityContext для хэш-кода сеанса, переданного с помощью requestParameter sid, cookie или настраиваемого requestHeader или комбинации любого из вышеперечисленных. Возвращает SecurityContextHolder.createEmptyContext (), если контекст не может быть разрешен. Метод saveContext помещает разрешенный контекст в contextMap.
Лукас Херман
8

На самом деле create-session="never"не означает быть полностью апатридом. Для этого есть проблема в управлении проблемами Spring Security.

хлейнон
источник
3

После борьбы с многочисленными решениями, опубликованными в этом ответе, чтобы попытаться заставить что-то работать при использовании <http>конфигурации пространства имен, я наконец нашел подход, который действительно работает для моего варианта использования. На самом деле я не требую, чтобы Spring Security не запускал сеанс (потому что я использую сеанс в других частях приложения), просто он вообще не «запоминает» аутентификацию в сеансе (его следует перепроверить каждый запрос).

Во-первых, я не мог понять, как использовать описанную выше технику «нулевой реализации». Было неясно, должны ли вы установить securityContextRepository в nullили в реализацию без операции. Первый не работает, потому что NullPointerExceptionвнутрь бросают SecurityContextPersistenceFilter.doFilter(). Что касается реализации без операции, я попытался реализовать ее самым простым способом, который я мог представить:

public class NullSpringSecurityContextRepository implements SecurityContextRepository {

    @Override
    public SecurityContext loadContext(final HttpRequestResponseHolder requestResponseHolder_) {
        return SecurityContextHolder.createEmptyContext();
    }

    @Override
    public void saveContext(final SecurityContext context_, final HttpServletRequest request_,
            final HttpServletResponse response_) {
    }

    @Override
    public boolean containsContext(final HttpServletRequest request_) {
        return false;
    }

}

В моем приложении это не работает из-за какого-то странного отношения ClassCastExceptionк response_типу.

Даже если предположить, что мне удалось найти реализацию, которая работает (просто не сохраняя контекст в сеансе), все еще остается проблема, как внедрить это в фильтры, созданные <http>конфигурацией. Вы не можете просто заменить фильтр в SECURITY_CONTEXT_FILTERпозиции, как указано в документации . Единственный способ подключиться к тому, SecurityContextPersistenceFilterчто создается под обложками, - это написать уродливый ApplicationContextAwarebean-компонент:

public class SpringSecuritySessionDisabler implements ApplicationContextAware {

    private final Logger logger = LoggerFactory.getLogger(SpringSecuritySessionDisabler.class);

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(final ApplicationContext applicationContext_) throws BeansException {
        applicationContext = applicationContext_;
    }

    public void disableSpringSecuritySessions() {
        final Map<String, FilterChainProxy> filterChainProxies = applicationContext
                .getBeansOfType(FilterChainProxy.class);
        for (final Entry<String, FilterChainProxy> filterChainProxyBeanEntry : filterChainProxies.entrySet()) {
            for (final Entry<String, List<Filter>> filterChainMapEntry : filterChainProxyBeanEntry.getValue()
                    .getFilterChainMap().entrySet()) {
                final List<Filter> filterList = filterChainMapEntry.getValue();
                if (filterList.size() > 0) {
                    for (final Filter filter : filterList) {
                        if (filter instanceof SecurityContextPersistenceFilter) {
                            logger.info(
                                    "Found SecurityContextPersistenceFilter, mapped to URL '{}' in the FilterChainProxy bean named '{}', setting its securityContextRepository to the null implementation to disable caching of authentication",
                                    filterChainMapEntry.getKey(), filterChainProxyBeanEntry.getKey());
                            ((SecurityContextPersistenceFilter) filter).setSecurityContextRepository(
                             new NullSpringSecurityContextRepository());
                        }
                    }
                }

            }
        }
    }
}

В любом случае, к решению, которое действительно работает, хотя и очень хакерское. Просто используйте, Filterкоторый удаляет запись сеанса, которую HttpSessionSecurityContextRepositoryищет, когда делает свое дело:

public class SpringSecuritySessionDeletingFilter extends GenericFilterBean implements Filter {

    @Override
    public void doFilter(final ServletRequest request_, final ServletResponse response_, final FilterChain chain_)
            throws IOException, ServletException {
        final HttpServletRequest servletRequest = (HttpServletRequest) request_;
        final HttpSession session = servletRequest.getSession();
        if (session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY) != null) {
            session.removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
        }

        chain_.doFilter(request_, response_);
    }
}

Затем в конфигурации:

<bean id="springSecuritySessionDeletingFilter"
    class="SpringSecuritySessionDeletingFilter" />

<sec:http auto-config="false" create-session="never"
    entry-point-ref="authEntryPoint">
    <sec:intercept-url pattern="/**"
        access="IS_AUTHENTICATED_REMEMBERED" />
    <sec:intercept-url pattern="/static/**" filters="none" />
    <sec:custom-filter ref="myLoginFilterChain"
        position="FORM_LOGIN_FILTER" />

    <sec:custom-filter ref="springSecuritySessionDeletingFilter"
        before="SECURITY_CONTEXT_FILTER" />
</sec:http>
Джефф Эванс
источник
Девять лет спустя это все еще правильный ответ. Теперь мы можем использовать конфигурацию Java вместо XML. Я добавил настраиваемый фильтр в свой WebSecurityConfigurerAdapterс " http.addFilterBefore(new SpringSecuritySessionDeletingFilter(), SecurityContextPersistenceFilter.class)"
workerjoe
3

Небольшое примечание: это "create-session", а не "create-sessions".

создать сеанс

Управляет энергией, с которой создается сеанс HTTP.

Если не установлен, по умолчанию используется «ifRequired». Другие варианты - «всегда» и «никогда».

Установка этого атрибута влияет на свойства allowSessionCreation и forceEagerSessionCreation HttpSessionContextIntegrationFilter. allowSessionCreation всегда будет истинным, если для этого атрибута не установлено значение «никогда». forceEagerSessionCreation имеет значение "false", если не установлено значение "always".

Таким образом, конфигурация по умолчанию позволяет создание сеанса, но не заставляет его. Исключением является то, что если включено управление одновременным сеансом, для forceEagerSessionCreation будет установлено значение true, независимо от того, какой здесь параметр. Использование «никогда» вызовет исключение во время инициализации HttpSessionContextIntegrationFilter.

Для получения конкретных сведений об использовании сеанса есть хорошая документация в javadoc HttpSessionSecurityContextRepository.

Джон Воган
источник
Все это отличные ответы, но я бился головой об стену, пытаясь понять, как этого добиться при использовании элемента конфигурации <http>. Даже при том auto-config=false, что вы, очевидно, не можете заменить то, что находится в SECURITY_CONTEXT_FILTERпозиции, своим собственным. Я взламывал, пытаясь отключить его с помощью некоторого ApplicationContextAwarebean (используя отражение, чтобы принудительно securityContextRepositoryустановить нулевую реализацию SessionManagementFilter), но без кубиков. И, к сожалению, я не могу перейти на весеннюю безопасность 3,1 года, которая могла бы обеспечить create-session=stateless.
Джефф Эванс
Посетите этот сайт, всегда информативный. Надеюсь, это поможет вам и другим " baeldung.com/spring-security-session " • всегда - сеанс всегда будет создаваться, если он еще не существует • ifRequired - сеанс будет создан только в случае необходимости (по умолчанию) • never - фреймворк никогда не создаст сам сеанс, но он будет использовать его, если он уже существует • без сохранения состояния - Spring Security не будет создавать или использовать
сеанс