如何为ONVIF DeviceClient添加UsernameToken身份认证?
在C# ONVIF客户端的SOAP信封中添加UsernameToken身份认证
我来帮你搞定这个问题!你遇到的核心问题是混淆了HTTP Digest认证和ONVIF要求的WS-Security UsernameToken认证,咱们一步步来解决:
先理清楚你的现状
你已经能成功获取摄像机系统时间,代码是这样的:
public bool Initialise(string cameraAddress, string userName, string password) { bool result = false; try { var messageElement = new TextMessageEncodingBindingElement() { MessageVersion = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.WSAddressing10) }; HttpTransportBindingElement httpBinding = new HttpTransportBindingElement() { AuthenticationScheme = AuthenticationSchemes.Digest }; CustomBinding bind = new CustomBinding(messageElement, httpBinding); System.Net.ServicePointManager.Expect100Continue = false; DeviceClient deviceClient = new DeviceClient(bind, new EndpointAddress($"http://{cameraAddress}/onvif/device_service")); var temps = deviceClient.GetSystemDateAndTime(); } catch (Exception ex) { ErrorMessage = ex.Message; } return result; }
抓包显示当前的SOAP信封只有基础结构,没有任何认证相关的头部字段。
你的需求是在SOAP信封头部添加WS-Security标准的UsernameToken,也就是包含用户名、密码(明文或摘要)的认证节点,这样才能访问需要权限的ONVIF接口。
之前你尝试用HttpDigest凭证配置,但这是HTTP层面的认证,和ONVIF要求的SOAP头部的UsernameToken完全不是一回事,所以才会没有效果。
解决方案:自定义SOAP消息检查器(最可靠的方式)
因为很多ONVIF设备对WCF原生的WS-Security配置兼容性一般,所以手动通过消息检查器注入UsernameToken是最稳妥的方案。
第一步:创建消息检查器类
这个类会在发送SOAP请求前,手动往头部添加UsernameToken节点:
public class UsernameTokenInspector : IClientMessageInspector { private readonly string _username; private readonly string _password; public UsernameTokenInspector(string username, string password) { _username = username; _password = password; } public object BeforeSendRequest(ref Message request, IClientChannel channel) { // 构建WS-Security头部的XML结构 var xmlDoc = new XmlDocument(); var securityRoot = xmlDoc.CreateElement( "wsse", "Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); // 创建UsernameToken节点 var tokenElement = xmlDoc.CreateElement( "wsse", "UsernameToken", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); tokenElement.SetAttribute( "wsu:Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", "UsernameToken-1"); // 添加用户名节点 var usernameNode = xmlDoc.CreateElement( "wsse", "Username", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); usernameNode.InnerText = _username; tokenElement.AppendChild(usernameNode); // 添加密码节点(这里用明文,如需摘要可以修改Type属性和密码值) var passwordNode = xmlDoc.CreateElement( "wsse", "Password", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); passwordNode.SetAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"); passwordNode.InnerText = _password; tokenElement.AppendChild(passwordNode); securityRoot.AppendChild(tokenElement); // 将XML转换为MessageHeader并添加到请求中 using var reader = new XmlNodeReader(securityRoot); var securityHeader = MessageHeader.CreateHeader( "Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", reader, true); request.Headers.Add(securityHeader); return null; } public void AfterReceiveReply(ref Message reply, object correlationState) { // 不需要处理回复,留空即可 } }
第二步:创建端点行为类
这个类用来把上面的消息检查器注册到WCF客户端的端点上:
public class UsernameTokenBehavior : IEndpointBehavior { private readonly string _username; private readonly string _password; public UsernameTokenBehavior(string username, string password) { _username = username; _password = password; } public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { clientRuntime.MessageInspectors.Add(new UsernameTokenInspector(_username, _password)); } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { } public void Validate(ServiceEndpoint endpoint) { } }
第三步:修改你的初始化代码
把HTTP认证改成匿名,然后添加我们的自定义行为:
public bool Initialise(string cameraAddress, string userName, string password) { bool result = false; try { var messageElement = new TextMessageEncodingBindingElement() { MessageVersion = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.WSAddressing10) }; HttpTransportBindingElement httpBinding = new HttpTransportBindingElement() { AuthenticationScheme = AuthenticationSchemes.Anonymous // 这里改为匿名,用SOAP头部认证 }; CustomBinding bind = new CustomBinding(messageElement, httpBinding); System.Net.ServicePointManager.Expect100Continue = false; DeviceClient deviceClient = new DeviceClient(bind, new EndpointAddress($"http://{cameraAddress}/onvif/device_service")); // 添加UsernameToken认证行为 deviceClient.Endpoint.Behaviors.Add(new UsernameTokenBehavior(userName, password)); var temps = deviceClient.GetSystemDateAndTime(); result = true; // 执行成功标记为true } catch (Exception ex) { ErrorMessage = ex.Message; } return result; }
可选方案:用WCF原生WS-Security配置
如果你的摄像机兼容WCF的WS-Security格式,也可以直接修改绑定配置:
var binding = new WSHttpBinding(SecurityMode.Message); binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName; binding.Security.Message.EstablishSecurityContext = false; binding.Security.Message.NegotiateServiceCredential = false; DeviceClient deviceClient = new DeviceClient(binding, new EndpointAddress($"http://{cameraAddress}/onvif/device_service")); deviceClient.ClientCredentials.UserName.UserName = userName; deviceClient.ClientCredentials.UserName.Password = password;
不过这种方式兼容性不如自定义检查器,建议优先用第一种方案。
内容的提问来源于stack exchange,提问作者LoukMouk




