鉴权分两半:生成期决定「结构体里有没有放令牌的地方」,运行期决定「往里填什么」。
这一节讲前半——它只需要在 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 秒实验
- 把 nonce 改成 8 字节,看 Base64 长度变成多少(提示:12 个字符)。
- 把
created里的Z去掉,看 digest 会变成什么——这解释了为什么时区写错就鉴权失败。 - 在
onvif.h里搜wsse__,看能不能找到UsernameToken的定义。
常见误区
- 先生成、后改 typemap。 改完必须删掉重生成, 而重生成之后基于旧头文件写的业务代码可能全要改。
- 把 Header 里的
wsse__Security写成值而不是指针。 那样每个请求都会带一个空 Security 节点, 有的设备会把它当成鉴权失败。 - 以为 nonce 是设备给的。 它是客户端生成的随机数, 所以「每次换一个」完全在客户端手里。
自测三问(点开对答案)
1. 为什么鉴权要在生成期就埋进去? —— **因为 gSOAP 的鉴权函数要往 Header 里的 `wsse__Security` 指针填东西**, **默认生成的 Header 是空结构,连指针都没有。** 2. digest 为什么固定是 28 个字符? —— **它是 20 字节摘要的 Base64 编码**,长度固定。**所以看长度就能判断用的是 Digest 还是明文。** 3. typemap 里那个 `|` 是干什么的? —— **表示「后面这段原样插入」**。 不写它,gSOAP 会套一层 typedef,**结构体名就不是你以为的那个了。**下一节跑 soapcpp2:八个文件,每个都有用,也都有坑。
- ☐ (本节完,下一节 08.01.06 正式生成框架代码)