如何在不使用会话的情况下使用Spring Security?

112

我正在使用Spring Security构建一个Web应用程序,它将托管在Amazon EC2上,并使用Amazon的弹性负载均衡器。不幸的是,ELB不支持粘性会话,因此我需要确保我的应用程序在没有会话的情况下正常工作。

到目前为止,我已经设置了RememberMeServices来通过cookie分配令牌,并且这很好用,但我希望cookie在浏览器会话结束时过期(例如,当浏览器关闭时)。

我想象我不是第一个想要在没有会话的情况下使用Spring Security的人……有什么建议吗?

8个回答

140
在使用Java Config的Spring Security 3中,您可以使用HttpSecurity.sessionManagement()
@Override
protected void configure(final HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

2
这是Java配置的正确答案,与@sappenin在已接受答案的评论中正确陈述的xml配置相似。我们使用这种方法,确实使我们的应用程序无需会话。 - Paul
这会产生副作用。Tomcat容器将在图像、样式表等请求后附加“;jsessionid = ...”,因为Tomcat不喜欢无状态,而Spring Security将在第一次加载时阻止这些资源,因为“URL包含潜在恶意字符串';'”。 - workerjoe
@workerjoe,你的意思是说通过这个Java配置,会话不是由Spring Security创建而是由Tomcat创建? - Vishwas Atrey
在我看来(可能是错误的),Tomcat 创建并维护会话。Spring 利用它们,添加自己的数据。我试图创建一个无状态的 Web 应用程序,但它没有起作用,就像我上面提到的那样。请参见我的问题的这个答案获取更多信息。 - workerjoe

28
我们今天花了4-5个小时研究同一个问题(注入自定义的SecurityContextRepository到SecurityContextPersistenceFilter中)。最后,我们解决了这个问题。 首先,在Spring Security参考文档的第8.3节中,有SecurityContextPersistenceFilter bean的定义。
<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 接口的 null 实现,这将防止安全上下文被存储,即使在请求期间已经创建了会话。”
我们需要将自定义的 SecurityContextRepository 注入到 SecurityContextPersistenceFilter 中。因此,我们只需更改上面的 bean 定义为我们的自定义实现,并将其放入安全上下文中。
当我们运行应用程序时,我们跟踪日志并发现 SecurityContextPersistenceFilter 没有使用我们的自定义实现,而是使用 HttpSessionSecurityContextRepository。
在尝试了其他一些方法之后,我们发现我们必须使用“http”命名空间的“security-context-repository-ref”属性来提供我们自定义的 SecurityContextRepository 实现。如果您使用“http”命名空间并想要注入自己的 SecurityContextRepository 实现,请尝试“security-context-repository-ref”属性。
当使用“http”命名空间时,独立的 SecurityContextPersistenceFilter 定义将被忽略。正如我复制的参考文档所述,它没有说明这一点。
如果我误解了什么,请纠正我。

谢谢,这是有价值的信息。我会在我的应用程序中尝试它。 - Jeff Evans
谢谢,这正是我需要的Spring 3.0。 - Justin Ludwig
1
当你说http命名空间不允许自定义SecurityContextPersistenceFilter时,你是相当准确的。我花了几个小时进行调试才弄清楚这一点。 - Jaime Hablutzel
非常感谢您发布这个问题!我差点把我剩下的头发都拔光了。我一直在想为什么SecurityContextPersistenceFilter的setSecurityContextRepository方法被弃用了(文档说要使用构造函数注入,但这也不正确)。 - fool4jesus

28

在Spring Security 3.0中似乎更加容易。如果您正在使用命名空间配置,那么只需按照以下方式操作:

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

或者您可以将SecurityContextRepository配置为null,那么就不会保存任何东西也可以这样做


5
这不像我想象的那样有效。相反,在下面的评论中区分了“从未”和“无状态”。使用“从未”,我的应用程序仍会创建会话。使用“无状态”,我的应用程序实际上变成了无状态,并且我不需要实现其他答案中提到的任何重写。请参见此处的JIRA问题:https://jira.springsource.org/browse/SEC-1424 - sappenin

11

看一下 SecurityContextPersistenceFilter 类。它定义了如何填充 SecurityContextHolder。默认情况下,它使用 HttpSessionSecurityContextRepository 将安全上下文存储在 HTTP 会话中。

我很容易地实现了这个机制,使用了自定义的 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
嗨Lukas,能否提供一些有关您的安全上下文存储库实现的详细信息? - Jim Downing
1
类TokenSecurityContextRepository包含HashMap<String, SecurityContext> contextMap。在loadContext()方法中,检查是否存在与通过请求参数sid、cookie、自定义请求头或任何上述组合传递的会话哈希码相对应的SecurityContext。 如果无法解析上下文,则返回SecurityContextHolder.createEmptyContext()。 saveContext方法将已解析的上下文放入contextMap中。 - Lukas Herman

8

实际上,create-session="never"并不意味着完全无状态。Spring Security问题管理中存在一个问题


4
编辑:从Spring Security 3.1开始,有一个STATELESS选项可以使用,可以代替所有这些。请参见其他答案。原始答案保留供后人参考。
在尝试使用<http>命名空间配置时,我苦苦挣扎,尝试了许多解决方案,最终找到了一种适用于我的用例的方法。我并不需要Spring Security不启动会话(因为我在应用程序的其他部分中使用会话),只需要它完全不“记住”身份验证(每次请求都应重新检查)。
首先,我无法弄清楚如何执行上面描述的“null实现”技术。不清楚您是否应将securityContextRepository设置为null还是无操作实现。前者不起作用,因为在SecurityContextPersistenceFilter.doFilter()中会抛出NullPointerException。至于无操作实现,我尝试以我能想象到的最简单的方式进行实现:
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,所以它在我的应用程序中不起作用。

假设我成功找到了一个可行的实现(只需不将上下文存储在会话中),仍然存在如何将其注入<http>配置构建的过滤器的问题。按照文档所述,您不能简单地替换SECURITY_CONTEXT_FILTER位置的过滤器。我发现唯一可以钩取在内部创建的SecurityContextPersistenceFilter的方法是编写一个丑陋的ApplicationContextAware bean:

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());
                        }
                    }
                }

            }
        }
    }
}

无论如何,以下是实际可行的解决方案,虽然有些hackish。只需使用一个删除HttpSessionSecurityContextRepository在执行其操作时查找的会话条目的Filter即可:
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>

1
九年过去了,这仍然是正确的答案。现在我们可以使用Java配置而不是XML。我在我的WebSecurityConfigurerAdapter中添加了自定义过滤器,使用"http.addFilterBefore(new SpringSecuritySessionDeletingFilter(), SecurityContextPersistenceFilter.class)"。 - workerjoe
如果您按照另一个答案中所述使用SessionCreationPolicy.STATELESS,那么这是不必要的。您一定还有其他问题。 - rougou
“STATELESS” 似乎是在3.1版本中添加的。在回答此问题时,最新发布的版本为3.0。这就解释了原因。 - Jeff Evans
感谢@JeffEvans,SpringSecuritySessionDeletingFilter为我节省了很多时间。在某些情况下,我需要无状态行为,而在其他情况下则不需要。 - Volatile

3

请注意:应该是"create-session"而不是"create-sessions"。

create-session

控制HTTP会话创建的热情程度。

如果未设置,则默认为"ifRequired"。其他选项包括"always"和"never"。

此属性的设置将影响HttpSessionContextIntegrationFilter的allowSessionCreation和forceEagerSessionCreation属性。除非将此属性设置为"never",否则allowSessionCreation始终为true。forceEagerSessionCreation为"false",除非将其设置为"always"。

因此,默认配置允许创建会话但不强制创建。异常情况是如果启用并发会话控制,则forceEagerSessionCreation将被设置为true,无论此处的设置如何。使用"never"将在HttpSessionContextIntegrationFilter初始化期间引发异常。

有关会话使用的详细信息,请参阅HttpSessionSecurityContextRepository javadoc中的文档。


这些都是很好的回答,但我一直在努力想要实现使用<http>配置元素时的情况。即使使用“auto-config=false”,显然也不能用自己的内容替换SECURITY_CONTEXT_FILTER位置里的内容。我一直在尝试使用一些ApplicationContextAware bean进行破解(使用反射将securityContextRepository强制设置为null实现SessionManagementFilter),但是没有成功。不幸的是,我无法切换到提供“create-session=stateless”的spring-security 3.1年版本。 - Jeff Evans
请访问这个网站,它总是提供有用的信息。希望这对你和其他人有所帮助 "http://www.baeldung.com/spring-security-session" • always – 如果不存在会话,则始终创建一个会话 • ifRequired – 仅在需要时创建会话(默认) • never – 框架不会自己创建会话,但如果已经存在会话,则会使用该会话 • stateless – Spring Security 不会创建或使用任何会话 - Java_Fire_Within

0

现在ELB支持粘性会话,我想从2016年开始。 但是也可以将您的会话存储在Redis中。


1
这并没有回答问题。一旦您拥有足够的声望,您将能够评论任何帖子;相反,提供不需要询问者澄清的答案。- 来自审核 - Subhashis Pandey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接