如何在没有会话的情况下使用 Spring 安全性?

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

到目前为止,我已经安装了 RememberMeServices,通过 cookie 分配令牌,这个工作很好,但是我希望 cookie 随着浏览器会话一起过期(例如,当浏览器关闭时)。

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

92423 次浏览

Take a look at SecurityContextPersistenceFilter class. It defines how the SecurityContextHolder is populated. By default it uses HttpSessionSecurityContextRepository to store security context in http session.

I have implemented this mechanism quite easily, with custom SecurityContextRepository.

See the securityContext.xml below:

<?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>

It seems to be even easier in Spring Securitiy 3.0. If you're using namespace configuration, you can simply do as follows:

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

Or you could configure the SecurityContextRepository as null, and nothing would ever get saved that way as well.

Actually create-session="never" doesn't mean being completely stateless. There's an issue for that in Spring Security issue management.

Just a quick note: it's "create-session" rather than "create-sessions"

create-session

Controls the eagerness with which an HTTP session is created.

If not set, defaults to "ifRequired". Other options are "always" and "never".

The setting of this attribute affect the allowSessionCreation and forceEagerSessionCreation properties of HttpSessionContextIntegrationFilter. allowSessionCreation will always be true unless this attribute is set to "never". forceEagerSessionCreation is "false" unless it is set to "always".

So the default configuration allows session creation but does not force it. The exception is if concurrent session control is enabled, when forceEagerSessionCreation will be set to true, regardless of what the setting is here. Using "never" would then cause an exception during the initialization of HttpSessionContextIntegrationFilter.

For specific details of the session usage, there is some good documentation in the HttpSessionSecurityContextRepository javadoc.

EDIT: As of Spring Security 3.1, there is a STATELESS option that can be used instead of all this. See the other answers. Original answer kept below for posterity.

After struggling with the numerous solutions posted in this answer, to try to get something working when using the <http> namespace config, I finally found an approach that actually works for my use case. I don't actually require that Spring Security doesn't start a session (because I use session in other parts of the application), just that it doesn't "remember" authentication in the session at all (it should be re-checked every request).

To begin with, I wasn't able to figure out how to do the "null implementation" technique described above. It wasn't clear whether you are supposed to set the securityContextRepository to null or to a no-op implementation. The former does not work because a NullPointerException gets thrown within SecurityContextPersistenceFilter.doFilter(). As for the no-op implementation, I tried implementing in the simplest way I could imagine:

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


}

This doesn't work in my application, because of some strange ClassCastException having to do with the response_ type.

Even assuming I did manage to find an implementation that works (by simply not storing the context in session), there is still the problem of how to inject that into the filters built by the <http> configuration. You cannot simply replace the filter at the SECURITY_CONTEXT_FILTER position, as per the docs. The only way I found to hook into the SecurityContextPersistenceFilter that is created under the covers was to write an ugly 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());
}
}
}


}
}
}
}

Anyway, to the solution that actually does work, albeit very hackish. Simply use a Filter that deletes the session entry that the HttpSessionSecurityContextRepository looks for when it does its thing:

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

Then in the configuration:

<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>

We worked on the same issue (injecting a custom SecurityContextRepository to SecurityContextPersistenceFilter) for 4-5 hours today. Finally, we figured it out. First of all, in the section 8.3 of Spring Security ref. doc, there is a SecurityContextPersistenceFilter bean definition

<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>

And after this definition, there is this explanation: "Alternatively you could provide a null implementation of the SecurityContextRepository interface, which will prevent the security context from being stored, even if a session has already been created during the request."

We needed to inject our custom SecurityContextRepository into the SecurityContextPersistenceFilter. So we simply changed the bean definition above with our custom impl and put it into the security context.

When we run the application, we traced the logs and saw that SecurityContextPersistenceFilter was not using our custom impl, it was using the HttpSessionSecurityContextRepository.

After a few other things we tried, we figured out that we had to give our custom SecurityContextRepository impl with the "security-context-repository-ref" attribute of "http" namespace. If you use "http" namespace and want to inject your own SecurityContextRepository impl, try "security-context-repository-ref" attribute.

When "http" namespace is used, a seperate SecurityContextPersistenceFilter definition is ignored. As I copied above, the reference doc. does not state that.

Please correct me if I misunderstood the things.

In Spring Security 3 with Java Config, you can use HttpSecurity.sessionManagement():

@Override
protected void configure(final HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

Now ELB supports sticky sessions, I think from 2016. But also it's possible to store your sessions in Redis.