如何在多租户环境中自动选择已配置的SAML身份提供程序以使用Spring SAML执行SSO

时间:2015-01-20 22:20:43

标签: spring-security spring-saml

我在多租户应用程序中使用Spring SAML来提供SSO。不同的租户使用不同的URL来访问应用程序,并且每个租户都配置了单独的身份提供程序。在给定用于访问应用程序的URL的情况下,如何自动分配正确的身份提供者?

示例:

租户1:http://tenant1.myapp.com

租户2:http://tenant2.myapp.com

我看到我可以在url(http://tenant1.myapp.com?idp=my.idp.entityid.com)中添加参数idp,SAMLContextProvider将选择具有该实体ID的身份提供者。我开发了一个数据库支持的MetadataProvider,它将租户主机名作为初始化参数,从该主机中获取链接到该主机名的数据库的元数据。现在我想我需要一些方法来迭代元数据提供程序,以将元数据的entityId链接到主机名。但是,我不知道如何获取元数据的entityId。这将解决我的问题。

2 个答案:

答案 0 :(得分:6)

您可以在方法MetadataManager#parseProvider中查看如何解析MetadataProvider中的可用实体ID。请注意,通常每个提供商都可以提供多个IDP和SP定义,而不仅仅是一个。

或者,您可以使用自己的类进一步扩展ExtendedMetadataDelegate,包含您希望的任何其他元数据(如entityId),然后只需将MetadataProvider重新键入您的自定义类,并从那里获取信息通过MetadataManager迭代数据。

如果我是你,我会采取一些不同的方法。我会扩展SAMLContextProviderImpl,覆盖方法populatePeerEntityId并在那里执行主机名/ IDP的所有匹配。有关详细信息,请参阅original method

答案 1 :(得分:3)

在撰写本文时,Spring SAML的版本为1.0.1.FINAL。它不支持干净利落的多租户。除了上面弗拉基米尔提出的建议外,我找到了实现多租户的另一种方式。它非常简单直接,不需要扩展任何Spring SAML类。此外,它利用Spring SAML在CachingMetadataManager中内置的别名处理。

在您的控制器中,从请求中捕获租户名称,并使用租户名称作为别名创建ExtendedMetadata对象。接下来,在ExtendedMetadataDelegate中创建ExtendedMetadata并初始化它。从中解析实体ID并检查它们是否存在于MetadataManager中。如果它们不存在,请添加提供程序并刷新元数据。然后使用MetadataManagergetEntityIdForAlias()获取实体ID。

这是控制器的代码。有评论内联解释一些警告:

@Controller
public class SAMLController {

    @Autowired
    MetadataManager metadataManager;

    @Autowired
    ParserPool parserPool;

    @RequestMapping(value = "/login.do", method = RequestMethod.GET)
    public ModelAndView login(HttpServletRequest request, HttpServletResponse response, @RequestParam String tenantName)
                                                        throws MetadataProviderException, ServletException, IOException{
        //load metadata url using tenant name
        String tenantMetadataURL = loadTenantMetadataURL(tenantName);

        //Deprecated constructor, needs to change
        HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(tenantMetadataURL, 15000);
        httpMetadataProvider.setParserPool(parserPool);

        //Create extended metadata using tenant name as the alias
        ExtendedMetadata metadata = new ExtendedMetadata();
        metadata.setLocal(true);
        metadata.setAlias(tenantName);

        //Create metadata provider and initialize it
        ExtendedMetadataDelegate metadataDelegate = new ExtendedMetadataDelegate(httpMetadataProvider, metadata);
        metadataDelegate.initialize();

        //getEntityIdForAlias() in MetadataManager must only be called after the metadata provider
        //is added and the metadata is refreshed. Otherwise, the alias will be mapped to a null
        //value. The following code is a roundabout way to figure out whether the provider has already
        //been added or not. 

        //The method parseProvider() has protected scope in MetadataManager so it was copied here         
        Set<String> newEntityIds = parseProvider(metadataDelegate);
        Set<String> existingEntityIds = metadataManager.getIDPEntityNames();

        //If one or more IDP entity ids do not exist in metadata manager, assume it's a new provider.
        //If we always add a provider without this check, the initialize methods in refreshMetadata()
        //ignore the provider in case of a duplicate but the duplicate still gets added to the list
        //of providers because of the call to the superclass method addMetadataProvider(). Might be a bug.
        if(!existingEntityIds.containsAll(newEntityIds)) {
            metadataManager.addMetadataProvider(metadataDelegate);
            metadataManager.refreshMetadata();
        }

        String entityId = metadataManager.getEntityIdForAlias(tenantName);

        return new ModelAndView("redirect:/saml/login?idp=" + URLEncoder.encode(entityId, "UTF-8"));
    }

    private Set<String> parseProvider(MetadataProvider provider) throws MetadataProviderException {
        Set<String> result = new HashSet<String>();

        XMLObject object = provider.getMetadata();
        if (object instanceof EntityDescriptor) {
            addDescriptor(result, (EntityDescriptor) object);
        } else if (object instanceof EntitiesDescriptor) {
            addDescriptors(result, (EntitiesDescriptor) object);
        }

        return result;

    }

    private void addDescriptors(Set<String> result, EntitiesDescriptor descriptors) throws MetadataProviderException {
        if (descriptors.getEntitiesDescriptors() != null) {
            for (EntitiesDescriptor descriptor : descriptors.getEntitiesDescriptors()) {
                addDescriptors(result, descriptor);
            }
        }

        if (descriptors.getEntityDescriptors() != null) {
            for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) {
                addDescriptor(result, descriptor);
            }
        }
    }

    private void addDescriptor(Set<String> result, EntityDescriptor descriptor) throws MetadataProviderException {
        String entityID = descriptor.getEntityID();
        result.add(entityID);
    }
}

我相信这直接解决了OP的问题,即如何获得给定租户的IDP。但这仅适用于具有单个实体ID的IDP。