首页 > Spring Security开启Session并发控制后如何防止删除同用户名的remember-me固化信息?

Spring Security开启Session并发控制后如何防止删除同用户名的remember-me固化信息?

spring-security.xml文件中,开启固化remember-me信息。

<remember-me key="elim" remember-me-parameter="remember-me"
            token-validity-seconds="604800" data-source-ref="dataSource"
            user-service-ref="customjdbcUserService" />

开启session并发控制

<session-management session-fixation-protection="migrateSession">
            <concurrency-control max-sessions="1"
                expired-url="/login?error=expired" />
        </session-management>

以同一用户名分先后在两个浏览器中登录。

+----------+--------------------------+--------------------------+---------------------+
| username | series                   | token                    | last_used           |
+----------+--------------------------+--------------------------+---------------------+
| admin    | lxtSA6Sy9grRXgflaePysg== | Qv1ooPUwSh6KkZt+jQdhxA== | 2015-10-29 14:20:33 |
| admin    | qay3kIvPY5pNiliyaefoUg== | nJs1T771Hk4yhXJvhV2lHQ== | 2015-10-29 14:20:10 |
+----------+--------------------------+--------------------------+---------------------+

第一个登录的session会失效,被重定向到/login?error=expired。
关闭第二个登录的那个浏览器,再打开,发现remember-me失效

在再打开浏览器并访问应用时,debug信息如下:

15:20:48,437 DEBUG AntPathRequestMatcher:151 - Checking match of request : '/'; against '/resources/**'
15:20:48,437 DEBUG AntPathRequestMatcher:151 - Checking match of request : '/'; against '/about'
15:20:48,437 DEBUG FilterChainProxy:324 - / at position 1 of 16 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
15:20:48,438 DEBUG HttpSessionSecurityContextRepository:192 - Obtained a valid SecurityContext from SPRING_SECURITY_CONTEXT: 'org.springframework.security.core.context.SecurityContextImpl@fec30301: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@fec30301: Principal: org.springframework.security.core.userdetails.User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN,ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@43458: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: hnjhg2sd962bk1qcr41mg48p; Granted Authorities: ROLE_ADMIN, ROLE_USER'
15:20:48,438 DEBUG FilterChainProxy:324 - / at position 2 of 16 in additional filter chain; firing Filter: 'ConcurrentSessionFilter'
15:20:48,438 DEBUG SecurityContextLogoutHandler:64 - Invalidating session: 2ixaysrn5488sn73756mn6ru
15:20:48,438 DEBUG HttpSessionEventPublisher:88 - Publishing event: org.springframework.security.web.session.HttpSessionDestroyedEvent[source=org.eclipse.jetty.server.session.HashedSession:2ixaysrn5488sn73756mn6ru@1251798491]
15:20:48,439 DEBUG SessionRegistryImpl:167 - Removing session 2ixaysrn5488sn73756mn6ru from principal's set of registered sessions
15:20:48,439 DEBUG PersistentTokenBasedRememberMeServices:407 - Logout of user admin
15:20:48,439 DEBUG PersistentTokenBasedRememberMeServices:346 - Cancelling cookie
15:20:48,440 DEBUG JdbcTemplate:908 - Executing prepared SQL update
15:20:48,440 DEBUG JdbcTemplate:627 - Executing prepared SQL statement [delete from persistent_logins where username = ?]
15:20:48,440 DEBUG DataSourceUtils:110 - Fetching JDBC Connection from DataSource
15:20:48,449 DEBUG JdbcTemplate:918 - SQL update affected 2 rows
15:20:48,449 DEBUG DataSourceUtils:327 - Returning JDBC Connection to DataSource
15:20:48,450 DEBUG DefaultRedirectStrategy:39 - Redirecting to '/login?error=expired'
15:20:48,450 DEBUG HttpSessionSecurityContextRepository:337 - SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
15:20:48,450 DEBUG SecurityContextPersistenceFilter:105 - SecurityContextHolder now cleared, as request processing completed
15:20:48,455 DEBUG AntPathRequestMatcher:151 - Checking match of request : '/login'; against '/resources/**'
15:20:48,456 DEBUG AntPathRequestMatcher:151 - Checking match of request : '/login'; against '/about'
15:20:48,456 DEBUG FilterChainProxy:324 - /login?error=expired at position 1 of 16 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
15:20:48,456 DEBUG HttpSessionSecurityContextRepository:159 - No HttpSession currently exists
15:20:48,456 DEBUG HttpSessionSecurityContextRepository:101 - No SecurityContext was available from the HttpSession: null. A new one will be created.
15:20:48,457 DEBUG FilterChainProxy:324 - /login?error=expired at position 2 of 16 in additional filter chain; firing Filter: 'ConcurrentSessionFilter'
15:20:48,457 DEBUG FilterChainProxy:324 - /login?error=expired at position 3 of 16 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
15:20:48,457 DEBUG FilterChainProxy:324 - /login?error=expired at position 4 of 16 in additional filter chain; firing Filter: 'HeaderWriterFilter'
15:20:48,457 DEBUG HstsHeaderWriter:128 - Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@3d3b49e2
15:20:48,458 DEBUG FilterChainProxy:324 - /login?error=expired at position 5 of 16 in additional filter chain; firing Filter: 'CsrfFilter'
15:20:48,458 DEBUG FilterChainProxy:324 - /login?error=expired at position 6 of 16 in additional filter chain; firing Filter: 'LogoutFilter'
15:20:48,458 DEBUG AntPathRequestMatcher:131 - Request 'GET /login' doesn't match 'POST /logout
15:20:48,458 DEBUG FilterChainProxy:324 - /login?error=expired at position 7 of 16 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
15:20:48,458 DEBUG AntPathRequestMatcher:131 - Request 'GET /login' doesn't match 'POST /login
15:20:48,459 DEBUG FilterChainProxy:324 - /login?error=expired at position 8 of 16 in additional filter chain; firing Filter: 'BasicAuthenticationFilter'
15:20:48,459 DEBUG FilterChainProxy:324 - /login?error=expired at position 9 of 16 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
15:20:48,459 DEBUG FilterChainProxy:324 - /login?error=expired at position 10 of 16 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
15:20:48,459 DEBUG FilterChainProxy:324 - /login?error=expired at position 11 of 16 in additional filter chain; firing Filter: 'RememberMeAuthenticationFilter'
15:20:48,460 DEBUG FilterChainProxy:324 - /login?error=expired at position 12 of 16 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
15:20:48,460 DEBUG AnonymousAuthenticationFilter:100 - Populated SecurityContextHolder with anonymous token: 'org.springframework.security.authentication.AnonymousAuthenticationToken@9055c2bc: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS'
15:20:48,460 DEBUG FilterChainProxy:324 - /login?error=expired at position 13 of 16 in additional filter chain; firing Filter: 'SessionManagementFilter'
15:20:48,460 DEBUG SessionManagementFilter:109 - Requested session ID 2ixaysrn5488sn73756mn6ru is invalid.
15:20:48,461 DEBUG FilterChainProxy:324 - /login?error=expired at position 14 of 16 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
15:20:48,461 DEBUG FilterChainProxy:324 - /login?error=expired at position 15 of 16 in additional filter chain; firing Filter: 'IPRoleAuthenticationFilter'
15:20:48,461 DEBUG FilterChainProxy:324 - /login?error=expired at position 16 of 16 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
15:20:48,461 DEBUG AntPathRequestMatcher:151 - Checking match of request : '/login'; against '/login'
15:20:48,461 DEBUG FilterSecurityInterceptor:218 - Secure object: FilterInvocation: URL: /login?error=expired; Attributes: [permitAll]
15:20:48,462 DEBUG FilterSecurityInterceptor:347 - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055c2bc: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
15:20:48,462 DEBUG AffirmativeBased:65 - Voter: org.springframework.security.web.access.expression.WebExpressionVoter@7134b8a7, returned: 1
15:20:48,462 DEBUG FilterSecurityInterceptor:242 - Authorization successful
15:20:48,462 DEBUG FilterSecurityInterceptor:255 - RunAsManager did not change Authentication object
15:20:48,462 DEBUG FilterChainProxy:309 - /login?error=expired reached end of additional filter chain; proceeding with original chain
15:20:48,463 DEBUG DispatcherServlet:861 - DispatcherServlet with name 'springmvc' processing GET request for [/login]
15:20:48,463 DEBUG RequestMappingHandlerMapping:294 - Looking up handler method for path /login
15:20:48,465 DEBUG RequestMappingHandlerMapping:299 - Returning handler method [public java.lang.String com.bay1ts.controller.BaseController.login(java.lang.String,org.springframework.ui.Model)]
15:20:48,465 DEBUG DefaultListableBeanFactory:248 - Returning cached instance of singleton bean 'baseController'
15:20:48,465 DEBUG DispatcherServlet:947 - Last-Modified value for [/login] is: -1
15:20:48,477 DEBUG DefaultListableBeanFactory:1616 - Invoking afterPropertiesSet() on bean with name 'error_expired'
15:20:48,477 DEBUG DispatcherServlet:1241 - Rendering view [org.springframework.web.servlet.view.JstlView: name 'error_expired'; URL [/WEB-INF/jsps/error_expired.jsp]] in DispatcherServlet with name 'springmvc'
15:20:48,478 DEBUG JstlView:432 - Added model object 'numUsers' of type [java.lang.Integer] to request in view with name 'error_expired'
15:20:48,478 DEBUG JstlView:432 - Added model object 'errormsg' of type [java.lang.String] to request in view with name 'error_expired'
15:20:48,478 DEBUG DefaultListableBeanFactory:248 - Returning cached instance of singleton bean 'requestDataValueProcessor'
15:20:48,478 DEBUG JstlView:166 - Forwarding to resource [/WEB-INF/jsps/error_expired.jsp] in InternalResourceView 'error_expired'
15:20:48,607 DEBUG HttpSessionEventPublisher:71 - Publishing event: org.springframework.security.web.session.HttpSessionCreatedEvent[source=org.eclipse.jetty.server.session.HashedSession:7f42twgob6sbqhvedpdoe6lu@924237923]
15:20:48,611 DEBUG HttpSessionSecurityContextRepository:337 - SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
15:20:48,612 DEBUG DispatcherServlet:996 - Successfully completed request
15:20:48,613 DEBUG ExceptionTranslationFilter:116 - Chain processed normally
15:20:48,613 DEBUG SecurityContextPersistenceFilter:105 - SecurityContextHolder now cleared, as request processing completed

其中重点信息为:

15:20:48,439 DEBUG PersistentTokenBasedRememberMeServices:407 - Logout of user admin
15:20:48,439 DEBUG PersistentTokenBasedRememberMeServices:346 - Cancelling cookie
15:20:48,440 DEBUG JdbcTemplate:908 - Executing prepared SQL update
15:20:48,440 DEBUG JdbcTemplate:627 - Executing prepared SQL statement [delete from persistent_logins where username = ?]
15:20:48,440 DEBUG DataSourceUtils:110 - Fetching JDBC Connection from DataSource
15:20:48,449 DEBUG JdbcTemplate:918 - SQL update affected 2 rows

可以从sql语句中看到发生了什么。
现在我想解决这个问题,即顶下已登录的用户后,我的remember-me信息能够保留。
我的思路是自定义15:20:48,440 DEBUG JdbcTemplate:627 - Executing prepared SQL statement [delete from persistent_logins where username = ?]这条语句,本质上也是自定义PersistentTokenBasedRememberMeServices中的JdbcTokenRepositoryImpl,我是这样实现的

public class CustomJdbcTokenRepositoryImpl extends JdbcTokenRepositoryImpl {
    public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where last_used=(select * from (select MIN(last_used) from persistent_logins where username=?)as t) and username=?;";
    private String removeUserTokensSql = DEF_REMOVE_USER_TOKENS_SQL;
    public void removeUserTokens(String username){
        getJdbcTemplate().update(removeUserTokensSql, username,username);
    }
}

在spring-security.xml中:

<beans:bean id="tokenRepository" class="com.bay1ts.security.CustomJdbcTokenRepositoryImpl">
        <beans:property name="dataSource" ref="dataSource"/>
    </beans:bean>
    
<beans:bean class="org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices">
        <beans:property name="tokenRepository" ref="tokenRepository"/>
    </beans:bean>

但是后来报异常了:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices#1' defined in class path resource [spring-security.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices.<init>()

这是PersistentTokenBasedRememberMeServices的部分实现:

public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {

    private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
    private SecureRandom random;

    public static final int DEFAULT_SERIES_LENGTH = 16;
    public static final int DEFAULT_TOKEN_LENGTH = 16;

    private int seriesLength = DEFAULT_SERIES_LENGTH;
    private int tokenLength = DEFAULT_TOKEN_LENGTH;

    public PersistentTokenBasedRememberMeServices(String key,
            UserDetailsService userDetailsService,
            PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService);
        random = new SecureRandom();
        this.tokenRepository = tokenRepository;
    }

实在不知道该怎么写这个依赖注入的bean,求大神帮忙!谢谢!


报错也提到了No default constructor found,所以在PersistentTokenBasedRememberMeServices 中加一个无参的构造函数就可以了。
如果想让spring用

public PersistentTokenBasedRememberMeServices(String key,
            UserDetailsService userDetailsService,
            PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService);
        random = new SecureRandom();
        this.tokenRepository = tokenRepository;
    }

来构造PersistentTokenBasedRememberMeServices,你需要在在spring-security.xml中定义成下面这个样子:

<beans:bean class="org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices">
    <constructor-arg type="java.lang.String" value="Your-Key-Value"/>
    <constructor-arg ref="userDetailsService"/>
    <constructor-arg ref="tokenRepository"/>
</beans:bean>
【热门文章】
【热门文章】