You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何为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

火山引擎 最新活动