C#发送带摘要认证Header的SOAP请求失败,寻求排查帮助
兄弟,看你遇到的这个SOAP认证问题确实头疼,毕竟第一次碰WS-Security很容易踩坑。先帮你理清楚现状:
你需要调用第三方的SOAP服务,对方要求请求必须携带符合OASIS 2004标准的WS-Security Header,其中密码是通过Base64(SHA1(nonce + created + password))生成的摘要。你用SoapUI测试没问题,但自己写的C#代码调用时,一直收到错误:
A security error was encountered when verifying the message Caused by: An error was discovered processing the wsse:Security header.
结合你贴的代码,我发现几个可能的问题点,给你逐个分析并给出解决办法:
问题1:绑定配置冲突,安全模式和凭证类型不匹配
你当前的绑定配置同时设置了Transport.ClientCredentialType = Digest和Message.ClientCredentialType = UserName,这两种配置是冲突的。因为第三方要求的是消息层的WS-Security UsernameToken摘要认证,不是传输层的Digest认证。
修正绑定配置:
// 使用TransportWithMessageCredential模式(因为服务地址是HTTPS) BasicHttpBinding _binding = new BasicHttpBinding(BasicHttpSecurityMode.TransportWithMessageCredential); // 消息层凭证类型设为UserName,对应WS-Security的UsernameToken _binding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName; // 指定WS-Security版本为WSS10(匹配第三方用的OASIS 2004标准) var msgSecurity = _binding.Security.Message as BasicHttpMessageSecurity; msgSecurity.SecurityVersion = SecurityVersion.WSSecurity10; // 算法套件匹配第三方的SHA1要求 _binding.Security.Message.AlgorithmSuite = SecurityAlgorithmSuite.Basic128Sha1Rsa15; // 移除Transport层的Digest配置,不需要 // _binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Digest;
问题2:手动添加Header + WCF自动生成Header,导致重复或格式冲突
你代码里同时做了两件事:
- 通过
ClientCredentials.UserName设置了用户名密码,WCF会自动生成WS-Security Header - 手动调用
OperationContext.Current.OutgoingMessageHeaders.Add(GetHeader())添加自定义Header
这会导致请求里出现两个wsse:Security节点,或者手动生成的Header格式不符合第三方要求,直接触发认证错误。
解决办法:
删掉手动添加Header的代码,让WCF自动生成符合标准的WS-Security Header。如果WCF默认生成的格式和第三方要求有差异,再通过自定义扩展调整,不要手动拼接Header(很容易出格式错误)。
问题3:密码摘要的生成顺序不符合第三方要求
第三方明确要求密码摘要的公式是:PasswordDigest = Base64(SHA1(nonce + created + password))
但WCF默认的密码摘要生成顺序是SHA1(nonce + password + created),这会导致生成的摘要和第三方预期的不一致,直接认证失败。这也是很多人踩坑的点!
解决办法:自定义用户名安全令牌处理程序
写一个自定义的UserNameSecurityTokenHandler,重写密码摘要的生成逻辑,按照第三方要求的顺序拼接:
public class CustomUserNameDigestHandler : UserNameSecurityTokenHandler { protected override byte[] GeneratePasswordDigest(byte[] nonce, DateTime created, string password) { // 1. 将created转换为第三方要求的ISO8601 UTC格式(比如2021-01-27T12:04:33.664Z) string createdStr = created.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); // 2. 按顺序拼接:nonce字节 + created字符串字节 + 密码字符串字节(UTF-8编码) byte[] createdBytes = Encoding.UTF8.GetBytes(createdStr); byte[] passwordBytes = Encoding.UTF8.GetBytes(password); byte[] combinedBytes = new byte[nonce.Length + createdBytes.Length + passwordBytes.Length]; Buffer.BlockCopy(nonce, 0, combinedBytes, 0, nonce.Length); Buffer.BlockCopy(createdBytes, 0, combinedBytes, nonce.Length, createdBytes.Length); Buffer.BlockCopy(passwordBytes, 0, combinedBytes, nonce.Length + createdBytes.Length, passwordBytes.Length); // 3. 计算SHA1哈希并返回 using (SHA1 sha1 = SHA1.Create()) { return sha1.ComputeHash(combinedBytes); } } }
然后在客户端配置中启用这个自定义处理程序:
var client = new PartnersInteraction.partnersPortTypeClient(_binding, new EndpointAddress("https://urlhere.com")); // 添加自定义令牌处理程序 client.ClientCredentials.UserName.CustomUserNameSecurityTokenHandler = new CustomUserNameDigestHandler(); // 设置用户名和明文密码(WCF会用自定义逻辑生成摘要) client.ClientCredentials.UserName.UserName = UserName; client.ClientCredentials.UserName.Password = Password; // 保留日志行为,方便排查 client.Endpoint.EndpointBehaviors.Add(_soapLoggingBehavior); return await client.getUPIDAsync(UserName);
额外排查建议:对比SoapUI和WCF发送的请求
如果按照上面的配置还是失败,建议启用WCF的消息日志,把WCF发送的SOAP请求和SoapUI发送的请求做对比,重点检查:
wsse:Security节点的命名空间是否和第三方要求一致Nonce的Base64值、Created的时间格式PasswordDigest的Base64值是否和SoapUI生成的一致
通过对比就能快速定位到格式差异的地方。
备注:内容来源于stack exchange,提问作者svstnv

