如何将自定义状态机注入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
相关产品推荐
相关产品推荐

