Spring SAML - 在运行时读取和刷新 IdP 元数据

Posted

技术标签:

【中文标题】Spring SAML - 在运行时读取和刷新 IdP 元数据【英文标题】:Spring SAML - Reading and refreshing IdP metadata at runtime 【发布时间】:2017-08-11 22:20:31 【问题描述】:

我正在使用带有 Spring-SAML 扩展的 WSO2 和 SSOCircle。我们此时正在测试配置,并在我们的 applicationContext 中定义了 2 个 IdP 和 2 个 SP。因此,目前,我们的 spring xml 配置中有 2 个静态定义的 IdP,这是有效的。出于测试目的,我们使用 CachingMetadataManager 和 ResourceBackedMetadataProvider 的组合,因此 IdP 元数据构建在我们的 WAR 存档中。示例:

<bean id="metadata" class="org.springframework.security.saml.metadata.CachingMetadataManager">
<constructor-arg>
  <list>
    <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
      <constructor-arg>
        <bean class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
          <constructor-arg>
            <bean class="java.util.Timer"/>
          </constructor-arg>
          <constructor-arg>
            <bean class="org.opensaml.util.resource.ClasspathResource">
              <constructor-arg value="/metadata/wso2idp_metadata.xml"/>
            </bean>
          </constructor-arg>
          <property name="parserPool" ref="parserPool"/>
        </bean>
      </constructor-arg>
      <constructor-arg>
        <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
        </bean>
      </constructor-arg>
    </bean>
    <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
      <constructor-arg>
        <bean class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
          <constructor-arg>
            <bean class="java.util.Timer"/>
          </constructor-arg>
          <constructor-arg>
            <bean class="org.opensaml.util.resource.ClasspathResource">
              <constructor-arg value="/metadata/ssocircleidp_metadata.xml"/>
            </bean>
          </constructor-arg>
          <property name="parserPool" ref="parserPool"/>
        </bean>
      </constructor-arg>
      <constructor-arg>
        <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
        </bean>
      </constructor-arg>
    </bean>
  </list>
</constructor-arg>

对于生产,我们希望能够将 IdP 元数据存储在数据库中(位于中心位置)。我希望能够在不重新部署 WAR 或重新启动服务器的情况下添加、删除和修改元数据。最初,我认为我可以覆盖 CachingMetadataManager 并定义一个 noarg 构造函数,该构造函数可以动态加载所有元数据提供程序,但这是不可能的,因为 CachingMetadataManager 只定义了一个必须接受 MetadataProvider 列表的构造函数。我最终做了以下事情:

<bean id="metadataList" class="org.arbfile.util.security.saml.DBMetadataProviderList">
  <constructor-arg ref="parserPool" />
  <constructor-arg>
    <bean class="java.util.Timer"/>
  </constructor-arg>
</bean>

<bean id="metadata" class="org.springframework.security.saml.metadata.CachingMetadataManager">
   <constructor-arg ref="metadataList" />
</bean>

Bean metadataList 可以简单定义为:

public final class DBMetadataProviderList extends ArrayList<MetadataProvider>

  private final static Logger log = LoggerFactory.getLogger(DBMetadataProviderList.class);
  private ParserPool parser;

  public DBMetadataProviderList(ParserPool _parser, Timer _timer) throws MetadataProviderException
  
    this.parser = _parser;
// Lookup metadata from DB
  


这确实允许我动态读取 IdP 元数据。不过,当谈到刷新时,我的逻辑就崩溃了。我找到了this post on the spring forum,但它已经 3 到 4 岁了。动态读取、添加和更新 IdP 元数据、对其进行缓存并在某个时间间隔刷新缓存的最佳方式是什么?在我的情况下,数据库表中的 1 行将等同于单个 IdP 元数据定义。

【问题讨论】:

嗨吉姆,你能提供一个更新,你是怎么得到的?使用下面的示例?我还需要实现这个场景:)。 【参考方案1】:

在阅读了几篇文章并扫描了源代码后,我发现这个问题的答案比我想象的要复杂。实际上有 3 种不同的场景需要解决。

    从数据库表中初始读取所有 IdP 元数据提供者 过期并重新读取 IdP 元数据 XML 数据 无需更改配置或重新启动服务器即可动态添加和删除提供程序

我将一次处理其中的每一项。第 1 项:可能有几种方法可以解决此问题,但请查看上面的 DBMetadataProviderList 类(在我的 OP 中)作为快速而肮脏的解决方案。下面是更完整的构造函数代码:

//This constructor allows us to read in metadata stored in a database. 
public DBMetadataProviderList(ParserPool _parser, Timer _timer) throws MetadataProviderException

    this.parser = _parser;
    List<String> metadataProviderIds = getUniqueEntityIdListFromDB();
    for (final String mdprovId : metadataProviderIds)
    
        DBMetadataProvider metadataProvider = new DBMetadataProvider(_timer, mdprovId);
        metadataProvider.setParserPool(this.parser);
        metadataProvider.setMaxRefreshDelay(480000); // 8 mins (set low for testing)
        metadataProvider.setMinRefreshDelay(120000); // 2 mins
        ExtendedMetadataDelegate md = new ExtendedMetadataDelegate(metadataProvider,  new ExtendedMetadata());
        add(md);
    

为了解决第 2 项问题,我使用 FilesystemMetadataProvider 作为指导并创建了一个 DBMetadataProvider 类。通过扩展AbstractReloadingMetadataProvider 类并实现fetchMetadata() 方法,我们有内置的缓存刷新,这要归功于opensaml。以下是重要部分(仅示例代码):

public class DBMetadataProvider extends AbstractReloadingMetadataProvider

  private String metaDataEntityId;  // unique Id for DB lookups

 /**
  * Constructor.
  * @param entityId the entity Id of the metadata.  Use as key to identify a database row.
  */
 public DBMetadataProvider(String entityId)
 
    super();
    setMetaDataEntityId(entityId);
 

 /**
  * Constructor.
  * @param backgroundTaskTimer timer used to refresh metadata in the background
  * @param entityId the entity Id of the metadata.  Use as key to identify a database row.
  */
 public DBMetadataProvider(Timer backgroundTaskTimer, String entityId)
 
    super(backgroundTaskTimer);
    setMetaDataEntityId(entityId);
 

 public String getMetaDataEntityId()  return metaDataEntityId;  

 public void setMetaDataEntityId(String metaDataEntityId) this.metaDataEntityId = metaDataEntityId; 

 @Override
 protected String getMetadataIdentifier()  return getMetaDataEntityId(); 

// This example code simply does straight JDBC
 @Override
 protected byte[] fetchMetadata() throws MetadataProviderException
 
    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;
    try
    
        conn = JDBCUtility.getConnection();
        ps = conn.prepareStatement("select  bla bla bla ENTITY_ID = ?");
        ps.setString(1, getMetaDataEntityId());
        rs = ps.executeQuery();
        if (rs.next())
        
            // include a modified date column in schema so that we know if row has changed
            Timestamp sqldt = rs.getTimestamp("MOD_DATE"); // use TimeStamp here to get full datetime
            DateTime metadataUpdateTime = new DateTime(sqldt.getTime(), ISOChronology.getInstanceUTC());
            if (getLastRefresh() == null || getLastUpdate() == null || metadataUpdateTime.isAfter(getLastRefresh()))
            
                log.info("Reading IdP metadata from database with entityId = " + getMetaDataEntityId());
                Clob clob = rs.getClob("XML_IDP_METADATA");
                return clob2Bytes(clob);
            
            return null;
        
        else
        
            // row was not found
            throw new MetadataProviderException("Metadata with entityId = '" + getMetaDataEntityId() + "' does not exist");
        
    
    catch (Exception e)
    
        String msg = "Unable to query metadata from database with entityId = " + getMetaDataEntityId();
        log.error(msg, e);
        throw new MetadataProviderException(msg, e);
    
    finally
    
        // clean up connections
    
  

 

resource 帮助我找到了缓存重新加载元数据提供程序类的正确技术。最后,#3 可以通过实现这个post来解决。

【讨论】:

能否为第三点分享一个例子。我关注了这篇文章:docs.spring.io/autorepo/docs/spring-security-saml/1.0.0.RELEASE/…,但在删除元数据生成器过滤器后我被困在做什么。

以上是关于Spring SAML - 在运行时读取和刷新 IdP 元数据的主要内容,如果未能解决你的问题,请参考以下文章

Spring SAML WSO2 刷新断言

元数据刷新死锁(spring-security-saml)

如何在运行时在 spring-SAML 中添加新的 idp 元数据

ADFS spring-saml 依赖方没有配置 AssertionConsumerService

由于状态消息为空,ADFS 响应失败时的 Spring SAML 单点登录

SAML Spring Security 会话超时