SpringSecurity3.1.2控制一个账户同时只能登录一次 - aokunsang - ITeye博客

网上看了很多资料,发现多多少少都有一些不足(至少我在使用的时候没成功),后来经过探索研究,得到解决方案。

具体SpringSecurity3怎么配置参考SpringSecurity3.1实践,这里只讲如何配置可以控制一个账户同时只能登录一次的配置实现。

网上很多配置是这样的,在<http>标签中加入concurrency-control配置,设置max-sessions=1。

Xml代码  收藏代码 "收藏这段代码")

  1. <session-managementinvalid-session-url="/timeout">
  2.   <concurrency-controlmax-sessions="1"error-if-maximum-exceeded="true"/>
  3. </session-management>

<session-management invalid-session-url="/timeout">
<concurrency-control max-sessions="1" error-if-maximum-exceeded="true"/>
</session-management>

 但是经测试一直没成功,经过一番查询原来是UsernamePasswordAuthenticationFilter中的一个默认属性sessionStrategy导致的。

首先我们在spring-security.xml中添加<debug/>,可以看到经过的过滤器如下:

Java代码  收藏代码 "收藏这段代码")

  1. Security filter chain: [  
  2.   ConcurrentSessionFilter --- (主要)并发控制过滤器,当配置<concurrency-control />时,会自动注册  
  3.   SecurityContextPersistenceFilter  --- 主要是持久化SecurityContext实例,也就是SpringSecurity的上下文。也就是SecurityContextHolder.getContext()这个东西,可以得到Authentication。  
  4.   LogoutFilter   ---  注销过滤器  
  5.   PmcUsernamePasswordAuthenticationFilter  --- (主要)登录过滤器,也就是配置文件中的<form-login/>配置、(本文主要问题就在这里)  
  6.   RequestCacheAwareFilter                 ---- 主要作用为:用户登录成功后,恢复被打断的请求(这些请求是保存在Cache中的)。这里被打断的请求只有出现AuthenticationException、AccessDeniedException两类异常时的请求。  
  7.   SecurityContextHolderAwareRequestFilter  ---- 不太了解  
  8.   AnonymousAuthenticationFilter        ------  匿名登录过滤器  
  9.   SessionManagementFilter              ----  session管理过滤器  
  10.   ExceptionTranslationFilter         ---- 异常处理过滤器(该过滤器只过滤下面俩拦截器)[处理的异常都是继承RuntimeException,并且它只处理AuthenticationException(认证异常)和AccessDeniedException(访问拒绝异常)]  
  11.   PmcFilterSecurityInterceptor       ------  自定义拦截器  
  12.   FilterSecurityInterceptor  
  13. ]  

Security filter chain: [
ConcurrentSessionFilter --- (主要)并发控制过滤器,当配置<concurrency-control />时,会自动注册
SecurityContextPersistenceFilter --- 主要是持久化SecurityContext实例,也就是SpringSecurity的上下文。也就是SecurityContextHolder.getContext()这个东西,可以得到Authentication。
LogoutFilter --- 注销过滤器
PmcUsernamePasswordAuthenticationFilter --- (主要)登录过滤器,也就是配置文件中的<form-login/>配置、(本文主要问题就在这里)
RequestCacheAwareFilter ---- 主要作用为:用户登录成功后,恢复被打断的请求(这些请求是保存在Cache中的)。这里被打断的请求只有出现AuthenticationException、AccessDeniedException两类异常时的请求。
SecurityContextHolderAwareRequestFilter ---- 不太了解
AnonymousAuthenticationFilter ------ 匿名登录过滤器
SessionManagementFilter ---- session管理过滤器
ExceptionTranslationFilter ---- 异常处理过滤器(该过滤器只过滤下面俩拦截器)[处理的异常都是继承RuntimeException,并且它只处理AuthenticationException(认证异常)和AccessDeniedException(访问拒绝异常)]
PmcFilterSecurityInterceptor ------ 自定义拦截器
FilterSecurityInterceptor
]

 那么先来看UsernamePasswordAuthenticationFilter,它实际是执行其父类AbstractAuthenticationProcessingFilter的doFilter()方法,看部分源码:

Java代码  收藏代码 "收藏这段代码")

  1. try {  
  2.         //子类继承该方法进行认证后返回Authentication
  3.             authResult = attemptAuthentication(request, response);  
  4.             if (authResult == null) {  
  5.                 // return immediately as subclass has indicated that it hasn't completed authentication
  6.                 return;  
  7.             }  
  8.         //判断session是否过期、把当前用户放入session(这句是关键)
  9.             sessionStrategy.onAuthentication(authResult, request, response);  
  10.         } catch(InternalAuthenticationServiceException failed) {  
  11.             logger.error("An internal error occurred while trying to authenticate the user.", failed);  
  12.             unsuccessfulAuthentication(request, response, failed);  
  13.             return;  
  14.         }  

try {

    //子类继承该方法进行认证后返回Authentication
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            // return immediately as subclass has indicated that it hasn't completed authentication
            return;
        }
    //判断session是否过期、把当前用户放入session(这句是关键)
        sessionStrategy.onAuthentication(authResult, request, response);
    } catch(InternalAuthenticationServiceException failed) {
        logger.error("An internal error occurred while trying to authenticate the user.", failed);
        unsuccessfulAuthentication(request, response, failed);
        return;
    }

 看sessionStrategy的默认值是什么?

Java代码  收藏代码 "收藏这段代码")

  1. private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();  

private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();

 然后我们查询NullAuthenticatedSessionStrategy类的onAuthentication()方法竟然为空方法[问题就出现在这里]。那么为了解决这个问题,我们需要向UsernamePasswordAuthenticationFilter中注入类ConcurrentSessionControlStrategy。

这里需要说明下,注入的属性name名称为sessionAuthenticationStrategy,因为它的setter方法这么写的:

Java代码  收藏代码 "收藏这段代码")

  1. publicvoid setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {  
  2.    this.sessionStrategy = sessionStrategy;  
  3. }  

public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}

 这样,我们的配置文件需要这样配置(只贴出部分代码)[具体参考SpringSecurity3.1实践那篇博客]:

Xml代码  收藏代码 "收藏这段代码")

  1. <httpentry-point-ref="loginAuthenticationEntryPoint">
  2.     <logout
  3. delete-cookies="JSESSIONID"
  4.         logout-success-url="/"
  5.         invalidate-session="true"/>
  6. lt;access-denied-handler error-page="/common/view/accessDenied.jsp"/>
  7.     <session-managementinvalid-session-url="/timeout"session-authentication-strategy-ref="sas"/>
  8.     <custom-filterref="pmcLoginFilter"position="FORM_LOGIN_FILTER"/>
  9.     <custom-filterref="concurrencyFilter"position="CONCURRENT_SESSION_FILTER"/>
  10.     <custom-filterref="pmcSecurityFilter"before="FILTER_SECURITY_INTERCEPTOR"/>
  11. </http>
  12. <!-- 登录过滤器(相当于<form-login/>) -->
  13. <beans:beanid="pmcLoginFilter"class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
  14.     <beans:propertyname="authenticationManager"ref="authManager"></beans:property>
  15.     <beans:propertyname="authenticationFailureHandler"ref="failureHandler"></beans:property>
  16.     <beans:propertyname="authenticationSuccessHandler"ref="successHandler"></beans:property>
  17.     <beans:propertyname="sessionAuthenticationStrategy"ref="sas"></beans:property>
  18. </beans:bean>
  19. <!-- ConcurrentSessionFilter过滤器配置(主要设置账户session过期路径) -->
  20. <beans:beanid="concurrencyFilter"class="org.springframework.security.web.session.ConcurrentSessionFilter">
  21.     <beans:propertyname="expiredUrl"value="/timeout"></beans:property>
  22.     <beans:propertyname="sessionRegistry"ref="sessionRegistry"></beans:property>
  23. </beans:bean>
  24. t;!-- 未验证用户的登录入口 -->
  25. <beans:beanid="loginAuthenticationEntryPoint"class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
  26.     <beans:constructor-argname="loginFormUrl"value="/"></beans:constructor-arg>
  27. </beans:bean>
  28. <!-- 注入到UsernamePasswordAuthenticationFilter中,否则默认使用的是NullAuthenticatedSessionStrategy,则获取不到登录用户数  
  29. error-if-maximum-exceeded:若当前maximumSessions为1,当设置为true表示同一账户登录会抛出SessionAuthenticationException异常,异常信息为:Maximum sessions of {0} for this principal exceeded;  
  30.                                                                  当设置为false时,不会报错,则会让同一账户最先认证的session过期。  
  31.    具体参考:ConcurrentSessionControlStrategy:onAuthentication()  
  32. -->
  33. <beans:beanid="sas"class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
  34.     <beans:propertyname="maximumSessions"value="1"></beans:property>
  35.     <beans:propertyname="exceptionIfMaximumExceeded"value="true"></beans:property>
  36.     <beans:constructor-argname="sessionRegistry"ref="sessionRegistry"></beans:constructor-arg>
  37. </beans:bean>
  38. <beans:beanid="sessionRegistry"class="org.springframework.security.core.session.SessionRegistryImpl"></beans:bean>

    <http entry-point-ref="loginAuthenticationEntryPoint">

    <logout 
      delete-cookies="JSESSIONID"
        logout-success-url="/" 
        invalidate-session="true"/>
    

    <access-denied-handler error-page="/common/view/accessDenied.jsp"/>

      
        <session-management invalid-session-url="/timeout" session-authentication-strategy-ref="sas"/>
    
    <custom-filter ref="pmcLoginFilter" position="FORM\_LOGIN\_FILTER"/>
    <custom-filter ref="concurrencyFilter" position="CONCURRENT\_SESSION\_FILTER"/>
    <custom-filter ref="pmcSecurityFilter" before="FILTER\_SECURITY\_INTERCEPTOR"/>

    </http>

<!-- 登录过滤器(相当于<form-login/>) -->
<beans:bean id="pmcLoginFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">

  <beans:property name="authenticationManager" ref="authManager"></beans:property>
  <beans:property name="authenticationFailureHandler" ref="failureHandler"></beans:property>
  <beans:property name="authenticationSuccessHandler" ref="successHandler"></beans:property>
  <beans:property name="sessionAuthenticationStrategy" ref="sas"></beans:property>

</beans:bean>

<!-- ConcurrentSessionFilter过滤器配置(主要设置账户session过期路径) -->
<beans:bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter">

  <beans:property name="expiredUrl" value="/timeout"></beans:property>
  <beans:property name="sessionRegistry" ref="sessionRegistry"></beans:property>

</beans:bean>

<!-- 未验证用户的登录入口 -->
<beans:bean id="loginAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">

  <beans:constructor-arg name="loginFormUrl" value="/"></beans:constructor-arg>

</beans:bean>

<!-- 注入到UsernamePasswordAuthenticationFilter中,否则默认使用的是NullAuthenticatedSessionStrategy,则获取不到登录用户数
error-if-maximum-exceeded:若当前maximumSessions为1,当设置为true表示同一账户登录会抛出SessionAuthenticationException异常,异常信息为:Maximum sessions of {0} for this principal exceeded;

                                                               当设置为false时,不会报错,则会让同一账户最先认证的session过期。
 具体参考:ConcurrentSessionControlStrategy:onAuthentication()

-->
<beans:bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">

  <beans:property name="maximumSessions" value="1"></beans:property>
  <beans:property name="exceptionIfMaximumExceeded" value="true"></beans:property>
  <beans:constructor-arg name="sessionRegistry" ref="sessionRegistry"></beans:constructor-arg>

</beans:bean>
<beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl"></beans:bean>

 这样设置后,即可实现一个账户同时只能登录一次,但是有个小瑕疵。

如:A用户用账号admin登录系统,B用户在别处也用账号admin登录系统,这时会出现两种情况:

  1、设置exceptionIfMaximumExceeded=true,会报异常:SessionAuthenticationException("Maximum sessions of {0} for this principal exceeded");

  2、设置exceptionIfMaximumExceeded=false,那么B用户会把A用户挤掉,A用户再点击页面,则会跳转到

ConcurrentSessionFilter的expiredUrl路径。

     最理想的解决办法是第一种,但是不能让其报异常,在登录失败的handler中扑捉该异常,跳转到登录页面提示<该账号已经登录>。

     但是这里还会有点问题,当用户关闭浏览器或者直接关机等非正常退出时候将登录不进去。目前我的解决方案是:比对IP地址,判断如果是本机用户可以多次登录系统,然后使第一次登录的账号无效。大致思路关注2点:

1、把ConcurrentSessionControlStrategy源码copy一份,更改部分业务逻辑(主要拿客户端ip做文章);

2、重载SessionRegistryImpl,让其调用registerNewSession()方法时候保存ip地址。

详细的解决方案以及配置文件参考附件。


Original url: Access
Created at: 2019-09-03 21:28:06
Category: default
Tags: none

请先后发表评论
  • 最新评论
  • 总共0条评论