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

Spring Security中如何在unsuccessfulAuthentication获取登录失败用户信息?

解决登录失败时获取尝试登录用户信息的问题

你遇到的核心问题是:认证失败时,SecurityContext中不会存储未通过验证的用户信息,所以直接从SecurityContextHolder拿Principal会返回null。这里提供两种实用的解决方法:

方法一:在认证前置步骤中存储尝试登录的用户名

修改attemptAuthentication方法,将解析到的用户名存入request属性中,后续在unsuccessfulAuthentication里直接读取该属性即可:

修改attemptAuthentication方法

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (!request.getMethod().equals(HttpMethod.POST.name())) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    UsernamePasswordAuthenticationToken authenticationToken;
    String attemptedUsername = null;

    try {
        LoginDto loginInfo = objectMapper.readValue(request.getInputStream(), LoginDto.class);
        attemptedUsername = loginInfo.getUserEmail();
        authenticationToken = new UsernamePasswordAuthenticationToken(attemptedUsername, loginInfo.getPassword());
    }
    catch (IOException e) {
        authenticationToken = new UsernamePasswordAuthenticationToken(null, null);
    }
    // 将尝试登录的用户名存入request属性
    request.setAttribute("attemptedUsername", attemptedUsername);
    return this.getAuthenticationManager().authenticate(authenticationToken);
}

修改unsuccessfulAuthentication方法

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
    // 从request属性中获取尝试登录的用户名
    String attemptedUsername = (String) request.getAttribute("attemptedUsername");
    
    // 这里可以添加日志记录或登录限制逻辑
    if (attemptedUsername != null) {
        securityService.createSecurityLog(SecurityLogAction.loginFail(attemptedUsername));
        // 示例:添加登录失败次数统计、IP限制等逻辑
    }

    SecurityContextHolder.clearContext();

    Response loginFail = Response.builder()
            .code(SecurityErrorCode.LOGIN_FAIL.getErrorCode())
            .result(SecurityErrorCode.LOGIN_FAIL.getErrorMessage())
            .build();
    response.setStatus(HttpStatus.UNAUTHORIZED.value());
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.setCharacterEncoding(StandardCharsets.UTF_8.name());
    response.getWriter().write(objectMapper.writeValueAsString(loginFail));
    response.getWriter().flush();
    response.getWriter().close();
}

方法二:通过缓存请求体重复读取登录信息

如果需要获取完整的登录请求数据(而非仅用户名),可以使用Spring提供的ContentCachingRequestWrapper包装请求,实现请求体的重复读取:

重写doFilter方法包装请求

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    // 包装请求,缓存请求体以便重复读取
    ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper((HttpServletRequest) request);
    super.doFilter(wrappedRequest, response, chain);
}

在unsuccessfulAuthentication中读取缓存的请求体

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
    ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request;
    LoginDto loginInfo = null;
    try {
        // 从缓存的请求体中解析登录信息
        loginInfo = objectMapper.readValue(wrappedRequest.getContentAsByteArray(), LoginDto.class);
    } catch (IOException e) {
        // 处理解析失败的情况,比如日志记录
    }

    if (loginInfo != null && loginInfo.getUserEmail() != null) {
        // 执行日志记录或登录限制逻辑
        securityService.createSecurityLog(SecurityLogAction.loginFail(loginInfo.getUserEmail()));
    }

    SecurityContextHolder.clearContext();

    Response loginFail = Response.builder()
            .code(SecurityErrorCode.LOGIN_FAIL.getErrorCode())
            .result(SecurityErrorCode.LOGIN_FAIL.getErrorMessage())
            .build();
    response.setStatus(HttpStatus.UNAUTHORIZED.value());
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.setCharacterEncoding(StandardCharsets.UTF_8.name());
    response.getWriter().write(objectMapper.writeValueAsString(loginFail));
    response.getWriter().flush();
    response.getWriter().close();
}

注意事项

  • 请删除原代码中SecurityUserDetails userDetails = (SecurityUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();这一行,认证失败时该值必然为null,会导致空指针异常。
  • 方法一实现简单,适合仅需用户名的场景;方法二更灵活,能获取完整请求数据,但需要额外的请求包装操作。

内容的提问来源于stack exchange,提问作者남혁준

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 11:43:13