C#调用Workday人力资源Web服务V38.1认证失败问题咨询
异常原因
Workday的SOAP服务根本不认HTTP传输层的认证信息,你之前写的_proxy.Credentials = new NetworkCredential(...)是把用户名密码塞到HTTP请求的Authorization头里,Workday服务端根本不会读这个位置的凭据,它要求认证信息必须放在SOAP信封内部的WS-Security(UsernameToken)头里传递。
你在SOAP UI里能调用成功,是因为SOAP UI在你填了认证信息之后,自动帮你把符合规范的WS-Security头插到SOAP请求里了。而WSDL.exe生成的代理类默认没有自动生成、注入这个安全头的逻辑,服务端收到的请求没有有效认证凭据,自然直接返回用户名密码错误,哪怕你账号密码本身是对的。
另外还有个高频踩坑点:Workday的认证用户名不是单纯的账号名,必须带租户前缀,格式是租户ID\账号名,少了租户前缀也会报完全相同的错误。
修复方法
- 不要修改WSDL.exe生成的原始cs文件,新建一个同命名空间的分部类,给
HumanResourcesService扩展WS-Security头注入逻辑:
using System; using System.IO; using System.Net; using System.Text; using System.Web.Services.Protocols; using System.Xml; public partial class HumanResourcesService { private string _authUsername; private string _authPassword; /// <summary> /// 初始化Workday人力资源服务客户端 /// </summary> /// <param name="username">集成账号名,不需要带租户前缀</param> /// <param name="password">账号密码</param> /// <param name="tenantId">Workday租户ID</param> /// <param name="endpointUrl">V38.1版本对应的服务端点地址</param> public HumanResourcesService(string username, string password, string tenantId, string endpointUrl) { _authUsername = $"{tenantId}\\{username}"; _authPassword = password; this.Url = endpointUrl; this.SoapVersion = SoapProtocolVersion.Soap11; this.RequestEncoding = Encoding.UTF8; this.EnableDecompression = true; } protected override WebRequest GetWebRequest(Uri uri) { var req = (HttpWebRequest)base.GetWebRequest(uri); // Workday部分版本要求SOAPAction头为空,避免400错误 req.Headers.Add("SOAPAction", string.Empty); return req; } protected override void WriteSoapMessage(SoapClientMessage message, Stream stream) { // 构造符合WS-Security 1.0规范的UsernameToken头,密码使用PasswordText格式(Workday强制要求HTTPS传输,不需要加密密码) const string wsseNs = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"; const string wsuNs = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"; const string soapEnvNs = "http://schemas.xmlsoap.org/soap/envelope/"; const string pwdType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"; var securityXml = new XmlDocument(); var securityNode = securityXml.CreateElement("wsse", "Security", wsseNs); securityNode.SetAttribute("mustUnderstand", soapEnvNs, "1"); var tokenNode = securityXml.CreateElement("wsse", "UsernameToken", wsseNs); tokenNode.SetAttribute("Id", wsuNs, "UsernameToken-" + Guid.NewGuid().ToString("N")); var usernameNode = securityXml.CreateElement("wsse", "Username", wsseNs); usernameNode.InnerText = _authUsername; tokenNode.AppendChild(usernameNode); var pwdNode = securityXml.CreateElement("wsse", "Password", wsseNs); pwdNode.SetAttribute("Type", pwdType); pwdNode.InnerText = _authPassword; tokenNode.AppendChild(pwdNode); securityNode.AppendChild(tokenNode); message.Headers.Add(new WorkdaySecurityHeader(securityNode)); base.WriteSoapMessage(message, stream); } } /// <summary> /// 自定义WS-Security请求头实现 /// </summary> internal class WorkdaySecurityHeader : SoapHeader { private readonly XmlElement _securityContent; public WorkdaySecurityHeader(XmlElement content) { _securityContent = content; MustUnderstand = true; } public override XmlElement GetXml(XmlDocument document) { return (XmlElement)document.ImportNode(_securityContent, true); } }
- 替换原来的代理初始化逻辑,删掉之前设置HTTP层Credentials的无效代码:
// 替换成你实际的租户ID、账号、密码、V38.1端点地址 _proxy = new HumanResourcesService( WorkdayHRWebservicesUsername, WorkdayHRWebservicesPassword, WorkdayTenantId, "https://你的租户域名.workday.com/ccx/service/你的租户ID/Human_Resources/v38.1" ); // 不需要设置UseDefaultCredentials、Credentials、PreAuthenticate这些HTTP认证相关的属性,设置了也不生效
- 调用前做以下校验,避免其他配置问题:
- 确认使用的集成账号已经在Workday后台绑定了对应安全组,拥有Human_Resources服务下Get_Worker_Profile接口的访问权限
- 确认端点地址和你SOAP UI里用的完全一致,不要混用其他版本(比如V40/V39)的服务地址
- 确认租户ID拼写正确,用户名的租户前缀和租户ID完全匹配
内容的提问来源于stack exchange,提问作者Øyvind
相关产品推荐
相关产品推荐

