Spring Security 6 【4-自定义登录与登出处理器】

Spring Security 6 自定义登录与登出处理器实现指南

下面我将详细介绍如何在 Spring Security 6 中实现自定义的登录和登出处理器,并提供完整的代码实现。

一、核心处理器接口

处理器类型接口描述
登录认证成功AuthenticationSuccessHandler处理用户认证成功后的逻辑
登录认证失败AuthenticationFailureHandler处理用户认证失败后的逻辑
登出成功LogoutSuccessHandler处理用户登出成功后的逻辑
登出处理器LogoutHandler执行实际登出操作

二、完整代码实现

1. 自定义登录成功处理器

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, 
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        
        // 1. 获取用户信息
        String username = authentication.getName();
        String ipAddress = request.getRemoteAddr();
        String userAgent = request.getHeader("User-Agent");
        
        // 2. 记录登录成功事件
        logLoginEvent(username, ipAddress, userAgent);
        
        // 3. 检查是否需要MFA验证
        if (requiresMfa(authentication)) {
            response.sendRedirect("/mfa-verify");
            return;
        }
        
        // 4. 检查是否有重定向URL
        String redirectUrl = determineTargetUrl(authentication);
        
        // 5. 根据用户角色重定向
        if (authentication.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
            response.sendRedirect("/admin/dashboard");
        } else if (redirectUrl != null) {
            response.sendRedirect(redirectUrl);
        } else {
            response.sendRedirect("/user/dashboard");
        }
    }
    
    private void logLoginEvent(String username, String ip, String userAgent) {
        // 实现登录审计日志
        System.out.printf("Login success: %s from %s using %s%n", 
                          username, ip, userAgent);
        // 实际项目中写入数据库或日志系统
    }
    
    private boolean requiresMfa(Authentication authentication) {
        // 检查用户是否启用了MFA
        if (authentication.getPrincipal() instanceof CustomUser user) {
            return user.isMfaEnabled();
        }
        return false;
    }
    
    private String determineTargetUrl(Authentication authentication) {
        // 从Session中获取原始请求URL
        HttpSession session = request.getSession(false);
        if (session != null) {
            SavedRequest savedRequest = 
                (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
            if (savedRequest != null) {
                return savedRequest.getRedirectUrl();
            }
        }
        return null;
    }
}

2. 自定义登录失败处理器

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
                                       HttpServletResponse response,
                                       AuthenticationException exception) throws IOException {
        
        // 1. 获取登录信息
        String username = request.getParameter("username");
        String ipAddress = request.getRemoteAddr();
        
        // 2. 记录登录失败事件
        logFailedLogin(username, ipAddress, exception);
        
        // 3. 根据异常类型提供特定消息
        String errorMessage = getErrorMessage(exception);
        
        // 4. 增加登录失败计数器
        incrementFailedLoginCounter(username);
        
        // 5. 重定向到登录页并显示错误
        response.sendRedirect("/login?error=" + URLEncoder.encode(errorMessage, "UTF-8"));
    }
    
    private void logFailedLogin(String username, String ip, AuthenticationException ex) {
        // 实现失败登录审计
        System.out.printf("Login failed: %s from %s - %s%n", 
                          username, ip, ex.getMessage());
    }
    
    private String getErrorMessage(AuthenticationException exception) {
        if (exception instanceof BadCredentialsException) {
            return "用户名或密码错误";
        } else if (exception instanceof DisabledException) {
            return "账户已被禁用";
        } else if (exception instanceof LockedException) {
            return "账户已被锁定";
        } else if (exception instanceof AccountExpiredException) {
            return "账户已过期";
        } else {
            return "登录失败,请重试";
        }
    }
    
    private void incrementFailedLoginCounter(String username) {
        // 实现登录失败计数逻辑
        // 在实际项目中可能触发账户锁定逻辑
    }
}

3. 自定义登出处理器

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class CustomLogoutHandler implements LogoutHandler {

    @Override
    public void logout(HttpServletRequest request, 
                      HttpServletResponse response, 
                      Authentication authentication) {
        
        if (authentication == null) return;
        
        // 1. 获取用户信息
        String username = authentication.getName();
        
        // 2. 清除用户相关会话数据
        clearUserSessionData(request);
        
        // 3. 记录登出事件
        logLogoutEvent(username);
        
        // 4. 清除安全上下文
        SecurityContextHolder.clearContext();
        
        // 5. 使当前会话无效
        request.getSession().invalidate();
    }
    
    private void clearUserSessionData(HttpServletRequest request) {
        // 清除自定义会话属性
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.removeAttribute("currentCart");
            session.removeAttribute("userPreferences");
        }
    }
    
    private void logLogoutEvent(String username) {
        // 实现登出审计日志
        System.out.println("User logged out: " + username);
    }
}

4. 自定义登出成功处理器

import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request,
                               HttpServletResponse response,
                               Authentication authentication) throws IOException {
        
        // 1. 获取登出信息
        String refererUrl = request.getHeader("Referer");
        
        // 2. 发送登出通知
        sendLogoutNotification(authentication);
        
        // 3. 重定向策略
        if (isApiRequest(request)) {
            // API请求返回JSON
            response.setContentType("application/json");
            response.getWriter().write("{\"status\":\"success\", \"message\":\"Logged out\"}");
        } else if (refererUrl != null && !refererUrl.contains("/login")) {
            // 重定向到来源页面
            response.sendRedirect(refererUrl);
        } else {
            // 默认重定向到登录页
            response.sendRedirect("/login?logout");
        }
    }
    
    private boolean isApiRequest(HttpServletRequest request) {
        return request.getRequestURI().startsWith("/api/");
    }
    
    private void sendLogoutNotification(Authentication authentication) {
        if (authentication != null && authentication.getPrincipal() instanceof CustomUser user) {
            // 发送登出通知(邮件、推送等)
            System.out.println("Sending logout notification to: " + user.getEmail());
        }
    }
}

三、安全配置集成

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomAuthenticationSuccessHandler successHandler;
    private final CustomAuthenticationFailureHandler failureHandler;
    private final CustomLogoutHandler logoutHandler;
    private final CustomLogoutSuccessHandler logoutSuccessHandler;

    public SecurityConfig(CustomAuthenticationSuccessHandler successHandler,
                         CustomAuthenticationFailureHandler failureHandler,
                         CustomLogoutHandler logoutHandler,
                         CustomLogoutSuccessHandler logoutSuccessHandler) {
        this.successHandler = successHandler;
        this.failureHandler = failureHandler;
        this.logoutHandler = logoutHandler;
        this.logoutSuccessHandler = logoutSuccessHandler;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**", "/login", "/error").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .successHandler(successHandler)  // 自定义成功处理器
                .failureHandler(failureHandler)  // 自定义失败处理器
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .addLogoutHandler(logoutHandler)  // 自定义登出处理器
                .logoutSuccessHandler(logoutSuccessHandler)  // 自定义登出成功处理器
                .deleteCookies("JSESSIONID", "remember-me")
                .invalidateHttpSession(true)
                .permitAll()
            )
            .addFilterBefore(new CaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

四、高级功能扩展

1. 验证码过滤器(集成到登录流程)

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;

public class CaptchaFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain) 
        throws ServletException, IOException {
        
        if ("/login".equals(request.getServletPath()) && "POST".equals(request.getMethod())) {
            // 验证验证码
            String captcha = request.getParameter("captcha");
            String sessionCaptcha = (String) request.getSession().getAttribute("captcha");
            
            if (sessionCaptcha == null || !sessionCaptcha.equalsIgnoreCase(captcha)) {
                // 验证失败,提前中断流程
                response.sendRedirect("/login?error=invalid_captcha");
                return;
            }
        }
        
        filterChain.doFilter(request, response);
    }
}

2. 多因素认证处理

@Component
public class MfaAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, 
                                       HttpServletResponse response,
                                       Authentication authentication) throws IOException {
        
        CustomUser user = (CustomUser) authentication.getPrincipal();
        
        if (user.isMfaEnabled()) {
            // 生成并发送验证码
            String mfaCode = generateMfaCode(user);
            sendMfaCode(user, mfaCode);
            
            // 存储临时认证
            MfaAuthenticationToken token = new MfaAuthenticationToken(
                user.getUsername(), 
                null, 
                user.getAuthorities()
            );
            
            SecurityContextHolder.getContext().setAuthentication(token);
            
            // 重定向到MFA验证页面
            response.sendRedirect("/mfa-verify");
        } else {
            // 继续标准流程
            new CustomAuthenticationSuccessHandler().onAuthenticationSuccess(
                request, response, authentication
            );
        }
    }
    
    private String generateMfaCode(CustomUser user) {
        // 生成6位随机数字
        return String.format("%06d", new Random().nextInt(999999));
    }
    
    private void sendMfaCode(CustomUser user, String code) {
        // 发送验证码到用户设备
        System.out.println("Sending MFA code to " + user.getPhone() + ": " + code);
        // 实际项目中集成短信或邮件服务
    }
}

// 自定义MFA认证令牌
public class MfaAuthenticationToken extends AbstractAuthenticationToken {
    
    private final String principal;
    
    public MfaAuthenticationToken(String principal, 
                                 Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }
    
    @Override
    public Object getCredentials() {
        return null;
    }
    
    @Override
    public Object getPrincipal() {
        return principal;
    }
}

3. 社交登录集成处理器

@Component
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final JwtTokenService tokenService;
    private final ObjectMapper objectMapper;
    
    public OAuth2AuthenticationSuccessHandler(JwtTokenService ts, ObjectMapper om) {
        this.tokenService = ts;
        this.objectMapper = om;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                       HttpServletResponse response,
                                       Authentication authentication) throws IOException {
        
        // 1. 生成JWT令牌
        String jwt = tokenService.generateToken(authentication);
        
        // 2. 检查是否是新用户
        boolean isNewUser = isNewUser(authentication);
        
        // 3. 构建响应
        Map<String, Object> responseBody = new HashMap<>();
        responseBody.put("token", jwt);
        responseBody.put("isNewUser", isNewUser);
        
        // 4. 设置响应
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(responseBody));
        response.getWriter().flush();
    }
    
    private boolean isNewUser(Authentication authentication) {
        if (authentication.getPrincipal() instanceof OidcUser oidcUser) {
            // 检查用户是否首次登录
            return oidcUser.getAttribute("newUser") != null;
        }
        return false;
    }
}

五、处理器的最佳实践

1. 安全增强措施

// 在登录成功处理器中添加安全头
@Override
public void onAuthenticationSuccess(...) {
    // 设置安全相关HTTP头
    response.setHeader("Content-Security-Policy", "default-src 'self'");
    response.setHeader("X-Content-Type-Options", "nosniff");
    response.setHeader("X-Frame-Options", "DENY");
    response.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
    
    // 其他逻辑...
}

2. 登录限流保护

// 在登录失败处理器中实现限流
private void incrementFailedLoginCounter(String username) {
    // 使用Redis实现登录失败计数器
    String key = "login_fail:" + username;
    long failures = redisTemplate.opsForValue().increment(key);
    redisTemplate.expire(key, 1, TimeUnit.HOURS);
    
    if (failures >= 5) {
        // 锁定账户1小时
        lockAccount(username);
        redisTemplate.delete(key);
    }
}

private void lockAccount(String username) {
    // 实际项目中更新数据库状态
    System.out.println("Locking account: " + username);
}

3. 会话管理增强

// 在登出处理器中实现会话清理
private void clearUserSessionData(HttpServletRequest request) {
    HttpSession session = request.getSession(false);
    if (session != null) {
        // 清除所有会话属性
        Enumeration<String> attrNames = session.getAttributeNames();
        while (attrNames.hasMoreElements()) {
            String attrName = attrNames.nextElement();
            session.removeAttribute(attrName);
        }
        
        // 添加会话销毁监听器
        session.setAttribute("LOGGED_OUT", true);
    }
}

// 会话监听器
@Component
public class SessionLogoutListener implements HttpSessionListener {
    
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        if (se.getSession().getAttribute("LOGGED_OUT") == null) {
            // 会话超时而非主动登出
            logSessionTimeout(se.getSession());
        }
    }
    
    private void logSessionTimeout(HttpSession session) {
        // 记录会话超时事件
        System.out.println("Session timed out: " + session.getId());
    }
}

六、完整登录流程时序图

UserBrowserCaptchaFilterAuthManagerSuccessHandlerFailureHandler提交登录表单POST /login验证码检查传递认证请求执行认证逻辑调用onSuccess记录日志重定向到MFA页面重定向到仪表盘alt[需要MFA]调用onFailure记录失败日志重定向到登录页alt[认证成功][认证失败]重定向到登录页(带错误)alt[验证码有效][验证码无效]UserBrowserCaptchaFilterAuthManagerSuccessHandlerFailureHandler

七、总结与最佳实践

1. 处理器选择建议

场景推荐处理器说明
标准Web应用SimpleUrlAuthenticationSuccessHandler简单重定向
RESTful APISavedRequestAwareAuthenticationSuccessHandler重定向到原始请求
前后端分离AuthenticationSuccessHandler 自定义JSON响应返回JSON数据
多租户系统自定义处理器根据租户重定向不同租户不同入口

2. 安全最佳实践

  1. 登录保护

    • 集成验证码防止暴力破解
    • 实现登录失败限流
    • 强制使用HTTPS传输凭证
  2. 会话管理

    • 登出时清除所有会话数据
    • 设置合理的会话超时
    • 实现会话固定保护
  3. 审计与监控

    • 记录所有关键安全事件
    • 监控异常登录模式
    • 实现实时告警机制
  4. 错误处理

    • 避免泄露敏感信息
    • 提供用户友好的错误消息
    • 记录详细的错误日志

3. 性能优化

// 异步处理非关键操作
@Async
public void logLoginEventAsync(String username, String ip, String userAgent) {
    // 实现异步日志记录
    loginLogService.logEvent(username, ip, userAgent);
}

// 在成功处理器中使用
@Override
public void onAuthenticationSuccess(...) {
    // 同步操作
    checkMfa();
    
    // 异步记录日志
    asyncExecutor.execute(() -> 
        loginLogService.logEvent(username, ip, userAgent)
    );
    
    // 重定向...
}

通过实现自定义的登录和登出处理器,您可以完全控制 Spring Security 的认证流程,根据业务需求添加各种增强功能,同时保持系统的安全性和用户体验。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值