You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 15:47:35