Spring Security中密码变更/过期时如何强制用户重新认证并关闭会话
解决方案:Spring Security Basic Auth 实现密码过期强制重认证
针对你的问题,核心矛盾在于Basic Auth默认会将认证信息缓存到Session中,导致密码变更/过期后,高频请求用户仍能通过缓存的Session继续访问。以下是具体实现方案:
1. 完善认证阶段的密码过期检查
首先在CustomAuthenticationProvider中补充密码过期验证逻辑,同时替换Spring Security标准异常(便于框架自动处理返回401状态码):
@Override public Authentication authenticate(Authentication authentication) { String userName = authentication.getName(); User user = userService.getUserByName(userName); // 检查密码是否过期 if (user.getPasswordExpiryDate().isBefore(LocalDateTime.now())) { log.error("Password expired for user {}", userName); throw new AccountExpiredException("Password has expired, please reset"); } String encryptedPassword = DigestUtils.encodePassword(authentication.getCredentials().toString(), user.getSalt()); String encryptedPasswordFromDb = user.getPassword(); if (encryptedPassword.equals(encryptedPasswordFromDb)) { // 存入密码最后修改时间,用于后续请求校验 Map<String, Object> authDetails = new HashMap<>(); authDetails.put("passwordLastModifiedAt", user.getPasswordLastModifiedAt()); UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userName, encryptedPassword, List.of(new SimpleGrantedAuthority(String.format("ROLE_%s", user.getRoleName())))); authToken.setDetails(authDetails); return authToken; } log.error("Incorrect password for user {}", userName); throw new BadCredentialsException(String.format("Incorrect password for user %s", userName)); }
2. 密码变更时强制失效所有活跃会话
通过SessionRegistry管理用户会话,在密码修改后立即失效该用户的所有活跃Session:
第一步:配置SessionRegistry
在SecurityConfig中添加Session管理相关Bean:
@Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } @Bean public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/actuator/**").permitAll() .antMatchers("/app/user", "/app/user/password/refresh").hasRole("ADMIN") .antMatchers("/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() .and().httpBasic() // 配置Session管理,绑定SessionRegistry .and().sessionManagement() .maximumSessions(1) // 可选:限制单用户同时登录数 .sessionRegistry(sessionRegistry()); }
第二步:密码变更时失效会话
在UserService的密码更新方法中,添加会话失效逻辑:
@Service public class UserService { private final SessionRegistry sessionRegistry; private final UserRepository userRepository; public UserService(SessionRegistry sessionRegistry, UserRepository userRepository) { this.sessionRegistry = sessionRegistry; this.userRepository = userRepository; } public void updateUserPassword(String userName, String newPassword) { User user = getUserByName(userName); // 更新密码及相关时间戳 String newEncryptedPwd = DigestUtils.encodePassword(newPassword, user.getSalt()); user.setPassword(newEncryptedPwd); user.setPasswordLastModifiedAt(LocalDateTime.now()); user.setPasswordExpiryDate(LocalDateTime.now().plusDays(90)); // 示例:90天后过期 userRepository.save(user); // 失效该用户的所有活跃会话 for (Object principal : sessionRegistry.getAllPrincipals()) { if (principal instanceof String && principal.equals(userName)) { sessionRegistry.getAllSessions(principal, false) .forEach(SessionInformation::expireNow); } } } }
3. 请求级实时校验密码状态
添加自定义过滤器,在每次请求时校验用户密码是否过期/已变更,确保即时生效:
@Component public class PasswordStatusCheckFilter extends OncePerRequestFilter { private final UserService userService; public PasswordStatusCheckFilter(UserService userService) { this.userService = userService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) { String userName = auth.getName(); User user = userService.getUserByName(userName); // 校验密码是否已变更 Map<String, Object> authDetails = (Map<String, Object>) auth.getDetails(); LocalDateTime storedModifyTime = (LocalDateTime) authDetails.get("passwordLastModifiedAt"); if (!user.getPasswordLastModifiedAt().isEqual(storedModifyTime)) { invalidateSessionAndReturn401(request, response); return; } // 校验密码是否过期 if (user.getPasswordExpiryDate().isBefore(LocalDateTime.now())) { invalidateSessionAndReturn401(request, response); response.getWriter().write("Password expired, please reset"); return; } } filterChain.doFilter(request, response); } private void invalidateSessionAndReturn401(HttpServletRequest request, HttpServletResponse response) throws IOException { SecurityContextHolder.clearContext(); HttpSession session = request.getSession(false); if (session != null) session.invalidate(); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setHeader("WWW-Authenticate", "Basic realm=\"YourAppRealm\""); } }
然后在SecurityConfig中把过滤器加入Spring Security链:
@Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(passwordStatusCheckFilter(), BasicAuthenticationFilter.class) // 其余原有配置... }
可选方案:无状态模式(适合低请求量场景)
如果可以接受每次请求都重新认证的性能开销,可直接禁用Session,让每次请求都走CustomAuthenticationProvider校验:
@Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() // 原有授权配置... .and().httpBasic() .and().sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 禁用Session }
这种方式下,密码变更/过期后下一次请求就会被拒绝,但会增加数据库查询频次。
内容的提问来源于stack exchange,提问作者SorryForAsking
相关产品推荐
相关产品推荐

