将 Spring Boot Security SAML 与 Azure AD Gallery 应用集成为多租户

Integrate Spring Boot Security SAML with Azure AD Gallery app as multi tenant

我正在开发 Java Spring 引导系统并尝试使用 SAML 单点登录与 Azure 非库应用集成。

我找到了如何创建非库应用程序、如何将非库应用程序应用到 Azure 库列表等。例如,这个 link 是关于配置 SAML SSO 的: Configure SAML-based single sign-on 所以我了解了 Azure 方面的配置和过程。

我正在使用 Spring 安全 SAML 扩展。但是我找不到 Spring 引导端配置,即使我做了很多研究,除了基于 XML 的官方 SAML 扩展文档。

顺便说一句,我的主要目标是将我们的组织应用程序添加到 Azure 库应用程序列表。我们的应用程序被多家公司使用,因此如果我们将组织应用程序添加到 Azure Gallery App list,我们的客户可以将他们的 Azure AD 帐户配置为 SSO 集成。

我的问题如下:

  1. 如何将 Azure Non-Gallery App 集成到 Spring Boot app?
  2. 如何处理多个 Azure AD 租户?

有人帮我吗?


编辑: 目前,我使用 Spring Boot 和 Azure AD 非库应用程序进行了单租户 SSO 登录。我使用 Azure AD 联合 XML 元数据 URL 配置了 IdP 元数据。您可以在下面查看源代码:

@Configuration
@Order(1)
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
    @Value("${security.saml2.metadata-url}")
    private String IdPMetadataURL;

    @Value("${server.ssl.key-alias}")
    private String keyStoreAlias;

    @Value("${server.ssl.key-store-password}")
    private String keyStorePassword;

    @Value("${server.port}")
    String port;

    @Value("${server.ssl.key-store}")
    private String keyStoreFile;

    @Autowired
    private SAMLUserService samlUserService;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/saml/**", "/", "/login", "/home", "/about").permitAll()
                .anyRequest().authenticated()
                .and()
                .apply(saml())
                .webSSOProfileConsumer(getWebSSOProfileConsumerImpl())
                .userDetailsService(samlUserService)
                .serviceProvider()
                .keyStore()
                .storeFilePath(this.keyStoreFile)
                .password(this.keyStorePassword)
                .keyname(this.keyStoreAlias)
                .keyPassword(this.keyStorePassword)
                .and()
                .protocol("https")
                .hostname(String.format("%s:%s", "localhost", this.port))
                .basePath("/")
                .and()
                .identityProvider()
                .metadataFilePath(IdPMetadataURL)
                .and();
    }

    public WebSSOProfileConsumerImpl getWebSSOProfileConsumerImpl(){
        WebSSOProfileConsumerImpl consumer = new WebSSOProfileConsumerImpl();
        consumer.setMaxAuthenticationAge(26000000); //300 days
        return consumer;
    }
}

从现在开始我需要生成 IdP 元数据 XML 而不是使用 IdP 元数据 URL。使用如下字段:

我想的过程是:

  1. 我们的客户在上面注册了他们的 Azure AD IdP 字段
  2. 我的Spring开机系统自动生成IdP元数据XML
  3. 然后客户的 Azure AD SSO 可以集成到我们的系统

有什么不对的请指教

为了将您的应用程序列入 Azure Gallery 应用程序列表,请通过 document。请完成文档中提到的整个过程,以便在 azure Gallery 中列出您的应用程序。

仅针对库中已存在的应用程序,才会提及应用程序的配置端。对于非图库应用,需要在应用端配置azure AD元数据值。

我正在使用 Spring 安全 SAML 扩展和 Spring Boot。您使用的是哪个 SAML IdP 无关紧要,因为您只需要 IdP 元数据。您生成 SP 元数据并按照 MS 文档中的说明使用它。您可以查看 Spring 安全 SAML 文档。

我终于完成了动态 IDP 的解决方案。我用了 spring-boot-security-saml this simplified project.Thank you for ulisesbocchio 这个实现它的人。 也非常感谢 ledjon 与我分享了他的经验。

这是我配置 http 安全性的 saml 部分的方式

http.apply(saml)
    .serviceProvider()
        .metadataGenerator()
        .entityId(LocalSamlConfig.LOCAL_SAML_ENTITY_ID)
        .entityBaseURL(entityBaseUrl)
        .includeDiscoveryExtension(false)
    .and()
        .sso()
        .successHandler(new SendToSuccessUrlPostAuthSuccessHandler(canvasAuthService))
    .and()
        .metadataManager(new LocalMetadataManagerAdapter(samlAuthProviderService))
        .extendedMetadata()
        .idpDiscoveryEnabled(false)
    .and()
        .keyManager()
        .privateKeyDERLocation("classpath:/saml/localhost.key.der")
        .publicKeyPEMLocation("classpath:/saml/localhost.cert")
    .and()
        .http()
            .authorizeRequests()
            .requestMatchers(saml.endpointsMatcher())
            .permitAll();

这里的重要部分是 .metadataManager(new LocalMetadataManagerAdapter(samlAuthProviderService)) 这就是我们要在这里解决的问题。对象 samlAuthProviderService 是一个 Bean-managed 对象,它包含实际从数据库中检索元数据的逻辑,因此没有太多关于它的内容。但这是我的 LocalMetadataManagerAdapter 大致的样子:

@Slf4j
public class LocalMetadataManagerAdapter extends CachingMetadataManager {

    private final SamlAuthProviderService samlAuthProviderService;

    public LocalMetadataManagerAdapter(SamlAuthProviderService samlAuthProviderService) throws MetadataProviderException {
        super(null);
        this.samlAuthProviderService = samlAuthProviderService;
    }

    @Override
    public boolean isRefreshRequired() {
        return false;
    }

    @Override
    public EntityDescriptor getEntityDescriptor(String entityID) throws MetadataProviderException {
        // we don't really want to use our default at all, so we're going to throw an error
        // this string value is defined in the "classpath:/saml/idp-metadata.xml" file:
        // which is then referenced in application.properties as saml.sso.idp.metadata-location=classpath:/saml/idp-metadata.xml
        if("defaultidpmetadata".equals(entityID)) {
            throw exNotFound("Unable to process requests for default idp. Please select idp with ?idp=x parameter.");
        }

        EntityDescriptor staticEntity = super.getEntityDescriptor(entityID);

        if(staticEntity != null)
            return staticEntity;

        // we need to inject one, and try again:
        injectProviderMetadata(entityID);

        return super.getEntityDescriptor(entityID);
    }

    @SneakyThrows
    private void injectProviderMetadata(String entityID) {
        String xml =
            samlAuthProviderService.getMetadataForConnection(entityID)
                .orElseThrow(() -> exRuntime("Unable to find metadata for entity: " + entityID));

        addMetadataProvider(new LocalMetadataProvider(entityID, xml));

        // this will force a refresh/re-wrap of the new entity
        super.refreshMetadata();
    }
}

这里的重要部分是 getEntityDescriptor() 的覆盖,它将在运行时调用以获取元数据对象。我还通过将 isRefreshRequired() 覆盖为 return false 来禁用刷新。您可以确定这对您的用例是否有意义。

引用的 LocalMetadataProvider 只是 class 到 store/return 需要时 xml 字符串的包装器:

public class LocalMetadataProvider extends AbstractReloadingMetadataProvider {

    private final String Id;
    private final String xmlData;

    public LocalMetadataProvider(String id, String xmlData) {
        this.Id = id;
        this.xmlData = xmlData;

        setParserPool(LocalBeanUtil.getBeanOrThrow(ParserPool.class));
    }

    @Override
    protected String getMetadataIdentifier() {
        return this.Id;
    }

    @Override
    protected byte[] fetchMetadata() throws MetadataProviderException {
        return xmlData.getBytes();
    }
}

最后我们可以将 idp 元数据 entityID 作为参数传递。并从数据库等中检索 entityID 元数据: /saml/login?idp=X 其中 X 是我们要传递给 getEntityDescriptor() 的实体 ID 值。