自定义AuthenticationFilter认证成功但未保持登录状态问题排查
自定义登录后后续请求变为匿名用户问题排查
我使用Spring Boot搭配Hilla框架,因表单登录无法满足需求,自行实现了自定义登录功能。已创建AuthenticationProvider、AuthenticationFilter并配置到WebSecurityConfig中,登录请求返回认证成功,但后续请求却被识别为匿名用户。AbstractAuthenticationProcessingFilter的successfulAuthentication方法负责设置SecurityContext,但我不清楚遗漏了哪些配置或操作有误。尝试过不返回自定义响应、不设置AuthenticationSuccessHandler、手动设置SecurityContext等方式,均无法保持登录状态。
SecurityConfig
@EnableWebSecurity @Configuration @RequiredArgsConstructor public class SecurityConfig extends VaadinWebSecurity { private final JTicketAuthenticationProvider authenticationProvider; private final RouteUtil routeUtil; private final AuthenticationConfiguration authenticationConfiguration; private final UserService userService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeHttpRequests(registry -> registry.requestMatchers( routeUtil::isRouteAllowed).permitAll() .requestMatchers("/user/login").permitAll() .requestMatchers(HttpMethod.POST).permitAll()); http.addFilterAfter(new JTicketAuthenticationFilter(authenticationConfiguration.getAuthenticationManager(), userService), UsernamePasswordAuthenticationFilter.class); http.authenticationProvider(authenticationProvider); super.configure(http); } }
AuthenticationProvider
@Component @RequiredArgsConstructor public class JTicketAuthenticationProvider implements AuthenticationProvider { private final UserService userService; private final BCryptPasswordEncoder encoder; @Override public Authentication authenticate(final Authentication authentication) throws AuthenticationException { final JTicketAuthentication auth = (JTicketAuthentication) authentication; final UserDto user = userService.getUserDtoByEmail(auth.getEmail()); if (user != null) { var userCredential = userService.getUserCredentialById(user.getId()); if (!user.isCredentialsNonExpired()) { throw new ApiException("Credentials are expired. Please reset your password"); } var userPrincipal = new UserPrincipal(user, userCredential); validAccount.accept(userPrincipal); if (encoder.matches(auth.getPassword(), userCredential.getPassword())) { return JTicketAuthentication.authenticated(user, userPrincipal.getAuthorities()); } else { throw new BadCredentialsException("Email and/or passwords do not match"); } } else { throw new ApiException("Unable to authenticate"); } } @Override public boolean supports(final Class<?> authentication) { return JTicketAuthentication.class.isAssignableFrom(authentication); } private final Consumer<UserPrincipal> validAccount = userPrincipal -> { if (!userPrincipal.isAccountNonLocked()) { throw new LockedException("Your account is currently locked"); } if (!userPrincipal.isEnabled()) { throw new DisabledException("Your account is currently disabled"); } if (!userPrincipal.isCredentialsNonExpired()) { throw new CredentialsExpiredException("Your password has expired. Please update your password"); } if (!userPrincipal.isAccountNonExpired()) { throw new DisabledException("Your account has expired. Please contact administrator password"); } }; }
AuthenticationFilter
@Slf4j public class JTicketAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private final UserService userService; public JTicketAuthenticationFilter(final AuthenticationManager authManager, UserService userService) { super(new AntPathRequestMatcher("/user/login", HttpMethod.POST.name()), authManager); this.userService = userService; setAuthenticationSuccessHandler((_, _, _) -> {}); //自定义响应 } @Override public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response) throws AuthenticationException { try { LoginRequest user = new ObjectMapper().configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, true) .readValue(request.getInputStream(), LoginRequest.class); userService.updateLoginAttempt(user.getEmail(), LoginType.LOGIN_ATTEMPT); JTicketAuthentication unauthenticated = JTicketAuthentication.authenticated(user.getEmail(), user.getPassword()); return getAuthenticationManager().authenticate(unauthenticated); } catch (final Exception e) { log.error(e.getMessage()); RequestUtils.handleErrorResponse(request, response, e); return null; } } @Override protected void successfulAuthentication(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain, final Authentication authResult) throws IOException, ServletException { final UserDto user = (UserDto) authResult.getPrincipal(); userService.updateLoginAttempt(user.getEmail(), LoginType.LOGIN_SUCCESS); var httpResponse = sendResponse(request, response, user); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpStatus.OK.value()); var out = response.getOutputStream(); var mapper = new ObjectMapper(); mapper.writeValue(out, httpResponse); out.flush(); super.successfulAuthentication(request, response, chain, authResult); } private Response sendResponse(final HttpServletRequest request, final HttpServletResponse response, final UserDto user) { return RequestUtils.getResponse(request, Map.of("user", user), "Login Success", HttpStatus.OK); } }
日志信息
2024-11-01T01:06:14.448+01:00 DEBUG 16804 --- [JTicket] [nio-8081-exec-9] o.v.e.s.JTicketAuthenticationFilter : Set SecurityContextHolder to JTicketAuthentication [Principal=...] 2024-11-01T01:06:14.449+01:00 DEBUG 16804 --- [JTicket] [nio-8081-exec-9] o.s.s.web.DefaultRedirectStrategy : Redirecting to / 2024-11-01T01:06:14.454+01:00 DEBUG 16804 --- [JTicket] [nio-8081-exec-7] o.s.security.web.FilterChainProxy : Securing GET / 2024-11-01T01:06:14.454+01:00 DEBUG 16804 --- [JTicket] [nio-8081-exec-7] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext 2024-11-01T01:06:14.454+01:00 DEBUG 16804 --- [JTicket] [nio-8081-exec-7] o.s.security.web.FilterChainProxy : Secured GET /
AnonymousAuthenticationFilter核心逻辑
private SecurityContext defaultWithAnonymous(HttpServletRequest request, SecurityContext currentContext) { Authentication currentAuthentication = currentContext.getAuthentication(); if (currentAuthentication == null) { Authentication anonymous = createAuthentication(request); if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.of(() -> "Set SecurityContextHolder to " + anonymous)); } else { this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext"); } SecurityContext anonymousContext = this.securityContextHolderStrategy.createEmptyContext(); anonymousContext.setAuthentication(anonymous); return anonymousContext; } else { if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.of(() -> "Did not set SecurityContextHolder since already authenticated " + currentAuthentication)); } } return currentContext; }
日志显示currentContext.getAuthentication()返回null,因此上下文被设置为匿名,恳请帮忙排查问题。
问题排查与修复方案
核心原因分析
登录请求的SecurityContext设置成功,但后续请求中SecurityContext为null,说明认证状态未被持久化到HttpSession中。主要问题点包括:
- 过滤器顺序错误,导致认证后的上下文无法被
SecurityContextPersistenceFilter捕获并保存到会话 - 自定义成功处理器覆盖了默认的会话持久化逻辑
- 与Hilla/Vaadin的会话管理机制未同步
修复步骤
1. 调整过滤器顺序
将自定义Filter放在SecurityContextPersistenceFilter之后,确保认证后的上下文能被正确持久化:
http.addFilterAfter(new JTicketAuthenticationFilter(authenticationConfiguration.getAuthenticationManager(), userService), SecurityContextPersistenceFilter.class);
2. 修正successfulAuthentication方法
先调用父类方法完成SecurityContext的会话持久化,再处理自定义响应;同时移除空的成功处理器,恢复默认逻辑:
// 移除空处理器设置:setAuthenticationSuccessHandler((_, _, _) -> {}); @Override protected void successfulAuthentication(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain, final Authentication authResult) throws IOException, ServletException { // 先执行父类逻辑,完成SecurityContext的会话保存 super.successfulAuthentication(request, response, chain, authResult); // 再处理自定义响应 final UserDto user = (UserDto) authResult.getPrincipal(); userService.updateLoginAttempt(user.getEmail(), LoginType.LOGIN_SUCCESS); var httpResponse = sendResponse(request, response, user); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpStatus.OK.value()); var out = response.getOutputStream(); var mapper = new ObjectMapper(); mapper.writeValue(out, httpResponse); out.flush(); }
3. 确保会话同步与序列化
- 确认
JTicketAuthentication实现Serializable接口,保证SecurityContext能被序列化到HttpSession - 检查是否设置了
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS),如果有,改为IF_REQUIRED - 验证浏览器是否携带
JSESSIONIDCookie,确保登录请求与后续请求属于同一会话
4. 验证会话中的SecurityContext
在测试接口中添加调试代码,检查会话中是否存在SPRING_SECURITY_CONTEXT属性:
@GetMapping("/session-check") public String checkSession(HttpSession session) { Object context = session.getAttribute("SPRING_SECURITY_CONTEXT"); System.out.println("SecurityContext in session: " + context); return context != null ? "Authenticated" : "Anonymous"; }
内容的提问来源于stack exchange,提问作者Sili Cat
相关产品推荐
相关产品推荐

