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());
}
}
六、完整登录流程时序图
七、总结与最佳实践
1. 处理器选择建议
| 场景 | 推荐处理器 | 说明 |
|---|---|---|
| 标准Web应用 | SimpleUrlAuthenticationSuccessHandler | 简单重定向 |
| RESTful API | SavedRequestAwareAuthenticationSuccessHandler | 重定向到原始请求 |
| 前后端分离 | AuthenticationSuccessHandler 自定义JSON响应 | 返回JSON数据 |
| 多租户系统 | 自定义处理器根据租户重定向 | 不同租户不同入口 |
2. 安全最佳实践
-
登录保护:
- 集成验证码防止暴力破解
- 实现登录失败限流
- 强制使用HTTPS传输凭证
-
会话管理:
- 登出时清除所有会话数据
- 设置合理的会话超时
- 实现会话固定保护
-
审计与监控:
- 记录所有关键安全事件
- 监控异常登录模式
- 实现实时告警机制
-
错误处理:
- 避免泄露敏感信息
- 提供用户友好的错误消息
- 记录详细的错误日志
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 的认证流程,根据业务需求添加各种增强功能,同时保持系统的安全性和用户体验。
6087

被折叠的 条评论
为什么被折叠?



