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

如何将自定义状态机注入Spring Authorization Server实现多步骤认证

基于Spring Authorization Server的多步骤认证状态机接入方案

你当前基于Spring Authorization Server搭建的授权服务器已支持用户名密码一步认证,现在需要替换为自定义多步骤认证流程:用户先输入用户名,再根据用户名进入对应后续步骤,通过状态机推进至End状态完成认证(状态转换需包含异常处理,流程大致为:Init→输入用户名→分支状态→步骤推进→End)。以下是具体的接入位置和实现方式:

一、核心接入位置

需要在Spring Security过滤器链中插入自定义多步骤认证过滤器,位置要放在UsernamePasswordAuthenticationFilter之前,这样能拦截原本的用户名密码认证请求,完全接管认证流程。

二、具体接入实现

1. 封装自定义过滤器

把你提供的示例逻辑封装成OncePerRequestFilter子类(保证每个请求仅执行一次):

public class MultiStepAuthFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 仅拦截授权端点的认证请求(适配Spring Authorization Server的默认端点)
        if (!request.getRequestURI().contains("/oauth2/authorize") || !"POST".equals(request.getMethod())) {
            filterChain.doFilter(request, response);
            return;
        }

        Cookie sessionCookie = Arrays.stream(request.getCookies())
                .filter(cookie -> "session".equals(cookie.getName()))
                .findFirst().orElse(null);
        CustomSession customSession = null;
        if (sessionCookie != null) {
            customSession = getSessionFromDB(sessionCookie.getValue());
        }
        if (customSession == null) {
            customSession = new CustomSession();
            customSession.sessionId = UUID.randomUUID().toString();
            customSession.currentState = new InitState();
            // 配置安全的会话Cookie
            Cookie newCookie = new Cookie("session", customSession.sessionId);
            newCookie.setHttpOnly(true);
            newCookie.setSecure(request.isSecure());
            newCookie.setPath("/");
            response.addCookie(newCookie);
        }

        State currentState = customSession.getCurrentState();
        State nextState = null;
        try {
            nextState = currentState.next(request, response);
        } catch (AuthStateException e) {
            // 处理状态转换异常:重置状态+返回错误提示
            customSession.setCurrentState(new InitState());
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().write("认证步骤异常,请重新开始");
            saveSessionToDB(customSession);
            return;
        }

        customSession.setCurrentState(nextState);
        saveSessionToDB(customSession);

        if (nextState instanceof EndState) {
            // 认证完成:构建Authentication对象并存入SecurityContext
            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    customSession.getAuthenticatedUser(),
                    null,
                    Collections.emptyList() // 替换为实际用户权限列表
            );
            SecurityContextHolder.getContext().setAuthentication(authentication);
            // 继续执行后续过滤器,完成授权流程
            filterChain.doFilter(request, response);
        } else {
            // 返回当前步骤的输入要求(可自定义JSON格式)
            response.setContentType("application/json");
            response.getWriter().write(JSONObject.toJSONString(nextState.getPrompt()));
        }
    }

    // 省略getSessionFromDB、saveSessionToDB等持久化方法实现
}

2. 配置过滤器到Security链

在Spring Authorization Server的Security配置类中,将自定义过滤器插入到默认用户名密码过滤器之前:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().authenticated()
                )
                // 禁用默认表单登录,完全使用自定义多步骤流程
                .formLogin(form -> form.disable())
                .addFilterBefore(multiStepAuthFilter(), UsernamePasswordAuthenticationFilter.class)
                // 配置Spring Authorization Server核心规则
                .oauth2AuthorizationServer(oauth2 -> oauth2
                        .authorizationEndpoint(endpoint -> endpoint
                                .authenticationProviders(authenticationProviders())
                        )
                );
        return http.build();
    }

    @Bean
    public MultiStepAuthFilter multiStepAuthFilter() {
        return new MultiStepAuthFilter();
    }

    // 自定义认证提供者,用于最终用户合法性校验
    private List<AuthenticationProvider> authenticationProviders() {
        return List.of(new CustomAuthenticationProvider());
    }
}

3. 可选:自定义认证提供者

如果需要在状态机到达End状态时做最终用户合法性校验,可实现AuthenticationProvider:

public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;

    public CustomAuthenticationProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        // 查询数据库验证用户有效性
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

三、关键注意事项

  • 会话一致性:必须保证CustomSession的持久化可靠性,避免用户刷新页面后丢失认证步骤状态
  • 异常闭环:在状态转换的next()方法中需覆盖所有异常场景,比如非法输入、状态跳转失败等,及时重置状态或返回清晰提示
  • 端点精准拦截:仅拦截授权服务器的认证请求,不要影响其他业务端点的正常访问
  • 安全加固:自定义会话Cookie需设置HttpOnly、Secure属性,防止XSS和CSRF攻击

内容的提问来源于stack exchange,提问作者devendrak353

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 20:30:37