Concurrent Session与Remember-me问题:非记住登录登出勿删持久化令牌
问题分析与解决方案
这个问题的核心在于默认的PersistentTokenBasedRememberMeServices在登出时会删除该用户所有的持久化令牌,不管当前登出的会话是否使用了「记住我」功能。当你在地点2以普通方式(未勾选Remember-me)登录并登出时,触发了logout方法里的tokenRepository.removeUserTokens(username)逻辑,直接清空了该用户的所有令牌,包括地点1生成的有效令牌。
要解决这个问题,我们需要自定义PersistentTokenBasedRememberMeServices的子类,重写logout方法,让它只删除当前会话对应的「记住我」令牌(如果存在的话),而非全部删除。
具体实现步骤
1. 自定义RememberMeServices子类
创建一个继承自PersistentTokenBasedRememberMeServices的类,修改登出时的令牌删除逻辑:
public class CustomPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices { private final PersistentTokenRepository tokenRepository; public CustomPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) { super(key, userDetailsService, tokenRepository); this.tokenRepository = tokenRepository; } @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { // 仅当当前认证是通过「记住我」方式触发时,才删除对应的令牌 if (authentication instanceof RememberMeAuthenticationToken) { String rememberMeCookie = extractRememberMeCookie(request); if (rememberMeCookie != null) { String[] cookieTokens = decodeCookie(rememberMeCookie); // 解析出令牌的系列号(series),只删除该系列号对应的令牌 if (cookieTokens.length == 2) { String series = cookieTokens[0]; tokenRepository.removeToken(series); } } } // 调用父类逻辑清除cookie等其他登出操作 super.logout(request, response, authentication); } }
2. 配置Spring Security使用自定义类
在你的Spring Security配置类中,替换默认的RememberMeServices为我们自定义的实现:
@Configuration @EnableWebSecurity public class SecurityConfig { private final UserDetailsService userDetailsService; private final PersistentTokenRepository persistentTokenRepository; // 通过构造注入依赖(推荐) public SecurityConfig(UserDetailsService userDetailsService, PersistentTokenRepository persistentTokenRepository) { this.userDetailsService = userDetailsService; this.persistentTokenRepository = persistentTokenRepository; } @Bean public RememberMeServices customRememberMeServices() { CustomPersistentTokenBasedRememberMeServices rememberMeServices = new CustomPersistentTokenBasedRememberMeServices( "your-remember-me-secret-key", // 建议从配置文件读取,比如@Value("${remember-me.key}") userDetailsService, persistentTokenRepository ); // 根据需求配置cookie属性 rememberMeServices.setCookieName("remember-me"); rememberMeServices.setTokenValiditySeconds(7 * 24 * 3600); // 7天有效期 return rememberMeServices; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .formLogin(form -> form.permitAll()) .logout(logout -> logout .permitAll() .rememberMeServices(customRememberMeServices()) ) .rememberMe(rememberMe -> rememberMe .rememberMeServices(customRememberMeServices()) ); return http.build(); } }
方案效果说明
- 当用户在地点1勾选「记住我」登录后,数据库会生成对应的持久化令牌;
- 用户在地点2以普通方式(未勾选「记住我」)登录并登出时,由于当前认证不是
RememberMeAuthenticationToken,不会触发令牌删除逻辑,地点1的令牌会保留; - 如果用户在地点2也勾选「记住我」登录,会生成新的独立令牌,登出时只会删除该会话对应的令牌,不影响地点1的令牌;
- 当用户在地点1主动登出时,会删除地点1对应的令牌,不影响其他地点的令牌(如果有的话)。
内容的提问来源于stack exchange,提问作者JustFig
相关产品推荐
相关产品推荐

