Spring SAML - 支持自定义 SAML 断言

Spring SAML - support for customized SAML Assertion

我们有一个产品有一个客户,当我们作为服务提供商并且 idp 在客户方时,我们使用 Spring Security SAML 为该客户实施了 SAML 流。

现在我们有另一个客户也希望使用 SAML 进行身份验证,我们希望同一个 SP 为这个客户实施 SAML 流程,第二个客户也将有 2 个 SAML 流程,一个用于移动设备,另一个对于使用相同 IDP 的其他设备。两个客户的IDP不同

问题

两个客户之间存在一些差异,例如断言属性不同,认证成功时的操作不同,目前我们提供自己的实现。

也可能会有更多变化,例如不同的绑定等...

我的问题是什么是最好的 option/best 实践来支持这种情况并能够扩展我的 SP 以支持更多 SAML 流,但断言属性和配置有所不同?

当我们使用 Spring SAML 时,我们是否应该为每个 SAML 风格使用不同的 Spring 安全上下文文件?

并行使用多个上下文时是否存在线程安全问题?

My question is what is the best option/best practice to support such scenario and to be able to extend my SP to support more SAML flows with differences in the Assertion attributes and more configurations?

要分支某些配置(例如断言属性),您需要创建单独的服务提供者。可以共享其他配置和服务。其他配置应该共享。例如,我使用单个自定义 SAMLUserDetailsS​​ervice 实现从凭证中提取唯一的 EntityID,并使用它为每个 IDP 映射不同的 SAML 属性。

When we use Spring SAML should we use different Spring Security context files for each of the SAML flavors? Are there issues with thread safety when using multiple contexts in parallel?

我不建议 运行 单独使用多个安全上下文。根据我的经验,Spring SAML 中涉及很多配置,并且很有可能,您将不得不通过这种方式不必要地复制大量代码。

在Spring SAML 中,有为不同的服务提供商使用不同的别名的概念。我已经为许多 IDP 设置了许多服务提供商,并且能够使用一个 Spring 安全上下文并在我需要处理差异的地方实施自定义服务。我没有您的要求的完整列表,并且可能有一些无法在单个 spring 安全上下文中完成,但我会等到确定是这种情况再采取该路线。

每个 IDP 之间具体有什么不同?

我被允许使用的代码有限 post,但我已经包含了我能做的。

  • 入口点URL - 如果您有多个在配置中设置了别名的 IDP,入口点 url默认为

    "/saml/login/alias/" +productAlias+ "?idp=" + entityId;

    如果您在负载平衡器后面,您可以将其配置为将您想要的任何 URL 重写为客户的 URL。

  • 绑定和断言 - 这些在每个服务提供商 metadata.xml 文件中配置,每个客户可能不同。真正的挑战是如何从经过身份验证的 SAML 请求中提取属性并以可用的形式获取它。

    我不知道是否有更好的方法来执行此操作,但我的要求是为我配置的任何 IDP 提供可映射和可配置的绑定。为此,我实现了一个自定义 SAMLUserDetailsService。从传入服务的 SAMLCredential 中,您可以使用 credential.getRemoteEntityID() 为客户提取映射。从那里您需要从凭证中解析出属性。

解析 Microsoft 和其他 IDP 的 SAML 属性的示例

 public class AttributeMapperImpl implements AttributeMapper {

    @Override
    public Map<String, List<String>> parseSamlStatements(List<AttributeStatement> attributeList) {
        Map<String, List<String>> map = new HashMap<>();
        attributeList.stream().map((statement) -> parseSamlAttributes(statement.getAttributes())).forEach((list) -> {
            map.putAll(list);
        });
        return map;
    }

    @Override
    public Map<String, List<String>> parseSamlAttributes(List<Attribute> attributes) {
        Map<String, List<String>> map = new HashMap<>();
        attributes.stream().forEach((attribute) -> {
            List<String> sList = parseXMLObject(attribute.getAttributeValues());
            map.put(attribute.getName(), sList);
        });
        return map;
    }

    @Override
    public List<String> parseXMLObject(List<XMLObject> objs) {
        List<String> list = new ArrayList<>();

        objs.stream().forEach((obj) -> {
            if(obj instanceof org.opensaml.xml.schema.impl.XSStringImpl){
                XSStringImpl xs = (XSStringImpl) obj;
                list.add(xs.getValue());
            }else if(obj instanceof org.opensaml.xml.schema.impl.XSAnyImpl){
                XSAnyImpl xs = (XSAnyImpl) obj;
                list.add(xs.getTextContent());
            }
        });  

        return list;
    }

    @Override
    public String parseSamlStatementsToString(Map<String, List<String>> map) {
        String values = "";
        Iterator it = map.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry pair = (Map.Entry) it.next();
            values += pair.getKey() + "=" + pair.getValue() + " ";
            it.remove(); // avoids a ConcurrentModificationException
        }
        return values;
    }

}
  • 对 Success/Failure 的操作 - 有许多可能的方法可以做到这一点。我选择在控制器中使用单个端点,该端点可以访问所有请求成功进入的会话。身份验证成功后,我可以退出会话,用户来自哪个 IDP,并相应地重定向它们。失败有点困难,因为它完全有可能并且很可能有些失败会非常严重,以至于您不知道请求来自哪个 IDP(即如果 saml 消息使用错误的证书签名)。