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

如何实现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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 14:54:18