如何实现Okta SAML后接续Azure AD OAuth的连续认证?
最优实现方案:SpringBoot 顺序双认证(Okta → Azure AD)
核心思路是利用Spring Security的过滤器链优先级和认证状态跟踪,将两次OAuth2认证串联成强制顺序流程,无需依赖反向代理,在单应用内完成。
一、依赖准备
首先引入必要的Spring Boot Starter依赖:
<!-- Maven 依赖 --> <dependencies> <!-- Spring Security 核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Okta OAuth2 客户端支持 --> <dependency> <groupId>com.okta.spring</groupId> <artifactId>okta-spring-boot-starter</artifactId> <version>3.0.4</version> </dependency> <!-- Azure AD OAuth2 客户端支持 --> <dependency> <groupId>com.azure.spring</groupId> <artifactId>azure-spring-boot-starter-active-directory</artifactId> <version>4.12.0</version> </dependency> </dependencies>
二、基础配置(application.yml)
分别配置Okta和Azure AD的OAuth2客户端参数:
# Okta 配置 okta: oauth2: client-id: ${OKTA_CLIENT_ID} client-secret: ${OKTA_CLIENT_SECRET} issuer: https://your-okta-domain.com/oauth2/default redirect-uri: "{baseUrl}/login/oauth2/code/okta" scopes: openid, profile, email # Azure AD 配置 azure: active-directory: client-id: ${AZURE_CLIENT_ID} client-secret: ${AZURE_CLIENT_SECRET} tenant-id: ${AZURE_TENANT_ID} redirect-uri: "{baseUrl}/login/oauth2/code/azure" scopes: openid, profile, User.Read
三、核心认证流程配置
通过自定义Spring Security过滤器链,实现先Okta认证,再强制Azure AD认证的顺序逻辑:
1. 自定义认证状态跟踪
在Okta认证成功后,将用户标记为「仅完成Okta认证」,通过Session存储状态:
// 定义Session中的状态标记常量 public static final String AAD_AUTH_COMPLETED = "AAD_AUTH_COMPLETED";
2. 配置SecurityFilterChain
调整过滤器顺序,让Okta的OAuth2过滤器优先执行,然后添加自定义过滤器检查Azure AD认证状态:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; @Configuration @EnableWebSecurity public class SecurityConfig { private final PostOktaAuthenticationFilter postOktaAuthenticationFilter; public SecurityConfig(PostOktaAuthenticationFilter postOktaAuthenticationFilter) { this.postOktaAuthenticationFilter = postOktaAuthenticationFilter; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 允许认证相关端点匿名访问 .authorizeHttpRequests(auth -> auth .requestMatchers("/login/**", "/error").permitAll() .anyRequest().authenticated() ) // 配置Okta OAuth2客户端认证 .oauth2Login(oauth2 -> oauth2 .clientRegistrationId("okta") .successHandler(oktaAuthenticationSuccessHandler()) ) // 配置Azure AD OAuth2客户端认证 .oauth2Login(oauth2 -> oauth2 .clientRegistrationId("azure") .successHandler(aadAuthenticationSuccessHandler()) ) // 添加自定义过滤器:在Okta认证后检查Azure AD状态 .addFilterAfter(postOktaAuthenticationFilter, AbstractPreAuthenticatedProcessingFilter.class); return http.build(); } // Okta认证成功处理器:标记Session状态,跳转Azure AD认证入口 private AuthenticationSuccessHandler oktaAuthenticationSuccessHandler() { return (request, response, authentication) -> { request.getSession().setAttribute(AAD_AUTH_COMPLETED, false); response.sendRedirect("/oauth2/authorization/azure"); }; } // Azure AD认证成功处理器:更新Session状态,完成双认证 private AuthenticationSuccessHandler aadAuthenticationSuccessHandler() { return (request, response, authentication) -> { request.getSession().setAttribute(AAD_AUTH_COMPLETED, true); response.sendRedirect("/home"); }; } }
3. 自定义过滤器:检查双认证状态
创建PostOktaAuthenticationFilter,拦截请求并检查是否已完成两次认证:
import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; public class PostOktaAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Boolean isAadCompleted = (Boolean) request.getSession().getAttribute(SecurityConfig.AAD_AUTH_COMPLETED); // 已完成Okta认证,但未完成Azure AD认证 → 强制跳转Azure AD if (auth != null && auth.isAuthenticated() && (isAadCompleted == null || !isAadCompleted)) { response.sendRedirect("/oauth2/authorization/azure"); return; } // 双认证已完成 → 放行请求 filterChain.doFilter(request, response); } // 排除认证相关端点,避免循环跳转 @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { String path = request.getRequestURI(); return path.startsWith("/login/") || path.startsWith("/oauth2/authorization/") || path.startsWith("/error"); } }
四、可选优化:复合Authentication对象
如果需要在业务中同时使用Okta和Azure AD的用户信息,可以创建复合Authentication,合并两次认证的用户数据:
import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; import java.util.stream.Collectors; public class DualAuthentication implements Authentication { private final Authentication oktaAuth; private final Authentication aadAuth; private boolean authenticated; public DualAuthentication(Authentication oktaAuth, Authentication aadAuth) { this.oktaAuth = oktaAuth; this.aadAuth = aadAuth; this.authenticated = oktaAuth.isAuthenticated() && aadAuth.isAuthenticated(); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { // 合并两次认证的权限集合 return oktaAuth.getAuthorities().stream() .collect(Collectors.toList()); } @Override public Object getCredentials() { return null; } @Override public Object getDetails() { return null; } @Override public Object getPrincipal() { // 返回包含双认证信息的用户对象 return new DualUser( oktaAuth.getName(), aadAuth.getName() ); } @Override public boolean isAuthenticated() { return authenticated; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { this.authenticated = isAuthenticated; } @Override public String getName() { return oktaAuth.getName(); } // 自定义复合用户类 public static class DualUser { private final String oktaUserId; private final String aadUserId; public DualUser(String oktaUserId, String aadUserId) { this.oktaUserId = oktaUserId; this.aadUserId = aadUserId; } // getter方法 public String getOktaUserId() { return oktaUserId; } public String getAadUserId() { return aadUserId; } } }
修改Azure AD认证成功处理器,更新SecurityContext为复合Authentication:
private AuthenticationSuccessHandler aadAuthenticationSuccessHandler() { return (request, response, authentication) -> { Authentication oktaAuth = SecurityContextHolder.getContext().getAuthentication(); DualAuthentication dualAuth = new DualAuthentication(oktaAuth, authentication); SecurityContextHolder.getContext().setAuthentication(dualAuth); request.getSession().setAttribute(SecurityConfig.AAD_AUTH_COMPLETED, true); response.sendRedirect("/home"); }; }
关键说明
- 过滤器顺序:确保Okta的OAuth2过滤器先执行,自定义过滤器在其后拦截未完成双认证的请求。
- 状态跟踪:通过Session存储认证状态,避免重复触发认证流程。
- 循环跳转防护:自定义过滤器排除认证相关端点,防止无限重定向。
内容的提问来源于stack exchange,提问作者Sun
相关产品推荐
相关产品推荐

