📹 第八章 ONVIF框架代码案例应用
📄 一句话概括

鉴权分两半:生成期决定「结构体里有没有放令牌的地方」,运行期决定「往里填什么」。 这一节讲前半——它只需要在 typemap.dat 里加一行,但漏了这一行,后面一行代码都写不了。

📝 本节和前面几节的关系

上一节生成了 onvif.h,这一节要在它里面「留出鉴权的位置」。 注意这一节和 08.04 是两回事:08.04 讲运行期怎么算、怎么发, 这一节只讲「生成代码时怎么让它有这个字段」。 两者混起来是新手最常见的困惑。

一、生成的默认 Header 是空的

wsdl2h 默认生成的 struct SOAP_ENV__Header 里什么都没有—— 不是「有一个空字段」,是「一个字段都没有」。

阶段 SOAP_ENV__Header 长什么样 能鉴权吗
默认生成 struct SOAP_ENV__Header { }; 不能,没地方放
改 typemap 后 { struct wsse__Security *wsse__Security; } 能

这个差别看起来只是「多一个字段」,实际是「有没有入口」的区别: gSOAP 的鉴权函数(soap_wsse_add_UsernameTokenDigest)要往 Header 里填东西, 它填的类型是 wsse__Security——如果你的 Header 里没有这个指针,编译器连指针都递不进去。 所以「先改 typemap、再生成」这个顺序不能反:反了就得删掉重生成, 而重生成之后,你之前基于旧头文件写的业务代码可能全要改。

二、那一行要写清楚什么

完整的一行是这样的:

SOAP_ENV__Header = | struct SOAP_ENV__Header {
      struct wsse__Security *wsse__Security; }

三个部分各有各的作用:

部分 作用 写错会怎样
SOAP_ENV__Header = 告诉 wsdl2h「这是我要改的结构」 改了不生效
| struct … 表示「整段照抄,不要包装」 被套一层 typedef
wsse__Security * 指针,不是值 结构体不完整,编不过

第三行那个「指针不是值」很关键:Header 里放指针, 意味着「有没有鉴权」和「鉴权内容是什么」可以分开处理—— 不用鉴权时指针是 NULL,序列化时会自动跳过整个 Security 节点。 如果写成值(struct wsse__Security wsse__Security;), 那每个请求都会带一个空的 <wsse:Security/>,有的设备会把「空 Security」当成「鉴权失败」。 第二行那个 | 是 gSOAP 的语法:表示「后面这段原样插入」, 不写它,gSOAP 会给你套一层 typedef,结构体名就变了。

三、digest 的三步与 28 个字符

运行期的事这里先给出结果(08.04 再展开): PasswordDigest = Base64( SHA1( nonce 原始字节 + Created + 密码 ) )

步 处理 长度
① nonce 的 Base64 解码成原始字节 16 字节
② SHA1 得到摘要 20 字节
③ Base64 编码 28 字符

第三行那个 28 是本节最实用的一个数: 抓包看到 Password 是 28 个字符,就知道用的是 Digest; 看到 8 个字符(比如 admin123),那就是明文。 这个判断在现场排查时非常快:不用解包、不用看 Type 属性,数字符数就行。 顺带说清一件事: nonce 是客户端生成的随机数,不是设备给的——所以「每次请求换一个」这件事完全在客户端手里, 而设备只能记录「最近用过的」来防重放。

四、生成完要检查什么

wsdl2h 跑完,不要急着往下走,先在这三个地方看一眼:

检查项 在哪看 不对的表现
Header 里有没有 Security onvif.h 搜 SOAP_ENV__Header 空结构
有没有 wsse__ 类型 搜 wsse__UsernameToken 搜不到
前置声明是否完整 搜 struct wsse__Security 只有用到、没有声明

第三行是「编不过」的典型原因: wsse.h 里定义了完整结构,但 onvif.h 只是引用它—— 所以 onvif.h 里应该有一句 #import "wsse.h" 或者前置声明。 都没有的话,soapcpp2 会报「不完整类型」,而报错的位置在 soapStub.h 里, 看起来像是生成器的问题,其实是 typemap 少了一行。 所以这三项检查的价值在于:在 soapcpp2 之前发现问题, 而不是等它生成完八个文件、编译报错,再回头怀疑生成器。

五、动手 Demo:鉴权要在生成期就埋进去

Demo 把「按 typemap 声明推出来的结构体」打出来, 再真算一遍 digest,看那 28 个字符是怎么来的。 结构体是推的、digest 是真算的——两者性质不同,输出里会分开说。

# ① 按 typemap 那行声明推出来的结构(不是 wsdl2h 的产物)
print("struct SOAP_ENV__Header { struct wsse__Security *wsse__Security; };")

# ② digest 真算:nonce → SHA1 → Base64
nonce = base64.b64encode(b"1234567890abcdef").decode()
created = "2026-10-24T12:00:00Z"
print(len(nonce), nonce)
print(len(S.digest(nonce, created)), S.digest(nonce, created))

实跑输出:

--------------------------------------------------------------------
鉴权要在生成期就埋进去
--------------------------------------------------------------------
**这一节讲的是「生成代码时就把鉴权结构带进去」,不是运行期怎么算 digest。**
**区别在于**:**生成期决定「结构体里有没有 `wsse__Security` 这个字段」**,
**运行期决定「这个字段里填什么」。前者漏了,后者根本没地方填。**

**埋的地方就是 `typemap.dat` 的那一行**(Demo 105 里写过):

  `SOAP_ENV__Header = | struct SOAP_ENV__Header { struct wsse__Security *wsse__Security; }`

**`wsdl2h` 看到这行,就不再生成默认的空 Header**,
**而是在里面塞一个指向 `wsse__Security` 的指针。**
**`wsse__Security` 本身来自 gSOAP 自带的 `wsse.h`**,所以要 `#import` 它。

**按那行声明推出来的结构体长这样**(照 typemap 的写法推的,不是 `wsdl2h` 的产物):

  struct SOAP_ENV__Header {
      struct wsse__Security *wsse__Security;   /* ① */
  };
  struct wsse__Security {
      struct wsse__UsernameToken *UsernameToken;  /* ② */
  };
  struct wsse__UsernameToken {
      char *Username;      /* ③ */
      char *Password;      /* ④ 明文或摘要 */
      char *Nonce;         /* ⑤ Base64 */
      char *wsu__Created;  /* ⑥ UTC 时间串 */
  };

**六个字段一一对应 WS-UsernameToken 的六个元素**,
**其中 ⑤⑥ 只用来算摘要,不单独发出去**——
**但设备端要拿它们重算一遍,所以必须一起传。**

**摘要本身的算法是三步**,我用同一份数据算一遍:

  步     做什么                                   长度
  ------------------------------------------------------------
  ①      nonce 取 Base64 解码后的**原始字节**     16 字节
  ②      SHA1( 原始字节 + Created 串 + 密码 )     20 字节
  ③      把 20 字节 Base64 编码                   **28 字符**

nonce(Base64)→ `MTIzNDU2Nzg5MGFiY2RlZg==`(24 字符)
Created → `2026-10-24T12:00:00Z`
digest → `SP8OVpfTu7lVpEpQV1X9SRH/6G0=`(28 字符)

**第三行那个 28 不是凑的**:20 字节 Base64 是 28 个字符(含一个 `=` 补位)。
**所以抓包看到 Password 是 28 个字符,就知道它用的是 Digest 不是明文。**
**明文密码的长度是可变的**,`admin123` 是 8 个字符——这个差别一眼能看出来。

第二段的输出里 MTIzNDU2Nzg5MGFiY2RlZg== 是 24 个字符, 对应 16 字节原文—— Base64 的规律是「每 3 字节变 4 字符」,16 字节不是 3 的倍数,所以末尾有 == 补位。 第三段那个 28 对应的就是 20 字节的 SHA1 摘要—— 同样是「不是 3 的倍数」的补位结果,只是刚好凑成 28。 这两个长度都是固定的:所以「Password 字段多长」可以直接反推「用的什么方式」。 输出里那句「不是 wsdl2h 的产物」必须留着: 我没有 wsdl2h,那段结构体是照 typemap 的声明推的—— 它说明了「那行声明会带来什么」,但不等于真实生成结果。 这一点在正文里也要重复一次,因为读者很容易把它当成「生成的代码就是这样」。

30 秒实验

  1. 把 nonce 改成 8 字节,看 Base64 长度变成多少(提示:12 个字符)。
  2. 把 created 里的 Z 去掉,看 digest 会变成什么——这解释了为什么时区写错就鉴权失败。
  3. 在 onvif.h 里搜 wsse__,看能不能找到 UsernameToken 的定义。

常见误区

  1. 先生成、后改 typemap。 改完必须删掉重生成, 而重生成之后基于旧头文件写的业务代码可能全要改。
  2. 把 Header 里的 wsse__Security 写成值而不是指针。 那样每个请求都会带一个空 Security 节点, 有的设备会把它当成鉴权失败。
  3. 以为 nonce 是设备给的。 它是客户端生成的随机数, 所以「每次换一个」完全在客户端手里。
自测三问(点开对答案) 1. 为什么鉴权要在生成期就埋进去? —— **因为 gSOAP 的鉴权函数要往 Header 里的 `wsse__Security` 指针填东西**, **默认生成的 Header 是空结构,连指针都没有。** 2. digest 为什么固定是 28 个字符? —— **它是 20 字节摘要的 Base64 编码**,长度固定。**所以看长度就能判断用的是 Digest 还是明文。** 3. typemap 里那个 `|` 是干什么的? —— **表示「后面这段原样插入」**。 不写它,gSOAP 会套一层 typedef,**结构体名就不是你以为的那个了。**

下一节跑 soapcpp2:八个文件,每个都有用,也都有坑。

相关笔记