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,提问作者남혁준
相关产品推荐
相关产品推荐

