spring-boot - 使用 Azure AD 的 Spring Boot SAML
问题描述
我正在尝试使用 Spring Boot 中的 Microsoft Azure 实现 SSO。该应用程序使用存储在服务器上的本地元数据(它部署在具有自定义客户端域的 CentOS 虚拟机中,用于部署战争的服务器是 Tomcat 9)。
实际问题:在第一次登录(成功)和几次刷新或关闭浏览器后,它崩溃并出现两个不同的错误: 成功登录后的第一个错误和 第二个错误
下面你可以看到我在安全包中的 SAML 实现。所有字段都作为我的application.properties中的值添加
我的 SAML 配置类:
@Configuration
public class SamlSecurityConfig {
private final Logger log = LoggerFactory.getLogger(SamlSecurityConfig.class);
@Value("${saml.keystore.location}")
private String samlKeystoreLocation;
@Value("${saml.keystore.password}")
private String samlKeystorePassword;
@Value("${saml.keystore.alias}")
private String samlKeystoreAlias;
@Value("${saml.idp}")
private String defaultIdp;
@Value("${saml.metadata.location}")
private String metadataLocation;
@Bean
public EmptyStorageFactory emptyStorageFactory() {
return new EmptyStorageFactory();
}
@Bean(initMethod = "initialize")
public StaticBasicParserPool parserPool() {
return new StaticBasicParserPool();
}
@Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
return new CustomSAMLAuthenticationProvider();
}
@Bean
public SAMLContextProvider contextProvider() {
SAMLContextProviderImpl contextProviderImpl = new SAMLContextProviderImpl();
contextProviderImpl.setStorageFactory(emptyStorageFactory());
return contextProviderImpl;
}
@Bean
public static SAMLBootstrap samlBootstrap() {
return new SAMLBootstrap();
}
@Bean
public SAMLDefaultLogger samlLogger() {
return new SAMLDefaultLogger();
}
@Bean
public WebSSOProfileConsumer webSSOprofileConsumer() {
WebSSOProfileConsumerImpl consumerImpl = new WebSSOProfileConsumerImpl();
consumerImpl.setMaxAuthenticationAge(3600);
return consumerImpl;
}
@Bean
@Qualifier("hokWebSSOprofileConsumer")
public WebSSOProfileConsumerHoKImpl hokWebSSOProfileConsumer() {
return new WebSSOProfileConsumerHoKImpl();
}
@Bean
public WebSSOProfile webSSOprofile() {
return new WebSSOProfileImpl();
}
@Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
return new WebSSOProfileConsumerHoKImpl();
}
@Bean
public WebSSOProfileECPImpl ecpProfile() {
return new WebSSOProfileECPImpl();
}
@Bean
public SingleLogoutProfile logoutProfile() {
return new SingleLogoutProfileImpl();
}
@Bean
public KeyManager keyManager() {
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader.getResource(samlKeystoreLocation);
Map<String, String> passwords = new HashMap<>();
passwords.put(samlKeystoreAlias, samlKeystorePassword);
return new JKSKeyManager(storeFile, samlKeystorePassword, passwords, samlKeystoreAlias);
}
@Bean
public WebSSOProfileOptions defaultWebSSOProfileOptions() {
WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
webSSOProfileOptions.setIncludeScoping(false);
return webSSOProfileOptions;
}
@Bean
public SAMLEntryPoint samlEntryPoint() {
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
return samlEntryPoint;
}
@Bean
public ExtendedMetadata extendedMetadata() {
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
extendedMetadata.setIdpDiscoveryEnabled(false);
extendedMetadata.setSignMetadata(false);
return extendedMetadata;
}
@Bean
@Qualifier("okta")
public ExtendedMetadataDelegate oktaExtendedMetadataProvider() throws MetadataProviderException {
File metadata = null;
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader.getResource(metadataLocation);
try {
metadata = new File(storeFile.getFile(), "sso.xml");
log.info("The XML was parsed successful!");
} catch (Exception e) {
e.printStackTrace();
log.error("Error on parsing the XML file!");
}
FilesystemMetadataProvider provider = new FilesystemMetadataProvider(metadata);
provider.setParserPool(parserPool());
ExtendedMetadataDelegate emd = new ExtendedMetadataDelegate(provider, extendedMetadata());
emd.setMetadataTrustCheck(false);
return emd;
}
@Bean
@Qualifier("metadata")
public CachingMetadataManager metadata() throws MetadataProviderException, ResourceException {
List<MetadataProvider> providers = new ArrayList<>();
providers.add(oktaExtendedMetadataProvider());
CachingMetadataManager metadataManager = new CachingMetadataManager(providers);
metadataManager.setDefaultIDP(defaultIdp);
return metadataManager;
}
@Bean
@Qualifier("saml")
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successRedirectHandler.setDefaultTargetUrl("/");
return successRedirectHandler;
}
@Bean
@Qualifier("saml")
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
failureHandler.setUseForward(true);
failureHandler.setDefaultFailureUrl("/error");
return failureHandler;
}
@Bean
public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
successLogoutHandler.setDefaultTargetUrl("/");
return successLogoutHandler;
}
@Bean
public SecurityContextLogoutHandler logoutHandler() {
SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
logoutHandler.setInvalidateHttpSession(true);
logoutHandler.setClearAuthentication(true);
return logoutHandler;
}
@Bean
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
return new SAMLLogoutProcessingFilter(successLogoutHandler(), logoutHandler());
}
@Bean
public SAMLLogoutFilter samlLogoutFilter() {
return new SAMLLogoutFilter(successLogoutHandler(), new LogoutHandler[] { logoutHandler() },
new LogoutHandler[] { logoutHandler() });
}
@Bean
public HTTPPostBinding httpPostBinding() {
return new HTTPPostBinding(parserPool(), VelocityFactory.getEngine());
}
@Bean
public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() {
return new HTTPRedirectDeflateBinding(parserPool());
}
@Bean
public SAMLProcessorImpl processor() {
ArrayList<SAMLBinding> bindings = new ArrayList<>();
bindings.add(httpRedirectDeflateBinding());
bindings.add(httpPostBinding());
return new SAMLProcessorImpl(bindings);
}
}
网络安全配置:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${saml.sp}")
private String samlAudience;
@Value("${saml.idp}")
private String defaultIdp;
@Autowired
@Qualifier("saml")
private SavedRequestAwareAuthenticationSuccessHandler samlAuthSuccessHandler;
@Autowired
@Qualifier("saml")
private SimpleUrlAuthenticationFailureHandler samlAuthFailureHandler;
@Value("${saml.metadata.location}")
private String metadataLocation;
@Autowired
private SAMLEntryPoint samlEntryPoint;
@Autowired
private SAMLLogoutFilter samlLogoutFilter;
@Autowired
private SAMLLogoutProcessingFilter samlLogoutProcessingFilter;
@Bean
public SAMLDiscovery samlDiscovery() {
SAMLDiscovery idpDiscovery = new SAMLDiscovery();
return idpDiscovery;
}
@Autowired
private SAMLAuthenticationProvider samlAuthenticationProvider;
@Autowired
private ExtendedMetadata extendedMetadata;
@Autowired
private KeyManager keyManager;
public MetadataGenerator metadataGenerator() {
MetadataGenerator metadataGenerator = new MetadataGenerator();
metadataGenerator.setEntityId(samlAudience);
metadataGenerator.setExtendedMetadata(extendedMetadata);
metadataGenerator.setIncludeDiscoveryExtension(true);
metadataGenerator.setKeyManager(keyManager);
// metadataGenerator.setEntityBaseURL(samlAudience);
return metadataGenerator;
}
@Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(samlAuthSuccessHandler);
samlWebSSOProcessingFilter.setAuthenticationFailureHandler(samlAuthFailureHandler);
return samlWebSSOProcessingFilter;
}
@Bean
public FilterChainProxy samlFilter() throws Exception {
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
samlWebSSOProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"), samlDiscovery()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"), samlEntryPoint));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"), samlLogoutFilter));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
samlLogoutProcessingFilter));
return new FilterChainProxy(chains);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public MetadataGeneratorFilter metadataGeneratorFilter() {
return new MetadataGeneratorFilter(metadataGenerator());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.httpBasic().authenticationEntryPoint(samlEntryPoint);
http.addFilterAfter(metadataGeneratorFilter(), ChannelProcessingFilter.class)
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(samlFilter(), CsrfFilter.class);
http.authorizeRequests().antMatchers("/users/getUsers").permitAll().antMatchers("/static/**").permitAll()
.antMatchers("/js/**").permitAll().antMatchers("/").permitAll().anyRequest().authenticated();
http.logout();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(samlAuthenticationProvider);
}
}
microsoft azure 提供的元数据文件:
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" ID="_e5dc3ca1-bdef-4e69-99f8-367d2645e58e" entityID="https://sts.windows.net/3b0e7247-e0d5-44bf-8ed1-d01b18d16ca2/">
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="#_e5dc3ca1-bdef-4e69-99f8-367d2645e58e">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>8jc/wHi+O7dzGLgvBIwVvuxZAPaTCpw9hC2Jr0eWnRA=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>Signature</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>Certificate</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
<RoleDescriptor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:fed="http://docs.oasis-open.org/wsfed/federation/200706" xsi:type="fed:SecurityTokenServiceType" protocolSupportEnumeration="http://docs.oasis-open.org/wsfed/federation/200706">
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>Certificate</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<fed:ClaimTypesOffered>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name">
<auth:DisplayName>Name</auth:DisplayName>
<auth:Description>The mutable display name of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier">
<auth:DisplayName>Subject</auth:DisplayName>
<auth:Description>An immutable, globally unique, non-reusable identifier of the user that is unique to the application for which a token is issued.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname">
<auth:DisplayName>Given Name</auth:DisplayName>
<auth:Description>First name of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname">
<auth:DisplayName>Surname</auth:DisplayName>
<auth:Description>Last name of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/identity/claims/displayname">
<auth:DisplayName>Display Name</auth:DisplayName>
<auth:Description>Display name of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/identity/claims/nickname">
<auth:DisplayName>Nick Name</auth:DisplayName>
<auth:Description>Nick name of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant">
<auth:DisplayName>Authentication Instant</auth:DisplayName>
<auth:Description>The time (UTC) when the user is authenticated to Windows Azure Active Directory.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod">
<auth:DisplayName>Authentication Method</auth:DisplayName>
<auth:Description>The method that Windows Azure Active Directory uses to authenticate users.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/identity/claims/objectidentifier">
<auth:DisplayName>ObjectIdentifier</auth:DisplayName>
<auth:Description>Primary identifier for the user in the directory. Immutable, globally unique, non-reusable.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/identity/claims/tenantid">
<auth:DisplayName>TenantId</auth:DisplayName>
<auth:Description>Identifier for the user's tenant.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/identity/claims/identityprovider">
<auth:DisplayName>IdentityProvider</auth:DisplayName>
<auth:Description>Identity provider for the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress">
<auth:DisplayName>Email</auth:DisplayName>
<auth:Description>Email address of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/groups">
<auth:DisplayName>Groups</auth:DisplayName>
<auth:Description>Groups of the user.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/identity/claims/accesstoken">
<auth:DisplayName>External Access Token</auth:DisplayName>
<auth:Description>Access token issued by external identity provider.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/expiration">
<auth:DisplayName>External Access Token Expiration</auth:DisplayName>
<auth:Description>UTC expiration time of access token issued by external identity provider.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/identity/claims/openid2_id">
<auth:DisplayName>External OpenID 2.0 Identifier</auth:DisplayName>
<auth:Description>OpenID 2.0 identifier issued by external identity provider.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/claims/groups.link">
<auth:DisplayName>GroupsOverageClaim</auth:DisplayName>
<auth:Description>Issued when number of user's group claims exceeds return limit.</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/role">
<auth:DisplayName>Role Claim</auth:DisplayName>
<auth:Description>Roles that the user or Service Principal is attached to</auth:Description>
</auth:ClaimType>
<auth:ClaimType xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706" Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/wids">
<auth:DisplayName>RoleTemplate Id Claim</auth:DisplayName>
<auth:Description>Role template id of the Built-in Directory Roles that the user is a member of</auth:Description>
</auth:ClaimType>
</fed:ClaimTypesOffered>
<fed:SecurityTokenServiceEndpoint>
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:Address>https://login.microsoftonline.com/3b0e7247-e0d5-44bf-8ed1-d01b18d16ca2/wsfed</wsa:Address>
</wsa:EndpointReference>
</fed:SecurityTokenServiceEndpoint>
<fed:PassiveRequestorEndpoint>
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:Address>https://login.microsoftonline.com/3b0e7247-e0d5-44bf-8ed1-d01b18d16ca2/wsfed</wsa:Address>
</wsa:EndpointReference>
</fed:PassiveRequestorEndpoint>
</RoleDescriptor>
<RoleDescriptor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:fed="http://docs.oasis-open.org/wsfed/federation/200706" xsi:type="fed:ApplicationServiceType" protocolSupportEnumeration="http://docs.oasis-open.org/wsfed/federation/200706">
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>Certificate</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<fed:TargetScopes>
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:Address>https://sts.windows.net/3b0e7247-e0d5-44bf-8ed1-d01b18d16ca2/</wsa:Address>
</wsa:EndpointReference>
</fed:TargetScopes>
<fed:ApplicationServiceEndpoint>
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:Address>https://login.microsoftonline.com/3b0e7247-e0d5-44bf-8ed1-d01b18d16ca2/wsfed</wsa:Address>
</wsa:EndpointReference>
</fed:ApplicationServiceEndpoint>
<fed:PassiveRequestorEndpoint>
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:Address>https://login.microsoftonline.com/3b0e7247-e0d5-44bf-8ed1-d01b18d16ca2/wsfed</wsa:Address>
</wsa:EndpointReference>
</fed:PassiveRequestorEndpoint>
</RoleDescriptor>
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>Certificate</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://login.microsoftonline.com/3b0e7247-e0d5-44bf-8ed1-d01b18d16ca2/saml2"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://login.microsoftonline.com/3b0e7247-e0d5-44bf-8ed1-d01b18d16ca2/saml2"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://login.microsoftonline.com/3b0e7247-e0d5-44bf-8ed1-d01b18d16ca2/saml2"/>
</IDPSSODescriptor>
</EntityDescriptor>
解决方案
错误:消息 SAML 协议消息未签名,跳过 XML 签名处理错误可能有不同的原因。
解决方案
Your AssertionConsumerServiceURL should be AssertionConsumerServiceURL="https://source.com/saml/SSO"
还请通过此解决方案,它也可能会有所帮助:https ://answers.sap.com/questions/12768674/saml-protocol-message-was-not-signed-skipping-xml-.html
推荐阅读
- c++ - 尽管一切看起来都很好,但值并没有交换
- sequelize.js - sequelize 中的批量删除和销毁有什么区别?
- adobe - Adobe Experience Manager - 用于无头消费的隐藏内容
- javascript - 仅接受十六进制输入
- java - 无法使用数据流访问记录类型数组列进行键值转换
- javascript - 正则表达式到特定字符串
- javascript - 如何仅在地图中执行所有等待之后才增加变量值,以便我可以执行承诺并行?
- python - Python 列表循环
- php - PHP / SQL:如果在表A和表B中更新后满足条件,则更新表C
- php - 如何从发送用户个人资料更改电子邮件中排除 WooCommerce 结帐?