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

自定义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中。主要问题点包括:

  1. 过滤器顺序错误,导致认证后的上下文无法被SecurityContextPersistenceFilter捕获并保存到会话
  2. 自定义成功处理器覆盖了默认的会话持久化逻辑
  3. 与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
  • 验证浏览器是否携带JSESSIONID Cookie,确保登录请求与后续请求属于同一会话

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 19:52:09