如何在多租户环境中自动选择配置的 SAML 身份提供程序以使用 Spring SAML 进行 SSO
Posted
技术标签:
【中文标题】如何在多租户环境中自动选择配置的 SAML 身份提供程序以使用 Spring SAML 进行 SSO【英文标题】:How do I automatically pick the configured SAML Identity provider in a multi-tenant environment to do SSO using Spring SAML 【发布时间】:2015-03-19 07:44:18 【问题描述】:我在多租户应用程序中使用 Spring SAML 来提供 SSO。不同的租户使用不同的 url 访问应用程序,并且每个租户都配置了单独的 Identity Provider。给定用于访问应用程序的 url,如何自动分配正确的身份提供者?
例子:
租户 1:http://tenant1.myapp.com
租户 2:http://tenant2.myapp.com
我看到我可以将参数 idp 添加到 url (http://tenant1.myapp.com?idp=my.idp.entityid.com) 并且 SAMLContextProvider 将选择具有该实体 id 的身份提供者。我开发了一个数据库支持的 MetadataProvider,它将租户主机名作为初始化参数,以从链接到该主机名的数据库中获取该租户的元数据。现在我想我需要一些方法来遍历元数据提供者以将元数据的 entityId 链接到主机名。不过,我看不到如何获取元数据的 entityId。这将解决我的问题。
【问题讨论】:
【参考方案1】:您可以在方法MetadataManager#parseProvider
中查看如何从MetadataProvider
中解析可用的实体ID。请注意,通常每个提供商都可以提供多个 IDP 和 SP 定义,而不仅仅是一个。
或者,您可以使用您自己的类进一步扩展 ExtendedMetadataDelegate
,包含您希望的任何其他元数据(如 entityId),然后只需将 MetadataProvider
重新键入您的自定义类,并在通过MetadataManager
.
如果我是你,我会采取一些不同的方法。我会扩展SAMLContextProviderImpl
,覆盖方法populatePeerEntityId
并在那里执行主机名/IDP 的所有匹配。详情请见original method。
【讨论】:
我创建了自己的 SAMLContextProvider 并覆盖了 populatePeerIdentityId。那效果很好。完成后,我意识到 SAMLContextProvider 仅在 SP 启动的 SSO 期间使用。我们主要使用 IDP 发起的 SSO,所以我也需要介绍这一点。我最终检查了传入消息的 peerEntityID 与在我的自定义 SAMLAuthenticationProvider 中为该租户配置的 IDP entityID。 这个将身份提供者映射到服务提供者的特性是支持多租户的关键。这是否计划在即将发布的版本中? 我们会看到,这个项目取决于我的空闲时间(它不是由任何人赞助的)而且没有太多。改善多租户是我想要完成的事情。【参考方案2】:在撰写本文时,Spring SAML 的版本为 1.0.1.FINAL。它不支持开箱即用的多租户。除了上述 Vladimir 给出的建议之外,我还找到了另一种实现多租户的方法。它非常简单直接,不需要扩展任何 Spring SAML 类。此外,它利用 Spring SAML 对 CachingMetadataManager
中别名的内置处理。
在您的控制器中,从请求中捕获租户名称,并使用租户名称作为别名创建一个ExtendedMetadata
对象。接下来从ExtendedMetadata
中创建一个ExtendedMetadataDelegate
并对其进行初始化。从中解析实体 id 并检查它们是否存在于MetadataManager
中。如果它们不存在,请添加提供程序并刷新元数据。然后使用getEntityIdForAlias()
从MetadataManager
获取实体ID。
这是控制器的代码。有 cmets inline 解释了一些注意事项:
@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。
【讨论】:
只想指出,除非您的用户有粘性会话,否则此解决方案在集群环境中不起作用......对 /login.do 的初始请求将元数据提供程序添加到与该请求关联的 JVM,但是用户可以在另一个不知道启动身份验证过程的 IDP 的 JVM 上返回应用程序...以上是关于如何在多租户环境中自动选择配置的 SAML 身份提供程序以使用 Spring SAML 进行 SSO的主要内容,如果未能解决你的问题,请参考以下文章
使用 spring-SAML 在 Pentaho 中多租户 SSO 登录后重定向
在多租户环境中,如何在运行时在不同的 url(子域)上为不同的服务提供者提供不同的元数据?
如何在同一应用程序中使用 spring-sample 示例配置 Spring Security 基本身份验证和 SAML 身份验证