在多租户环境中,我如何在运行时在不同的 URL(子域)上为不同的服务提供商提供不同的元数据?
In an multi-tenant enviroment how can I provide different metadata for different Service Providers at runtime on different urls (subdomains)?
处理 SP 发起的单点登录 (SSO),SP 和 IdP 都是自托管的,因此可以灵活地编辑两者。我正在使用 spring-security-saml2-core-1.0.1.RELEASE 来托管 spring 应用程序(spring-security-3.2.8,spring-mvc -3.2.14.RELEASE),在 url 上为多个租户提供服务说:
sp1.example.org,
sp2.example.org
IdP 使用 Shibboleth IdPv3.2.1 托管,它可以很好地处理托管在不同 SP 服务器上的多个应用程序。
我正在尝试从同一服务器为 sp1 和 sp2 发送不同的元数据。我通过覆盖 SAMLContextProviderImpl populatePeerEntityId
阅读了关于多租户 SP here and here 自定义逻辑的文章,同样我试图覆盖 populateLocalEntityId
因为我不能使用 alias
。
有人可以提供用于处理多租户元数据的覆盖 populateLocalEntityId
的示例代码吗?
SP配置如下图:
<!-- Filters for processing of SAML messages -->
<beans:bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
<filter-chain-map request-matcher="ant">
<filter-chain pattern="/saml/login/**" filters="samlEntryPoint" />
<filter-chain pattern="/saml/logout/**" filters="samlLogoutFilter" />
<filter-chain pattern="/saml/metadata/**" filters="metadataDisplayFilter" />
<filter-chain pattern="/saml/SSO/**" filters="samlWebSSOProcessingFilter" />
<filter-chain pattern="/saml/SSOHoK/**" filters="samlWebSSOHoKProcessingFilter" />
<filter-chain pattern="/saml/SingleLogout/**" filters="samlLogoutProcessingFilter" />
<filter-chain pattern="/saml/discovery/**" filters="samlIDPDiscovery" />
</filter-chain-map>
</beans:bean>
<!-- Handler deciding where to redirect user after successful login -->
<beans:bean id="successRedirectHandler" class="com.example.web.sso.CustomAuthenticationSuccessHandler" ></beans:bean>
<!-- <beans:bean id="successRedirectHandler"
class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/WEB-INF/security/idpSelection.jsp"/>
</beans:bean> -->
<!-- Use the following for interpreting RelayState coming from unsolicited
response as redirect URL:
<beans:bean id="successRedirectHandler" class="org.springframework.security.saml.SAMLRelayStateSuccessHandler">
<property name="defaultTargetUrl" value="/" /> </beans:bean> -->
<!-- Handler deciding where to redirect user after failed login -->
<beans:bean id="failureRedirectHandler" class="com.example.web.sso.CustomAuthenticationFailureHandler"></beans:bean>
<!-- <beans:bean id="failureRedirectHandler"
class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<property name="useForward" value="true" />
<property name="defaultFailureUrl" value="/error.jsp" />
</beans:bean> -->
<!-- Handler for successful logout -->
<beans:bean id="successLogoutHandler"
class="com.example.web.sso.CustomLogoutSuccessHandler" ></beans:bean>
<!-- <beans:bean id="successLogoutHandler"
class="org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler">
<property name="defaultTargetUrl" value="/logout.jsp" />
</beans:bean> -->
<authentication-manager alias="samlauthenticationManager">
<!-- Register authentication manager for SAML provider -->
<authentication-provider ref="samlAuthenticationProvider" />
<!-- Register authentication manager for administration UI -->
<authentication-provider>
<user-service id="adminInterfaceService">
<user name="admin" password="admin" authorities="ROLE_ADMIN" />
</user-service>
</authentication-provider>
</authentication-manager>
<!-- Logger for SAML messages and events -->
<beans:bean id="samlLogger" class="org.springframework.security.saml.log.SAMLDefaultLogger" >
<beans:property name="logMessages" value="true" />
<beans:property name="logErrors" value="true" />
</beans:bean>
<!-- Central storage of cryptographic keys -->
<beans:bean id="keyManager" class="org.springframework.security.saml.key.JKSKeyManager">
<beans:constructor-arg value="/WEB-INF/keys/samlKeystore.jks"></beans:constructor-arg>
<beans:constructor-arg type="java.lang.String" value="nalle123" />
<beans:constructor-arg>
<beans:map>
<beans:entry key="apollo" value="nalle123" />
</beans:map>
</beans:constructor-arg>
<beans:constructor-arg type="java.lang.String" value="apollo" />
</beans:bean>
<!-- Entry point to initialize authentication, default values taken from
properties file -->
<beans:bean id="samlEntryPoint" class="com.example.web.sso.CustomSAMLEntryPoint">
<beans:property name="defaultProfileOptions">
<beans:bean class="org.springframework.security.saml.websso.WebSSOProfileOptions">
<beans:property name="binding" value="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"/>
<beans:property name="nameID" value="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" />
<beans:property name="includeScoping" value="false" />
<beans:property name="forceAuthN" value="false" />
</beans:bean>
</beans:property>
</beans:bean>
<!-- IDP Discovery Service -->
<beans:bean id="samlIDPDiscovery" class="org.springframework.security.saml.SAMLDiscovery">
<beans:property name="idpSelectionPath" value="/WEB-INF/security/idpSelection.jsp" />
</beans:bean>
<!-- Filter automatically generates default SP metadata -->
<beans:bean id="metadataGeneratorFilter"
class="org.springframework.security.saml.metadata.MetadataGeneratorFilter">
<beans:constructor-arg>
<beans:bean class="org.springframework.security.saml.metadata.MetadataGenerator">
<beans:property name="entityId" value="com:example:namespaceId:saml:poc" />
<!-- <beans:property name="entityBaseURL" value="https://sp1.example.com:8080/" /> -->
<beans:property name="requestSigned" value="true" />
<beans:property name="wantAssertionSigned" value="true" />
<beans:property name="extendedMetadata">
<beans:bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
<beans:property name="idpDiscoveryEnabled" value="true" />
</beans:bean>
</beans:property>
</beans:bean>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="metadataGenerator" class="org.springframework.security.saml.metadata.MetadataGenerator">
<beans:property name="entityId" value="com:example:namespaceId:saml:poc" />
<beans:property name="entityBaseURL" value="https://sp1.example.com:8080/" />
<beans:property name="requestSigned" value="true" />
<beans:property name="wantAssertionSigned" value="true" />
<beans:property name="extendedMetadata">
<beans:bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
<beans:property name="idpDiscoveryEnabled" value="true" />
</beans:bean>
</beans:property>
</beans:bean>
<!-- The filter is waiting for connections on URL suffixed with filterSuffix
and presents SP metadata there -->
<beans:bean id="metadataDisplayFilter"
class="org.springframework.security.saml.metadata.MetadataDisplayFilter" />
<!-- Configure HTTP Client to accept certificates from the keystore for
HTTPS verification -->
<!-- <beans:bean class="org.springframework.security.saml.trust.httpclient.TLSProtocolConfigurer">
<beans:property name="sslHostnameVerification" value="default"/> </beans:bean> -->
<!-- IDP Metadata configuration - paths to metadata of IDPs in circle of
trust is here -->
<beans:bean id="metadata"
class="org.springframework.security.saml.metadata.CachingMetadataManager">
<beans:constructor-arg>
<beans:list>
<!-- Example of classpath metadata with Extended Metadata -->
<beans:bean
class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
<beans:constructor-arg>
<beans:bean
class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
<beans:constructor-arg>
<beans:bean class="java.util.Timer" />
</beans:constructor-arg>
<beans:constructor-arg>
<beans:bean class="org.opensaml.util.resource.ClasspathResource">
<!-- <beans:bean class="org.opensaml.util.resource.FilesystemResource"> -->
<beans:constructor-arg value = "/WEB-INF/metadata/sp1-mymetadata.xml"></beans:constructor-arg>
</beans:bean>
</beans:constructor-arg>
<beans:property name="parserPool" ref="parserPool" />
</beans:bean>
</beans:constructor-arg>
<beans:constructor-arg>
<beans:bean
class="org.springframework.security.saml.metadata.ExtendedMetadata">
<beans:property name="local" value="true" />
<beans:property name="securityProfile" value="metaiop" />
<beans:property name="sslSecurityProfile" value="pkix" />
<beans:property name="sslHostnameVerification" value="default" />
<!-- <beans:property name="sslHostnameVerification" value="allowAll" /> -->
<beans:property name="signMetadata" value="false" />
<beans:property name="signingKey" value="apollo" />
<beans:property name="encryptionKey" value="apollo" />
<beans:property name="requireArtifactResolveSigned" value="false" />
<beans:property name="requireLogoutRequestSigned" value="false" />
<beans:property name="requireLogoutResponseSigned" value="false" />
<beans:property name="idpDiscoveryEnabled" value="false" />
<beans:property name="idpDiscoveryURL" value="https://sp1.example.com/saml/discovery" />
<beans:property name="idpDiscoveryResponseURL" value="https://sp1.example.com/saml/login?disco=true" />
</beans:bean>
</beans:constructor-arg>
</beans:bean>
<!-- Example of HTTP metadata without Extended Metadata -->
<!-- <beans:bean class="org.opensaml.saml2.metadata.provider.HTTPMetadataProvider">
URL containing the metadata
<beans:constructor-arg>
<beans:value type="java.lang.String">https://idp.ssocircle.com/idp-meta.xml</beans:value>
<beans:value type="java.lang.String">https://sp1.example.com/idp-meta.xml</beans:value>
</beans:constructor-arg>
Timeout for metadata loading in ms
<beans:constructor-arg>
<beans:value type="int">15000</beans:value>
</beans:constructor-arg>
<beans:property name="parserPool" ref="parserPool" />
</beans:bean> -->
<beans:bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
<beans:constructor-arg>
<beans:bean class="org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider">
<beans:constructor-arg>
<beans:value type="java.io.File">/shared/saml/idp-metadata-exampleIdp.xml</beans:value>
</beans:constructor-arg>
<beans:property name="parserPool" ref="parserPool"/>
</beans:bean>
</beans:constructor-arg>
<beans:constructor-arg>
<beans:bean class="org.springframework.security.saml.metadata.ExtendedMetadata"/>
</beans:constructor-arg>
</beans:bean>
<!-- Example of file system metadata without Extended Metadata -->
<!-- <bean class="org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider">
<constructor-arg> <value type="java.io.File">/usr/local/metadata/idp.xml</value>
</constructor-arg> <property name="parserPool" ref="parserPool"/> </bean> -->
</beans:list>
</beans:constructor-arg>
<!-- OPTIONAL used when one of the metadata files contains information
about this service provider -->
<!-- <property name="hostedSPName" value=""/> -->
<!-- OPTIONAL property: can tell the system which IDP should be used for
authenticating user by default. -->
<!-- <property name="defaultIDP" value="http://localhost:8080/opensso"/> -->
<beans:property name="defaultIDP" value="https://login.example.com/idp/shibboleth"/>
</beans:bean>
<!-- SAML Authentication Provider responsible for validating of received
SAML messages -->
<beans:bean id="samlAuthenticationProvider"
class="org.springframework.security.saml.SAMLAuthenticationProvider">
<!-- OPTIONAL property: can be used to store/load user data after login -->
<beans:property name="userDetails" ref="sAMLUserDetailsServiceImpl" />
<beans:property name="forcePrincipalAsString" value="false" />
</beans:bean>
<beans:bean id="sAMLUserDetailsServiceImpl"
class="com.example.service.impl.SAMLUserDetailsServiceImpl"></beans:bean>
<!-- Provider of default SAML Context -->
<!-- <beans:bean id="contextProvider"
class="org.springframework.security.saml.context.SAMLContextProviderImpl"> -->
<beans:bean id="contextProvider"
class="com.example.service.impl.CustomSAMLContextProviderImpl">
<beans:property name="storageFactory">
<!-- <beans:bean class="org.springframework.security.saml.storage.EmptyStorageFactory" /> -->
<beans:bean class="org.springframework.security.saml.storage.HttpSessionStorageFactory" />
</beans:property>
</beans:bean>
<!-- <beans:bean id="contextProvider"
class="org.springframework.security.saml.context.SAMLContextProviderLB">
<beans:property name="scheme" value="https" />
<beans:property name="serverName" value="https://sp1.example.com" />
<beans:property name="serverPort" value="443" />
<beans:property name="includeServerPortInRequestURL" value="false" />
</beans:bean> -->
<!-- Processing filter for WebSSO profile messages -->
<beans:bean id="samlWebSSOProcessingFilter" class="org.springframework.security.saml.SAMLProcessingFilter">
<beans:property name="authenticationManager" ref="samlauthenticationManager" />
<beans:property name="authenticationSuccessHandler" ref="successRedirectHandler" />
<beans:property name="authenticationFailureHandler" ref="failureRedirectHandler" />
<beans:property name="sessionAuthenticationStrategy" ref="sas"/>
</beans:bean>
<!-- Processing filter for WebSSO Holder-of-Key profile -->
<beans:bean id="samlWebSSOHoKProcessingFilter"
class="org.springframework.security.saml.SAMLWebSSOHoKProcessingFilter">
<beans:property name="authenticationManager" ref="samlauthenticationManager" />
<beans:property name="authenticationSuccessHandler" ref="successRedirectHandler" />
<beans:property name="authenticationFailureHandler" ref="failureRedirectHandler" />
</beans:bean>
<!-- Logout handler terminating local session -->
<beans:bean id="logoutHandler"
class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler">
<beans:property name="invalidateHttpSession" value="true" />
</beans:bean>
<!-- Override default logout processing filter with the one processing SAML
messages -->
<beans:bean id="samlLogoutFilter" class="org.springframework.security.saml.SAMLLogoutFilter">
<beans:constructor-arg index="0" ref="successLogoutHandler" />
<beans:constructor-arg index="1" ref="logoutHandler" />
<beans:constructor-arg index="2" ref="logoutHandler" />
</beans:bean>
<!-- Filter processing incoming logout messages -->
<!-- First argument determines URL user will be redirected to after successful
global logout -->
<beans:bean id="samlLogoutProcessingFilter"
class="org.springframework.security.saml.SAMLLogoutProcessingFilter">
<beans:constructor-arg index="0" ref="successLogoutHandler" />
<beans:constructor-arg index="1" ref="logoutHandler" />
</beans:bean>
<!-- Class loading incoming SAML messages from httpRequest stream -->
<beans:bean id="processor"
class="org.springframework.security.saml.processor.SAMLProcessorImpl">
<beans:constructor-arg>
<beans:list>
<beans:ref bean="postBinding" />
<beans:ref bean="redirectBinding" />
<beans:ref bean="artifactBinding" />
<beans:ref bean="soapBinding" />
<beans:ref bean="paosBinding" />
</beans:list>
</beans:constructor-arg>
</beans:bean>
<!-- SAML 2.0 WebSSO Assertion Consumer -->
<beans:bean id="webSSOprofileConsumer"
class="org.springframework.security.saml.websso.WebSSOProfileConsumerImpl" >
<!-- maximum lifetime of assertion issued by Idp default 3000-->
<!-- <beans:property name="maxAssertionTime" value="300"></beans:property> -->
<!-- maximum lifetime of authentication issued default 7200-->
<!-- <beans:property name="maxAssertionTime" value="300"></beans:property> -->
</beans:bean>
<!-- SAML 2.0 Holder-of-Key WebSSO Assertion Consumer -->
<beans:bean id="hokWebSSOprofileConsumer"
class="org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl" />
<!-- SAML 2.0 Web SSO profile -->
<beans:bean id="webSSOprofile"
class="org.springframework.security.saml.websso.WebSSOProfileImpl" />
<!-- SAML 2.0 Holder-of-Key Web SSO profile -->
<beans:bean id="hokWebSSOProfile"
class="org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl" />
<!-- SAML 2.0 ECP profile -->
<beans:bean id="ecpprofile"
class="org.springframework.security.saml.websso.WebSSOProfileECPImpl" />
<!-- SAML 2.0 Logout Profile -->
<beans:bean id="logoutprofile"
class="org.springframework.security.saml.websso.SingleLogoutProfileImpl" />
<!-- Bindings, encoders and decoders used for creating and parsing messages -->
<beans:bean id="postBinding"
class="org.springframework.security.saml.processor.HTTPPostBinding">
<beans:constructor-arg ref="parserPool" />
<beans:constructor-arg ref="velocityEngine" />
</beans:bean>
<beans:bean id="redirectBinding"
class="org.springframework.security.saml.processor.HTTPRedirectDeflateBinding">
<beans:constructor-arg ref="parserPool" />
</beans:bean>
<beans:bean id="artifactBinding"
class="org.springframework.security.saml.processor.HTTPArtifactBinding">
<beans:constructor-arg ref="parserPool" />
<beans:constructor-arg ref="velocityEngine" />
<beans:constructor-arg>
<beans:bean
class="org.springframework.security.saml.websso.ArtifactResolutionProfileImpl">
<beans:constructor-arg>
<beans:bean class="org.apache.commons.httpclient.HttpClient">
<beans:constructor-arg>
<beans:bean
class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager" />
</beans:constructor-arg>
</beans:bean>
</beans:constructor-arg>
<beans:property name="processor">
<beans:bean
class="org.springframework.security.saml.processor.SAMLProcessorImpl">
<beans:constructor-arg ref="soapBinding" />
</beans:bean>
</beans:property>
</beans:bean>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="soapBinding"
class="org.springframework.security.saml.processor.HTTPSOAP11Binding">
<beans:constructor-arg ref="parserPool" />
</beans:bean>
<beans:bean id="paosBinding"
class="org.springframework.security.saml.processor.HTTPPAOS11Binding">
<beans:constructor-arg ref="parserPool" />
</beans:bean>
<!-- Initialization of OpenSAML library -->
<beans:bean class="org.springframework.security.saml.SAMLBootstrap" />
<!-- Initialization of the velocity engine -->
<beans:bean id="velocityEngine" class="org.springframework.security.saml.util.VelocityFactory"
factory-method="getEngine" />
<!-- XML parser pool needed for OpenSAML parsing -->
<beans:bean id="parserPool" class="org.opensaml.xml.parse.StaticBasicParserPool"
init-method="initialize">
<beans:property name="builderFeatures">
<beans:map>
<beans:entry key="http://apache.org/xml/features/dom/defer-node-expansion"
value="false" />
</beans:map>
</beans:property>
</beans:bean>
<beans:bean id="parserPoolHolder"
class="org.springframework.security.saml.parser.ParserPoolHolder" ></beans:bean>
更新 1:更好的解决方案是扩展 SAMLContextProviderImpl 并覆盖 populateLocalEntityId、getLocalEntity、getLocalAndPeerEntity 以设置正确的 SAMLMessageContext,因为会为每个请求创建新的 SAMLMessageContext。
@Override
public SAMLMessageContext getLocalAndPeerEntity(HttpServletRequest request, HttpServletResponse response) throws MetadataProviderException {
SAMLMessageContext context = new SAMLMessageContext();
populateGenericContext(request, response, context);
//changed to send URL instead of URI
populateLocalEntityId(context, request.getRequestURL().toString());
populateLocalContext(context);
populatePeerEntityId(context);
populatePeerContext(context);
return context;
}
@Override
public SAMLMessageContext getLocalEntity(HttpServletRequest request, HttpServletResponse response) throws MetadataProviderException {
SAMLMessageContext context = new SAMLMessageContext();
populateGenericContext(request, response, context);
populateLocalEntityId(context, request.getRequestURL().toString());
populateLocalContext(context);
return context;
}
@Override
protected void populateLocalEntityId(SAMLMessageContext context, String requestURL) throws MetadataProviderException {
String entityId;
HTTPInTransport inTransport = (HTTPInTransport) context.getInboundMessageTransport();
// Pre-configured entity Id
entityId = (String) inTransport.getAttribute(org.springframework.security.saml.SAMLConstants.LOCAL_ENTITY_ID);
if (entityId != null) {
// same code as super class
} else { // Defaults
//Now setting proper entityId as required
//in this case https://sp1.wooqer.com/sp
if(org.apache.commons.lang.StringUtils.ordinalIndexOf(requestURL, "/", 3) != -1) {
context.setLocalEntityId(requestURL.substring(0, org.apache.commons.lang.StringUtils.ordinalIndexOf(requestURL, "/", 3)).concat("/sp"));
} else {
context.setLocalEntityId(requestURL.concat("/sp"));
}
context.setLocalEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
}
}
在 MetadataGeneratorFilter 中设置 hostedSPName 不是一个好的解决方案,因为必须在同步块下设置 @Autowired MetadataManager 以确保多个请求不会覆盖值。 MetadataManager 仍然可以在我们无法确定其状态的过滤器外部使用。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这可以通过创建所有 SP 的联盟(在这种情况下是一个应用程序下的子域 运行)然后 select 在运行时输入适当的实体 ID 和元数据来完成。
我做了以下步骤来处理这个问题。因此,首先为联合创建一个元数据,并在一个联合中的这个应用程序上添加所有子域 (SP) 运行:
<?xml version="1.0" encoding="UTF-8"?>
<EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Name="https://example-federation.org/metadata/example-federation-name.xml">
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" ID="org_example_shagunakarsh_saml_poc_sp1" entityID="org:example:shagunakarsh:saml:poc:sp1">
<!--other params-->
......
</md:EntityDescriptor>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="org_example_shagunakarsh_saml_poc_sp2" entityID="org:example:shagunakarsh:saml:poc:sp2">
<!--other params-->
......
</md:EntityDescriptor>
</EntitiesDescriptor>
现在我们需要在 security-applicationContext.xml:
中的 metadata bean 中指定此元数据文件
<beans:bean id="metadata"
class="org.springframework.security.saml.metadata.CachingMetadataManager">
....
<beans:constructor-arg value = "/path/to/metadata/federation-mymetadata.xml"></beans:constructor-arg>
(阅读更新 1,不建议为此目的覆盖过滤器)现在我们需要 select 在运行时使用访问的 URL 适当的元数据,这可以通过扩展 MetadataGeneratorFilter 和重写 processMetadataInitialization 函数:
@Override
protected void processMetadataInitialization(HttpServletRequest request) throws ServletException {
// In case the hosted SP metadata weren't initialized, let's do it now
if (manager.getHostedSPName() == null) {
synchronized (MetadataManager.class) {
//same code as Base Class
}
} else {
// if known SP is found from federation metadata
String requestURL = request.getRequestURL().toString();
String subDomain = requestURL.substring(requestURL.indexOf("//") + 2, requestURL.indexOf("."));
//set proper SP entityID
manager.setHostedSPName("org:example:shagunakarsh:saml:poc:" + subDomain);
}
}
然后使用此 CustomMetadataGeneratorFilter 更新安全性-applicationContext.xml:
<beans:bean id="metadataGeneratorFilter" class="org.springframework.security.saml.metadata.CustomMetadataGeneratorFilter">
另外不要忘记更新您的 IdP 中的元数据(本例为 Shibboleth IDPv3)。重新部署 IdP 和 SP,它应该可以工作。
处理 SP 发起的单点登录 (SSO),SP 和 IdP 都是自托管的,因此可以灵活地编辑两者。我正在使用 spring-security-saml2-core-1.0.1.RELEASE 来托管 spring 应用程序(spring-security-3.2.8,spring-mvc -3.2.14.RELEASE),在 url 上为多个租户提供服务说:
sp1.example.org,
sp2.example.org
IdP 使用 Shibboleth IdPv3.2.1 托管,它可以很好地处理托管在不同 SP 服务器上的多个应用程序。
我正在尝试从同一服务器为 sp1 和 sp2 发送不同的元数据。我通过覆盖 SAMLContextProviderImpl populatePeerEntityId
阅读了关于多租户 SP here and here 自定义逻辑的文章,同样我试图覆盖 populateLocalEntityId
因为我不能使用 alias
。
有人可以提供用于处理多租户元数据的覆盖 populateLocalEntityId
的示例代码吗?
SP配置如下图:
<!-- Filters for processing of SAML messages -->
<beans:bean id="samlFilter" class="org.springframework.security.web.FilterChainProxy">
<filter-chain-map request-matcher="ant">
<filter-chain pattern="/saml/login/**" filters="samlEntryPoint" />
<filter-chain pattern="/saml/logout/**" filters="samlLogoutFilter" />
<filter-chain pattern="/saml/metadata/**" filters="metadataDisplayFilter" />
<filter-chain pattern="/saml/SSO/**" filters="samlWebSSOProcessingFilter" />
<filter-chain pattern="/saml/SSOHoK/**" filters="samlWebSSOHoKProcessingFilter" />
<filter-chain pattern="/saml/SingleLogout/**" filters="samlLogoutProcessingFilter" />
<filter-chain pattern="/saml/discovery/**" filters="samlIDPDiscovery" />
</filter-chain-map>
</beans:bean>
<!-- Handler deciding where to redirect user after successful login -->
<beans:bean id="successRedirectHandler" class="com.example.web.sso.CustomAuthenticationSuccessHandler" ></beans:bean>
<!-- <beans:bean id="successRedirectHandler"
class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/WEB-INF/security/idpSelection.jsp"/>
</beans:bean> -->
<!-- Use the following for interpreting RelayState coming from unsolicited
response as redirect URL:
<beans:bean id="successRedirectHandler" class="org.springframework.security.saml.SAMLRelayStateSuccessHandler">
<property name="defaultTargetUrl" value="/" /> </beans:bean> -->
<!-- Handler deciding where to redirect user after failed login -->
<beans:bean id="failureRedirectHandler" class="com.example.web.sso.CustomAuthenticationFailureHandler"></beans:bean>
<!-- <beans:bean id="failureRedirectHandler"
class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<property name="useForward" value="true" />
<property name="defaultFailureUrl" value="/error.jsp" />
</beans:bean> -->
<!-- Handler for successful logout -->
<beans:bean id="successLogoutHandler"
class="com.example.web.sso.CustomLogoutSuccessHandler" ></beans:bean>
<!-- <beans:bean id="successLogoutHandler"
class="org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler">
<property name="defaultTargetUrl" value="/logout.jsp" />
</beans:bean> -->
<authentication-manager alias="samlauthenticationManager">
<!-- Register authentication manager for SAML provider -->
<authentication-provider ref="samlAuthenticationProvider" />
<!-- Register authentication manager for administration UI -->
<authentication-provider>
<user-service id="adminInterfaceService">
<user name="admin" password="admin" authorities="ROLE_ADMIN" />
</user-service>
</authentication-provider>
</authentication-manager>
<!-- Logger for SAML messages and events -->
<beans:bean id="samlLogger" class="org.springframework.security.saml.log.SAMLDefaultLogger" >
<beans:property name="logMessages" value="true" />
<beans:property name="logErrors" value="true" />
</beans:bean>
<!-- Central storage of cryptographic keys -->
<beans:bean id="keyManager" class="org.springframework.security.saml.key.JKSKeyManager">
<beans:constructor-arg value="/WEB-INF/keys/samlKeystore.jks"></beans:constructor-arg>
<beans:constructor-arg type="java.lang.String" value="nalle123" />
<beans:constructor-arg>
<beans:map>
<beans:entry key="apollo" value="nalle123" />
</beans:map>
</beans:constructor-arg>
<beans:constructor-arg type="java.lang.String" value="apollo" />
</beans:bean>
<!-- Entry point to initialize authentication, default values taken from
properties file -->
<beans:bean id="samlEntryPoint" class="com.example.web.sso.CustomSAMLEntryPoint">
<beans:property name="defaultProfileOptions">
<beans:bean class="org.springframework.security.saml.websso.WebSSOProfileOptions">
<beans:property name="binding" value="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"/>
<beans:property name="nameID" value="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" />
<beans:property name="includeScoping" value="false" />
<beans:property name="forceAuthN" value="false" />
</beans:bean>
</beans:property>
</beans:bean>
<!-- IDP Discovery Service -->
<beans:bean id="samlIDPDiscovery" class="org.springframework.security.saml.SAMLDiscovery">
<beans:property name="idpSelectionPath" value="/WEB-INF/security/idpSelection.jsp" />
</beans:bean>
<!-- Filter automatically generates default SP metadata -->
<beans:bean id="metadataGeneratorFilter"
class="org.springframework.security.saml.metadata.MetadataGeneratorFilter">
<beans:constructor-arg>
<beans:bean class="org.springframework.security.saml.metadata.MetadataGenerator">
<beans:property name="entityId" value="com:example:namespaceId:saml:poc" />
<!-- <beans:property name="entityBaseURL" value="https://sp1.example.com:8080/" /> -->
<beans:property name="requestSigned" value="true" />
<beans:property name="wantAssertionSigned" value="true" />
<beans:property name="extendedMetadata">
<beans:bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
<beans:property name="idpDiscoveryEnabled" value="true" />
</beans:bean>
</beans:property>
</beans:bean>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="metadataGenerator" class="org.springframework.security.saml.metadata.MetadataGenerator">
<beans:property name="entityId" value="com:example:namespaceId:saml:poc" />
<beans:property name="entityBaseURL" value="https://sp1.example.com:8080/" />
<beans:property name="requestSigned" value="true" />
<beans:property name="wantAssertionSigned" value="true" />
<beans:property name="extendedMetadata">
<beans:bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
<beans:property name="idpDiscoveryEnabled" value="true" />
</beans:bean>
</beans:property>
</beans:bean>
<!-- The filter is waiting for connections on URL suffixed with filterSuffix
and presents SP metadata there -->
<beans:bean id="metadataDisplayFilter"
class="org.springframework.security.saml.metadata.MetadataDisplayFilter" />
<!-- Configure HTTP Client to accept certificates from the keystore for
HTTPS verification -->
<!-- <beans:bean class="org.springframework.security.saml.trust.httpclient.TLSProtocolConfigurer">
<beans:property name="sslHostnameVerification" value="default"/> </beans:bean> -->
<!-- IDP Metadata configuration - paths to metadata of IDPs in circle of
trust is here -->
<beans:bean id="metadata"
class="org.springframework.security.saml.metadata.CachingMetadataManager">
<beans:constructor-arg>
<beans:list>
<!-- Example of classpath metadata with Extended Metadata -->
<beans:bean
class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
<beans:constructor-arg>
<beans:bean
class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
<beans:constructor-arg>
<beans:bean class="java.util.Timer" />
</beans:constructor-arg>
<beans:constructor-arg>
<beans:bean class="org.opensaml.util.resource.ClasspathResource">
<!-- <beans:bean class="org.opensaml.util.resource.FilesystemResource"> -->
<beans:constructor-arg value = "/WEB-INF/metadata/sp1-mymetadata.xml"></beans:constructor-arg>
</beans:bean>
</beans:constructor-arg>
<beans:property name="parserPool" ref="parserPool" />
</beans:bean>
</beans:constructor-arg>
<beans:constructor-arg>
<beans:bean
class="org.springframework.security.saml.metadata.ExtendedMetadata">
<beans:property name="local" value="true" />
<beans:property name="securityProfile" value="metaiop" />
<beans:property name="sslSecurityProfile" value="pkix" />
<beans:property name="sslHostnameVerification" value="default" />
<!-- <beans:property name="sslHostnameVerification" value="allowAll" /> -->
<beans:property name="signMetadata" value="false" />
<beans:property name="signingKey" value="apollo" />
<beans:property name="encryptionKey" value="apollo" />
<beans:property name="requireArtifactResolveSigned" value="false" />
<beans:property name="requireLogoutRequestSigned" value="false" />
<beans:property name="requireLogoutResponseSigned" value="false" />
<beans:property name="idpDiscoveryEnabled" value="false" />
<beans:property name="idpDiscoveryURL" value="https://sp1.example.com/saml/discovery" />
<beans:property name="idpDiscoveryResponseURL" value="https://sp1.example.com/saml/login?disco=true" />
</beans:bean>
</beans:constructor-arg>
</beans:bean>
<!-- Example of HTTP metadata without Extended Metadata -->
<!-- <beans:bean class="org.opensaml.saml2.metadata.provider.HTTPMetadataProvider">
URL containing the metadata
<beans:constructor-arg>
<beans:value type="java.lang.String">https://idp.ssocircle.com/idp-meta.xml</beans:value>
<beans:value type="java.lang.String">https://sp1.example.com/idp-meta.xml</beans:value>
</beans:constructor-arg>
Timeout for metadata loading in ms
<beans:constructor-arg>
<beans:value type="int">15000</beans:value>
</beans:constructor-arg>
<beans:property name="parserPool" ref="parserPool" />
</beans:bean> -->
<beans:bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
<beans:constructor-arg>
<beans:bean class="org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider">
<beans:constructor-arg>
<beans:value type="java.io.File">/shared/saml/idp-metadata-exampleIdp.xml</beans:value>
</beans:constructor-arg>
<beans:property name="parserPool" ref="parserPool"/>
</beans:bean>
</beans:constructor-arg>
<beans:constructor-arg>
<beans:bean class="org.springframework.security.saml.metadata.ExtendedMetadata"/>
</beans:constructor-arg>
</beans:bean>
<!-- Example of file system metadata without Extended Metadata -->
<!-- <bean class="org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider">
<constructor-arg> <value type="java.io.File">/usr/local/metadata/idp.xml</value>
</constructor-arg> <property name="parserPool" ref="parserPool"/> </bean> -->
</beans:list>
</beans:constructor-arg>
<!-- OPTIONAL used when one of the metadata files contains information
about this service provider -->
<!-- <property name="hostedSPName" value=""/> -->
<!-- OPTIONAL property: can tell the system which IDP should be used for
authenticating user by default. -->
<!-- <property name="defaultIDP" value="http://localhost:8080/opensso"/> -->
<beans:property name="defaultIDP" value="https://login.example.com/idp/shibboleth"/>
</beans:bean>
<!-- SAML Authentication Provider responsible for validating of received
SAML messages -->
<beans:bean id="samlAuthenticationProvider"
class="org.springframework.security.saml.SAMLAuthenticationProvider">
<!-- OPTIONAL property: can be used to store/load user data after login -->
<beans:property name="userDetails" ref="sAMLUserDetailsServiceImpl" />
<beans:property name="forcePrincipalAsString" value="false" />
</beans:bean>
<beans:bean id="sAMLUserDetailsServiceImpl"
class="com.example.service.impl.SAMLUserDetailsServiceImpl"></beans:bean>
<!-- Provider of default SAML Context -->
<!-- <beans:bean id="contextProvider"
class="org.springframework.security.saml.context.SAMLContextProviderImpl"> -->
<beans:bean id="contextProvider"
class="com.example.service.impl.CustomSAMLContextProviderImpl">
<beans:property name="storageFactory">
<!-- <beans:bean class="org.springframework.security.saml.storage.EmptyStorageFactory" /> -->
<beans:bean class="org.springframework.security.saml.storage.HttpSessionStorageFactory" />
</beans:property>
</beans:bean>
<!-- <beans:bean id="contextProvider"
class="org.springframework.security.saml.context.SAMLContextProviderLB">
<beans:property name="scheme" value="https" />
<beans:property name="serverName" value="https://sp1.example.com" />
<beans:property name="serverPort" value="443" />
<beans:property name="includeServerPortInRequestURL" value="false" />
</beans:bean> -->
<!-- Processing filter for WebSSO profile messages -->
<beans:bean id="samlWebSSOProcessingFilter" class="org.springframework.security.saml.SAMLProcessingFilter">
<beans:property name="authenticationManager" ref="samlauthenticationManager" />
<beans:property name="authenticationSuccessHandler" ref="successRedirectHandler" />
<beans:property name="authenticationFailureHandler" ref="failureRedirectHandler" />
<beans:property name="sessionAuthenticationStrategy" ref="sas"/>
</beans:bean>
<!-- Processing filter for WebSSO Holder-of-Key profile -->
<beans:bean id="samlWebSSOHoKProcessingFilter"
class="org.springframework.security.saml.SAMLWebSSOHoKProcessingFilter">
<beans:property name="authenticationManager" ref="samlauthenticationManager" />
<beans:property name="authenticationSuccessHandler" ref="successRedirectHandler" />
<beans:property name="authenticationFailureHandler" ref="failureRedirectHandler" />
</beans:bean>
<!-- Logout handler terminating local session -->
<beans:bean id="logoutHandler"
class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler">
<beans:property name="invalidateHttpSession" value="true" />
</beans:bean>
<!-- Override default logout processing filter with the one processing SAML
messages -->
<beans:bean id="samlLogoutFilter" class="org.springframework.security.saml.SAMLLogoutFilter">
<beans:constructor-arg index="0" ref="successLogoutHandler" />
<beans:constructor-arg index="1" ref="logoutHandler" />
<beans:constructor-arg index="2" ref="logoutHandler" />
</beans:bean>
<!-- Filter processing incoming logout messages -->
<!-- First argument determines URL user will be redirected to after successful
global logout -->
<beans:bean id="samlLogoutProcessingFilter"
class="org.springframework.security.saml.SAMLLogoutProcessingFilter">
<beans:constructor-arg index="0" ref="successLogoutHandler" />
<beans:constructor-arg index="1" ref="logoutHandler" />
</beans:bean>
<!-- Class loading incoming SAML messages from httpRequest stream -->
<beans:bean id="processor"
class="org.springframework.security.saml.processor.SAMLProcessorImpl">
<beans:constructor-arg>
<beans:list>
<beans:ref bean="postBinding" />
<beans:ref bean="redirectBinding" />
<beans:ref bean="artifactBinding" />
<beans:ref bean="soapBinding" />
<beans:ref bean="paosBinding" />
</beans:list>
</beans:constructor-arg>
</beans:bean>
<!-- SAML 2.0 WebSSO Assertion Consumer -->
<beans:bean id="webSSOprofileConsumer"
class="org.springframework.security.saml.websso.WebSSOProfileConsumerImpl" >
<!-- maximum lifetime of assertion issued by Idp default 3000-->
<!-- <beans:property name="maxAssertionTime" value="300"></beans:property> -->
<!-- maximum lifetime of authentication issued default 7200-->
<!-- <beans:property name="maxAssertionTime" value="300"></beans:property> -->
</beans:bean>
<!-- SAML 2.0 Holder-of-Key WebSSO Assertion Consumer -->
<beans:bean id="hokWebSSOprofileConsumer"
class="org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl" />
<!-- SAML 2.0 Web SSO profile -->
<beans:bean id="webSSOprofile"
class="org.springframework.security.saml.websso.WebSSOProfileImpl" />
<!-- SAML 2.0 Holder-of-Key Web SSO profile -->
<beans:bean id="hokWebSSOProfile"
class="org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl" />
<!-- SAML 2.0 ECP profile -->
<beans:bean id="ecpprofile"
class="org.springframework.security.saml.websso.WebSSOProfileECPImpl" />
<!-- SAML 2.0 Logout Profile -->
<beans:bean id="logoutprofile"
class="org.springframework.security.saml.websso.SingleLogoutProfileImpl" />
<!-- Bindings, encoders and decoders used for creating and parsing messages -->
<beans:bean id="postBinding"
class="org.springframework.security.saml.processor.HTTPPostBinding">
<beans:constructor-arg ref="parserPool" />
<beans:constructor-arg ref="velocityEngine" />
</beans:bean>
<beans:bean id="redirectBinding"
class="org.springframework.security.saml.processor.HTTPRedirectDeflateBinding">
<beans:constructor-arg ref="parserPool" />
</beans:bean>
<beans:bean id="artifactBinding"
class="org.springframework.security.saml.processor.HTTPArtifactBinding">
<beans:constructor-arg ref="parserPool" />
<beans:constructor-arg ref="velocityEngine" />
<beans:constructor-arg>
<beans:bean
class="org.springframework.security.saml.websso.ArtifactResolutionProfileImpl">
<beans:constructor-arg>
<beans:bean class="org.apache.commons.httpclient.HttpClient">
<beans:constructor-arg>
<beans:bean
class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager" />
</beans:constructor-arg>
</beans:bean>
</beans:constructor-arg>
<beans:property name="processor">
<beans:bean
class="org.springframework.security.saml.processor.SAMLProcessorImpl">
<beans:constructor-arg ref="soapBinding" />
</beans:bean>
</beans:property>
</beans:bean>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="soapBinding"
class="org.springframework.security.saml.processor.HTTPSOAP11Binding">
<beans:constructor-arg ref="parserPool" />
</beans:bean>
<beans:bean id="paosBinding"
class="org.springframework.security.saml.processor.HTTPPAOS11Binding">
<beans:constructor-arg ref="parserPool" />
</beans:bean>
<!-- Initialization of OpenSAML library -->
<beans:bean class="org.springframework.security.saml.SAMLBootstrap" />
<!-- Initialization of the velocity engine -->
<beans:bean id="velocityEngine" class="org.springframework.security.saml.util.VelocityFactory"
factory-method="getEngine" />
<!-- XML parser pool needed for OpenSAML parsing -->
<beans:bean id="parserPool" class="org.opensaml.xml.parse.StaticBasicParserPool"
init-method="initialize">
<beans:property name="builderFeatures">
<beans:map>
<beans:entry key="http://apache.org/xml/features/dom/defer-node-expansion"
value="false" />
</beans:map>
</beans:property>
</beans:bean>
<beans:bean id="parserPoolHolder"
class="org.springframework.security.saml.parser.ParserPoolHolder" ></beans:bean>
更新 1:更好的解决方案是扩展 SAMLContextProviderImpl 并覆盖 populateLocalEntityId、getLocalEntity、getLocalAndPeerEntity 以设置正确的 SAMLMessageContext,因为会为每个请求创建新的 SAMLMessageContext。
@Override
public SAMLMessageContext getLocalAndPeerEntity(HttpServletRequest request, HttpServletResponse response) throws MetadataProviderException {
SAMLMessageContext context = new SAMLMessageContext();
populateGenericContext(request, response, context);
//changed to send URL instead of URI
populateLocalEntityId(context, request.getRequestURL().toString());
populateLocalContext(context);
populatePeerEntityId(context);
populatePeerContext(context);
return context;
}
@Override
public SAMLMessageContext getLocalEntity(HttpServletRequest request, HttpServletResponse response) throws MetadataProviderException {
SAMLMessageContext context = new SAMLMessageContext();
populateGenericContext(request, response, context);
populateLocalEntityId(context, request.getRequestURL().toString());
populateLocalContext(context);
return context;
}
@Override
protected void populateLocalEntityId(SAMLMessageContext context, String requestURL) throws MetadataProviderException {
String entityId;
HTTPInTransport inTransport = (HTTPInTransport) context.getInboundMessageTransport();
// Pre-configured entity Id
entityId = (String) inTransport.getAttribute(org.springframework.security.saml.SAMLConstants.LOCAL_ENTITY_ID);
if (entityId != null) {
// same code as super class
} else { // Defaults
//Now setting proper entityId as required
//in this case https://sp1.wooqer.com/sp
if(org.apache.commons.lang.StringUtils.ordinalIndexOf(requestURL, "/", 3) != -1) {
context.setLocalEntityId(requestURL.substring(0, org.apache.commons.lang.StringUtils.ordinalIndexOf(requestURL, "/", 3)).concat("/sp"));
} else {
context.setLocalEntityId(requestURL.concat("/sp"));
}
context.setLocalEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
}
}
在 MetadataGeneratorFilter 中设置 hostedSPName 不是一个好的解决方案,因为必须在同步块下设置 @Autowired MetadataManager 以确保多个请求不会覆盖值。 MetadataManager 仍然可以在我们无法确定其状态的过滤器外部使用。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这可以通过创建所有 SP 的联盟(在这种情况下是一个应用程序下的子域 运行)然后 select 在运行时输入适当的实体 ID 和元数据来完成。
我做了以下步骤来处理这个问题。因此,首先为联合创建一个元数据,并在一个联合中的这个应用程序上添加所有子域 (SP) 运行:
<?xml version="1.0" encoding="UTF-8"?>
<EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Name="https://example-federation.org/metadata/example-federation-name.xml">
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" ID="org_example_shagunakarsh_saml_poc_sp1" entityID="org:example:shagunakarsh:saml:poc:sp1">
<!--other params-->
......
</md:EntityDescriptor>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="org_example_shagunakarsh_saml_poc_sp2" entityID="org:example:shagunakarsh:saml:poc:sp2">
<!--other params-->
......
</md:EntityDescriptor>
</EntitiesDescriptor>
现在我们需要在 security-applicationContext.xml:
中的 metadata bean 中指定此元数据文件<beans:bean id="metadata"
class="org.springframework.security.saml.metadata.CachingMetadataManager">
....
<beans:constructor-arg value = "/path/to/metadata/federation-mymetadata.xml"></beans:constructor-arg>
(阅读更新 1,不建议为此目的覆盖过滤器)现在我们需要 select 在运行时使用访问的 URL 适当的元数据,这可以通过扩展 MetadataGeneratorFilter 和重写 processMetadataInitialization 函数:
@Override
protected void processMetadataInitialization(HttpServletRequest request) throws ServletException {
// In case the hosted SP metadata weren't initialized, let's do it now
if (manager.getHostedSPName() == null) {
synchronized (MetadataManager.class) {
//same code as Base Class
}
} else {
// if known SP is found from federation metadata
String requestURL = request.getRequestURL().toString();
String subDomain = requestURL.substring(requestURL.indexOf("//") + 2, requestURL.indexOf("."));
//set proper SP entityID
manager.setHostedSPName("org:example:shagunakarsh:saml:poc:" + subDomain);
}
}
然后使用此 CustomMetadataGeneratorFilter 更新安全性-applicationContext.xml:
<beans:bean id="metadataGeneratorFilter" class="org.springframework.security.saml.metadata.CustomMetadataGeneratorFilter">
另外不要忘记更新您的 IdP 中的元数据(本例为 Shibboleth IDPv3)。重新部署 IdP 和 SP,它应该可以工作。