Spring Security会话并发性



我有一个使用Spring 4.0和Security 3.2构建的应用程序,我想实现会话并发性,但它似乎不起作用。安全的所有其他方面都工作得很好。以下是我的xml配置:
首先在my web.xml:

<listener>
    <listener-class>
        org.springframework.security.web.session.HttpSessionEventPublisher
    </listener-class>
</listener> 

then in my security.xml

<security:http  auto-config="false" 
                use-expressions="true"
                authentication-manager-ref="authManager"
                access-decision-manager-ref="webAccessDecisionManager"
                entry-point-ref="authenticationEntryPoint">             
    <security:intercept-url pattern="/agent/**" access="hasAnyRole('ROLE_AGENT')" />
    <security:intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />       
    <security:intercept-url pattern="/public/**" access="permitAll" />
    <security:intercept-url pattern="/**" access="permitAll" />
    <security:session-management session-authentication-strategy-ref="sas"
                                 invalid-session-url="/public/login.xhtml"/>
    <security:logout logout-success-url="/public/login.xhtml" 
                     invalidate-session="true" 
                     delete-cookies="true"/>
    <security:expression-handler ref="webExpressionHandler"/>
    <security:custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
    <security:custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
</security:http>

<bean id="authenticationEntryPoint"  class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <constructor-arg index="0" value="/public/login.xhtml" />
</bean>
<bean id="customAuthenticationFailureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"
   p:defaultFailureUrl="/public/login.xhtml" />
<bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl"/>
 <bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter">
    <constructor-arg index="0" ref="sessionRegistry"/>
    <constructor-arg index="1" value="/session-expired.htm"/>       
</bean>
<bean id="myAuthFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
    <property name="sessionAuthenticationStrategy" ref="sas" />
    <property name="authenticationManager" ref="authManager" />
    <property name="authenticationFailureHandler" ref="customAuthenticationFailureHandler"/>
</bean>
<bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
    <constructor-arg name="sessionRegistry" ref="sessionRegistry" />
    <property name="maximumSessions" value="1" />
    <property name="exceptionIfMaximumExceeded" value="true" />
</bean>  
<bean id="authManager" class="org.springframework.security.authentication.ProviderManager">
    <property name="providers">
        <list>  
            <ref bean="myCompLdapAuthProvider"/>        
            <ref bean="myCompDBAuthProvider"/>
        </list>     
    </property>     
</bean>

My UserDetails实现了hashCode()和equals(),所有这些并发会话限制都不起作用。经过一个小的调试会话,我已经观察到,我的会话从来没有在sessionRegistry中找到,我想这是主要原因,但我不知道为什么!
知道我哪里做错了吗?

注:在我的调试日志中有这样的记录:

(FilterChainProxy.java:337) - /resources/images/icons/connection_on.gif at position 2 of 11 in     additional filter chain; firing Filter: 'ConcurrentSessionFilter'
(FilterChainProxy.java:337) - /resources/images/icons/connection_on.gif at position 3 of 11 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
(FilterChainProxy.java:337) - /resources/images/icons/connection_on.gif at position 4 of 11 in additional filter chain; firing Filter: 'LogoutFilter'
(FilterChainProxy.java:337) - /resources/images/icons/connection_on.gif at position 5 of 11 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
(FilterChainProxy.java:337) - /resources/images/icons/connection_on.gif at position 6 of 11 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
(FilterChainProxy.java:337) - /resources/images/icons/connection_on.gif at position 7 of 11 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
(FilterChainProxy.java:337) - /resources/images/icons/connection_on.gif at position 8 of 11 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
(AnonymousAuthenticationFilter.java:107) - SecurityContextHolder not populated with anonymous token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken@96cf68e: Principal: MyUserDetails [username=adrian.videanu, dn=org.springframework.ldap.core.DirContextAdapter: dn=cn=Adrian Videanu,ou=IT,ou=Organization .....

所以过滤器被调用…

更新

我可以看到会话创建事件被发布,因为我在日志中有这一行:

(HttpSessionEventPublisher.java:66) - Publishing event: org.springframework.security.web.session.HttpSessionCreatedEvent[source=org.apache.catalina.session.StandardSessionFacade@3827a0aa]  

但我从来没有从SessionRegistryImpl命中registerNewSession方法,因为我想它应该。当我最初打开登录页面时,也会调用httpessioneventpublisher,因为我猜这是会话创建的时候,但是在我输入凭据并推送提交httpessioneventpublisher之后,就不再调用了。

更新2
作为测试,我将SessionRegistryImpl注入到我的一个bean中,以便尝试访问它的一些方法:

@Named
@Scope("view")
public class UserDashboardMB  implements Serializable {
private static final long serialVersionUID = 1L;
@Inject
private SessionRegistry sessionRegistry;
public void init(){
    System.out.println("-- START INIT -- ");
    List<Object> principals = sessionRegistry.getAllPrincipals();
    System.out.println("Principals = "+principals);
    for (Object p:principals){
        System.out.println("Principal = "+p);
    }
    System.out.println("-- STOP INIT -- ");
}   
}  

,输出为:
INFO:——START INIT——
INFO: Principals = []
INFO:——STOP INIT——
所以这里没有人居住。

3
更新我已经将"sas"bean替换为Serge提供的bean,但它似乎仍然不起作用。我再次启用了调试器,我认为问题是,在类UsernamePasswordAuthenticationFilter方法doFilter()上,我的请求都没有得到应有的处理。下面是doFilter()的一部分:

 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
        return;
    }
    if (logger.isDebugEnabled()) {
        logger.debug("Request is to process authentication");
    }
    Authentication authResult;
// rest of method here
}

从我在调试器中看到的,我的请求似乎不需要验证和链。doFilter(请求、响应);被调用。

更新4
我想我找到问题了。过滤器没有正常运行,因为filterUrl参数不是正确的。正如我在文档中读到的:

该过滤器默认响应URL/j_spring_security_check。

,但是我的登录部分是用JSF托管bean和操作实现的。现在,我的登录表单是/public/login.xhtml,发布登录信息的url是相同的。如果我将其设置为filterUrl,我就会遇到问题,因为它在初始表单渲染时也会被调用,并且由于没有设置用户/密码,我有一个无限循环。
有办法克服吗?
这是我的LoginManagedBean的样子:

@Named
@Scope("request")
public class LoginMB implements Serializable {
private static final long serialVersionUID = 1L;
@Autowired
@Qualifier("authManager")
private AuthenticationManager authenticationManager;
// setters and getters
public String login(){
    FacesContext context = FacesContext.getCurrentInstance();
    try {
        Authentication request = new UsernamePasswordAuthenticationToken(this.getUsername(), this.getPassword());
        Authentication result = authenticationManager.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);
        // perform some extra logic here and return protected page
        return "/agent/dashboard.xhtml?faces-redirect=true";
    } catch (AuthenticationException e) {
        e.printStackTrace();                        
        logger.error("Auth Exception ->"+e.getMessage());
        FacesMessage fm = new FacesMessage("Invalid user/password");
        fm.setSeverity(FacesMessage.SEVERITY_ERROR);
        context.addMessage(null, fm);                   
    }
    return null;
   }
}

spring security 3.1和spring security 3.2在并发会话管理方面略有不同。

旧的ConcurrentSessionControlStrategy现在已弃用。它检查是否超过了并发会话的数量,并在SessionRegistry中注册会话以供将来使用。

部分在3.2中被ConcurrentSessionControlAuthenticationStrategy取代。它有效地控制并发会话的数量是否超过了,但不再注册新的会话(即使javadoc假装:我查看了源代码来理解它!)

会话的注册现在委托给RegisterSessionAuthenticationStrategy !因此,要使会话并发工作,必须同时使用这两种方法。而3.2参考手册中的例子,有效地为bean sas使用了包含ConcurrentSessionControlAuthenticationStrategySessionFixationProtectionStrategy RegisterSessionAuthenticationStrategy CompositeSessionAuthenticationStrategy !

要使整个事情正常工作,您只需将sas bean替换为:

<bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
    <constructor-arg>
        <list>
            <bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
                <constructor-arg ref="sessionRegistry"/>
                <property name="maximumSessions" value="1" />
                <property name="exceptionIfMaximumExceeded" value="true" />
            </bean>
            <bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
            </bean>
            <bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
                <constructor-arg ref="sessionRegistry"/>
            </bean>
        </list>
    </constructor-arg>
</bean>

我终于设法解决了这个问题。问题是由于我在spring标准过滤器和自定义jsf登录表单之间的混合设置。我在我的xml配置中只留下了Serge指出的"sas"bean,在我的LoginMB中,我手动和编程地调用了SessionAuthenticationStrategy onAuthentication()方法。现在我的LoginMB看起来像:

@Named
@Scope("request")
public class LoginMB implements Serializable {
@Autowired
@Qualifier("authManager")
private AuthenticationManager authenticationManager;
@Inject
@Qualifier("sas")
private SessionAuthenticationStrategy sessionAuthenticationStrategy;

public String login(){
    FacesContext context = FacesContext.getCurrentInstance();
    try {
        Authentication authRequest = new UsernamePasswordAuthenticationToken(this.getUsername(), this.getPassword());
        Authentication result = authenticationManager.authenticate(authRequest);
        SecurityContextHolder.getContext().setAuthentication(result);
        HttpServletRequest httpReq = (HttpServletRequest)FacesContext.getCurrentInstance().getExternalContext().getRequest();
        HttpServletResponse httpResp = (HttpServletResponse)FacesContext.getCurrentInstance().getExternalContext().getResponse();
        sessionAuthenticationStrategy.onAuthentication(result, httpReq, httpResp);
        // custom logic here 
        return "/agent/dashboard.xhtml?faces-redirect=true";
    }catch(SessionAuthenticationException sae){
        sae.printStackTrace();
        logger.error("Auth Exception ->"+sae.getMessage());
        String userMessage = "Session auth exception!";
        if (sae.getMessage().compareTo("Maximum sessions of 1 for this principal exceeded") == 0){
            userMessage = "Cannot login from more than 1 location.";
        }
        FacesMessage fm = new FacesMessage(userMessage);
        fm.setSeverity(FacesMessage.SEVERITY_FATAL);
        context.addMessage(null, fm);
    }       
    catch (AuthenticationException e) {
        e.printStackTrace();                        
        logger.error("Auth Exception ->"+e.getMessage());
        FacesMessage fm = new FacesMessage("Invalid user/password");
        fm.setSeverity(FacesMessage.SEVERITY_FATAL);
        context.addMessage(null, fm);                   
    }
    return null;
}

会话已注册,会话限制生效

最新更新