- 示例工程
【免费下载链接】Windows-classic-samples
This repo contains samples that demonstrate the API used in Windows classic desktop applications.
本篇文章以 Windows-classic-samples 仓库中的 HttpServerWithUsernameOverSsl 示例 为核心,系统讲解如何用 Windows Web Services API(WWSAPI)构建一个采用「SSL 传输安全 + 用户名密码消息安全」混合模式(mixed-mode)的 HTTP 服务端:SSL 负责对传输通道进行签名与加密并提供服务器身份认证,而客户端身份则由消息中 WS-Security 头携带的用户名/密码对来验证。读完本文,你将掌握 WWSAPI 服务端的完整编程模型(Listener → Channel → Message)、双安全绑定(WS_SECURITY_DESCRIPTION)的组装方式、密码校验回调的编写、WSDL 消息契约的代码生成流程,以及该示例在 Visual Studio 中的构建、URL 预留与证书注册等实战细节。
示例概览:什么是 Username-over-SSL 混合模式安全
该示例的官方定位在 README.md 中描述为:使用 Windows Web Services API 创建一个「接受通道、接收消息并回复消息」的服务器,并启用 username over SSL mixed-mode security。需要说明的是,虽然 README 原文将服务器描述为 "TCP server",但从 主程序源码 可以确认,它实际使用WS_HTTP_CHANNEL_BINDING创建的是 HTTP 通道,因此本质上是 HTTP 服务器示例。
在该安全模型中,两道防线分工明确:
| 安全层次 | 承担的安全职责 | 对应 WWSAPI 绑定 |
|---|---|---|
| 传输层(SSL/TLS) | 对传输连接进行签名与加密,并提供服务器身份认证(防止中间人冒充服务器) | WS_SSL_TRANSPORT_SECURITY_BINDING |
| 消息层(WS-Security) | 在 SOAP 消息的 WS-Security 头中携带用户名/密码对,完成客户端身份认证 | WS_USERNAME_MESSAGE_SECURITY_BINDING |
之所以称为「混合模式」,是因为它将传输级安全与消息级安全组合在同一份安全描述(security description)中:WS_SSL_TRANSPORT_SECURITY_BINDING与WS_USERNAME_MESSAGE_SECURITY_BINDING通过WS_SECURITY_BINDING*数组一并注册到监听器上。这种组合常见于企业级 Web 服务场景:HTTPS 通道保证数据机密性与服务器可信性,消息头中的用户名/密码则允许服务端对调用方做业务层面的身份鉴别,而无需依赖复杂的证书双向认证。
仓库结构与工程文件
该示例的完整目录结构如下:
Samples/WebServices/HttpServerWithUsernameOverSsl/ ├── README.md # 示例说明(本文主体文档) ├── readme.txt # 补充的构建/URL 预留/安全说明 └── cpp/ ├── HttpServerWithUsernameOverSsl.cpp # 服务端主程序 ├── HttpServerWithUsernameOverSsl.sln # Visual Studio 解决方案 ├── HttpServerWithUsernameOverSsl.vcxproj # 工程文件 ├── HttpServerWithUsernameOverSsl.vcxproj.filters └── PurchaseOrder.wsdl # 服务消息契约(WSDL 定义)从 工程文件 可以看出几个关键工程配置:
- WSDL 代码生成:
PurchaseOrder.wsdl配置了一个自定义构建步骤,命令为Wsutil.exe /wsdl:PurchaseOrder.wsdl /out:.,输出PurchaseOrder.wsdl.c与PurchaseOrder.wsdl.h两个生成文件,分别加入ClCompile与ClInclude项。这是 WWSAPI 的典型流程——先用 Wsutil 工具把 WSDL 编译成 C 头文件与源码,再在业务代码中#include "PurchaseOrder.wsdl.h"使用生成的消息描述符。 - 链接依赖库:
webservices.lib(WWSAPI 核心库)、crypt32.lib(加密 API)、rpcrt4.lib、Iphlpapi.lib。 - 平台与配置:支持 Win32 / ARM / x64 三种平台,Debug 与 Release 两种配置,工具集为 v110(Visual Studio 2012 对应版本),字符集为 Unicode,子系统为控制台(Console)应用。
核心安全配置:双绑定安全描述的组装
安全配置集中在 HttpServerWithUsernameOverSsl.cpp 的wmain()函数入口处,共分为三步。
1. 用户名消息安全绑定(客户端认证)
// declare and initialize a username message security binding WS_USERNAME_MESSAGE_SECURITY_BINDING usernameBinding = {}; // zero out the struct usernameBinding.binding.bindingType = WS_USERNAME_MESSAGE_SECURITY_BINDING_TYPE; // set the binding type usernameBinding.bindingUsage = WS_SUPPORTING_MESSAGE_SECURITY_USAGE; // set the binding usage usernameBinding.passwordValidator = MyPasswordValidator;要点说明:
bindingType必须设置为WS_USERNAME_MESSAGE_SECURITY_BINDING_TYPE,标识这是一个基于用户名/密码的消息安全绑定。bindingUsage = WS_SUPPORTING_MESSAGE_SECURITY_USAGE表示该绑定是「支撑型」消息安全:它不单独建立安全通道,而是作为 SSL 传输安全之上的附加认证层,把凭证放进消息的 WS-Security 头。passwordValidator指向自定义的密码校验回调MyPasswordValidator,服务端在此回调中完成对收到的用户名/密码对的验证。
2. SSL 传输安全绑定(传输保护与服务器认证)
// declare and initialize an SSL transport security binding WS_SSL_TRANSPORT_SECURITY_BINDING sslBinding = {}; // zero out the struct sslBinding.binding.bindingType = WS_SSL_TRANSPORT_SECURITY_BINDING_TYPE; // set the binding type // NOTE: At the server, the SSL certificate for the listen URI must be // registered with http.sys using a tool such as httpcfg.exe.源码注释特别强调了一个运维前提:服务端监听 URI 对应的 SSL 证书必须先注册到 http.sys。httpcfg.exe 是早期工具,现代 Windows 上等价操作为netsh http add sslcert(见下文「运行前的准备」一节)。
3. 安全描述(Security Description)的组装
// declare and initialize the array of all security bindings WS_SECURITY_BINDING* securityBindings[2] = { &sslBinding.binding, &usernameBinding.binding }; // declare and initialize the security description WS_SECURITY_DESCRIPTION securityDescription = {}; // zero out the struct securityDescription.securityBindings = securityBindings; securityDescription.securityBindingCount = WsCountOf(securityBindings);最终,包含两个绑定的securityDescription在创建监听器时作为参数传入WsCreateListener,从而把「HTTPS 传输加密 + WS-Security 用户名认证」组合应用到整个监听器生命周期内。
密码校验回调:MyPasswordValidator 的实现
服务端的客户端认证并非由 WWSAPI 自行完成,而是通过回调把凭证交给应用层验证。MyPasswordValidator 展示了完整的回调签名与硬编码校验逻辑:
static HRESULT CALLBACK MyPasswordValidator( _In_ void* callbackState, _In_ const WS_STRING* username, _In_ const WS_STRING* password, _In_ const WS_ASYNC_CONTEXT* asyncContext, _In_opt_ WS_ERROR* error) { UNREFERENCED_PARAMETER(callbackState); UNREFERENCED_PARAMETER(asyncContext); UNREFERENCED_PARAMETER(error); const WS_STRING fixedUsername = WS_STRING_VALUE(L"usr1"); const WS_STRING fixedPassword = WS_STRING_VALUE(L"pwd1"); if (CompareWsString(username, &fixedUsername) && CompareWsString(password, &fixedPassword)) { return S_OK; } return S_FALSE; }行为要点:
- 回调返回
S_OK表示凭证通过验证;返回S_FALSE表示验证失败,通道握手将被拒绝。 - 示例中凭证硬编码为
usr1/pwd1,仅供演示。生产环境中应将callbackState指向真实的账号数据库或认证服务,避免硬编码。 - 比较操作使用自定义的 CompareWsString:它基于
CompareStringW(LOCALE_INVARIANT, ...)按不区分大小写(0为区分大小写标志位)逐字符比较WS_STRING,并兼容非 NULL 结尾的字符串(长度由WS_STRING.length显式给出,这也是WS_STRING与普通 C 字符串的重要区别)。
服务端主流程:Listener → Channel → Message 全链路
主程序在wmain()中完整演示了 WWSAPI 服务端编程模型的每个阶段。
初始化阶段:Error、Heap 与 Listener
hr = WsCreateError(NULL, 0, &error); // 错误对象,用于收集富错误信息 hr = WsCreateHeap(/*maxSize*/ 2048, /*trimSize*/ 512, NULL, 0, &heap, error); hr = WsCreateListener( WS_CHANNEL_TYPE_REPLY, // 应答型通道 WS_HTTP_CHANNEL_BINDING, // HTTP 传输绑定 NULL, 0, &securityDescription, // 双安全绑定描述 &listener, error); hr = WsOpenListener(listener, &uri, NULL, error); // 监听 https://localhost:8443/example- 监听 URI 定义为
WS_STRING_VALUE(L"https://localhost:8443/example"),协议必须是https才能匹配 SSL 传输安全绑定。 WS_CHANNEL_TYPE_REPLY表明这是服务端应答型通道;客户端对应的是WS_CHANNEL_TYPE_REQUEST。
随后通过WsCreateChannelForListener为监听器创建通道,再用WsCreateMessageForChannel分别创建requestMessage与replyMessage两个消息对象。
请求处理循环:Accept → Receive → Dispatch → Reply
主循环固定运行300 次(for (int i = 0; i < 300; i++)),每次迭代的完整流程如下:
// 1. 接受客户端连接 hr = WsAcceptChannel(listener, channel, NULL, error); // 2. 声明期望收到的消息描述:采购订单 或 订单状态查询 const WS_MESSAGE_DESCRIPTION* requestMessageDescriptions[] = { &PurchaseOrder_wsdl.messages.PurchaseOrder, // 消息体含 _PurchaseOrderType &PurchaseOrder_wsdl.messages.GetOrderStatus, // 消息体含 GetOrderStatus }; // 3. 接收消息并按描述反序列化消息体 hr = WsReceiveMessage(channel, requestMessage, requestMessageDescriptions, WsCountOf(requestMessageDescriptions), WS_RECEIVE_REQUIRED_MESSAGE, WS_READ_REQUIRED_POINTER, heap, &requestBodyPointer, sizeof(requestBodyPointer), &indexOfMatchedMessageDescription, NULL, error);WsReceiveMessage会把收到的 SOAP 消息体反序列化到由WS_HEAP分配的内存中,并通过indexOfMatchedMessageDescription返回命中的消息描述索引,从而在代码里区分两种请求。
分发与应答逻辑:
- 命中
PurchaseOrder:将_PurchaseOrderType的quantity、productName打印到控制台,构造orderConfirmation(expectedShipDate = L"1/1/2006"、orderID = 123),以PurchaseOrder_wsdl.messages.OrderConfirmation作为应答描述调用WsSendReplyMessage回发。 - 命中
GetOrderStatus:若orderID != 123,走 Fault 分支——用WsSetFaultErrorDetail写入_OrderNotFoundFaultType的详情(Action 为http://example.com/fault),用WsAddErrorString追加L"Invalid order ID"错误字符串,再将hr置为E_FAIL以触发 Fault 发送;若orderID == 123,则以GetOrderStatusResponse(status = L"Pending")正常应答。
Fault 与正常应答的分支处理:
if (FAILED(hr)) { // 把错误对象中累积的信息序列化为 Fault 消息体发送 hr = WsSendFaultMessageForError(channel, replyMessage, error, hr, WS_FULL_FAULT_DISCLOSURE, requestMessage, NULL, error); hr = WsResetError(error); // 重置错误对象以供复用 hr = WsResetMessage(replyMessage, error); } else { hr = WsSendReplyMessage(channel, replyMessage, replyMessageDescription, WS_WRITE_REQUIRED_VALUE, replyBodyPointer, replyBodySize, requestMessage, NULL, error); hr = WsResetMessage(replyMessage, error); } // 迭代收尾:重置请求消息、重置堆、关闭并重置通道 hr = WsResetMessage(requestMessage, error); hr = WsResetHeap(heap, error); // 释放堆上的反序列化数据 hr = WsCloseChannel(channel, NULL, error); hr = WsResetChannel(channel, error); // 通道复位后可在下一轮迭代复用这里的资源管理手法值得借鉴:消息对象(WsResetMessage)、堆(WsResetHeap)与通道(WsResetChannel)都通过「重置」而非「重建」的方式复用,避免高频创建/销毁对象的开销;WsCloseChannel与WsResetChannel组合则实现了通道的一次性会话语义。
退出清理
Exit标签处依次执行WsCloseChannel/WsFreeChannel、WsFreeMessage×2、WsCloseListener/WsFreeListener、WsFreeError、WsFreeHeap,并在失败时通过 PrintError 打印富错误信息(利用WsGetErrorProperty读取错误字符串计数、WsGetErrorString逐条输出)。
消息契约:PurchaseOrder.wsdl 与 Wsutil 代码生成
PurchaseOrder.wsdl 定义了本示例的 SOAP 服务契约,命名空间为http://example.org:
| WSDL 元素 | 类型结构 | 用途 |
|---|---|---|
PurchaseOrderType | quantity: xsd:int+productName: xsd:string | 采购订单请求体 |
OrderConfirmationType | orderID: xsd:unsignedInt+expectedShipDate: xsd:string | 订单确认应答体 |
GetOrderStatusType | orderID: xsd:unsignedInt | 订单状态查询请求体 |
GetOrderStatusResponseType | orderID: xsd:unsignedInt+status: xsd:string | 订单状态应答体 |
OrderNotFoundFaultType | orderID: xsd:unsignedInt | 订单不存在 Fault 详情 |
portType定义了两个操作:
Order:输入PurchaseOrder(Actionhttp://example.org/purchaseorder),输出OrderConfirmation(Actionhttp://example.org/orderconfirmation);OrderStatus:输入GetOrderStatus(Actionhttp://example.org/getorderstatus),输出GetOrderStatusResponse(Actionhttp://example.org/getorderstatusresponse),并声明 FaultOrderNotFound(Actionhttp://example.org/ordernotfound)。
主程序直接引用PurchaseOrder_wsdl.messages.PurchaseOrder、PurchaseOrder_wsdl.messages.GetOrderStatus等生成符号,因此每次修改 WSDL 后都必须重新执行Wsutil.exe /wsdl:PurchaseOrder.wsdl /out:.重新生成代码(在 Visual Studio 中构建时由自定义构建步骤自动完成)。
配套客户端:如何与服务端对接
仓库中有一个与之配套的客户端示例 HttpClientWithUsernameOverSsl,运行在https://localhost:8443/example上,与服务端构成完整的请求-应答闭环。其安全配置与服务端严格对称:
// 客户端凭证(与服务端硬编码校验的 usr1/pwd1 一致) WS_STRING_USERNAME_CREDENTIAL usernameCredential = {}; static const WS_STRING userName = WS_STRING_VALUE(L"usr1"); static const WS_STRING passWord = WS_STRING_VALUE(L"pwd1"); usernameCredential.credential.credentialType = WS_STRING_USERNAME_CREDENTIAL_TYPE; usernameCredential.username = userName; usernameCredential.password = passWord; // 用户名消息安全绑定:把 clientCredential 挂到绑定上 WS_USERNAME_MESSAGE_SECURITY_BINDING usernameBinding = {}; usernameBinding.binding.bindingType = WS_USERNAME_MESSAGE_SECURITY_BINDING_TYPE; usernameBinding.bindingUsage = WS_SUPPORTING_MESSAGE_SECURITY_USAGE; usernameBinding.clientCredential = &usernameCredential.credential; // SSL 传输绑定 + 安全描述组装,顺序同为 {ssl, username} WS_SSL_TRANSPORT_SECURITY_BINDING sslBinding = {}; sslBinding.binding.bindingType = WS_SSL_TRANSPORT_SECURITY_BINDING_TYPE; WS_SECURITY_BINDING* securityBindings[2] = { &sslBinding.binding, &usernameBinding.binding };客户端通过WsCreateChannel(WS_CHANNEL_TYPE_REQUEST, WS_HTTP_CHANNEL_BINDING, ...)、WsOpenChannel建立 HTTPS 连接,再用WsRequestReply完成「发送采购订单 → 读取订单确认」「发送状态查询 → 读取状态应答」两轮交互;当请求orderID = 321(服务端不认识的订单号)时,客户端收到WS_E_ENDPOINT_FAULT_RECEIVED,随后用WsGetFaultErrorDetail反序列化 Fault 详情并打印Order 321 was not found。把两个示例对照阅读,即可完整掌握 WWSAPI 在「用户名 + SSL 混合安全」下客户端与服务端的全部握手与通信细节。
构建与运行
系统要求
按 README.md 的说明:
- 客户端操作系统:Windows 7
- 服务端操作系统:Windows Server 2008 R2
- 示例创建时面向 Windows 8.1 / Windows Server 2012 R2,使用 Visual Studio 2013 开发,但多数情况下可直接在更高版本的 Visual Studio 与 Windows 上无改动运行。
- 若要在更早期的系统(Windows XP、Windows Vista、Windows Server 2003/2008)上运行,可能需要安装包含 Windows Web Services API 运行时 DLL 的 Windows Update(详见 readme.txt 的 Prerequisites 说明)。
构建步骤
- 打开解决方案文件 HttpServerWithUsernameOverSsl.sln。
- 按F7(Visual Studio 2013 为 F6),或通过菜单 Build → Build Solution 构建。
- 可在 Build 菜单的 Configuration Manager 中切换目标平台(Win32 / x64 / ARM)与配置(Debug / Release)。
- 构建产物输出到工程默认的
Debug或Release目录(实际输出路径为$(SolutionDir)$(Platform)\bin\$(Configuration)\,见 vcxproj)。
运行步骤
构建完成后按F5(带调试运行)或Ctrl+F5(不带调试运行)启动;也可以在 Debug 菜单中选择对应选项。
运行前的准备:URL 预留与证书注册
服务端程序通过 http.sys 监听 HTTP(S) 端口,非提升权限的 Visual Studio 进程默认无权注册监听 URL,因此需要先在提升权限(管理员)的命令提示符中执行 URL 预留命令(来源:readme.txt 的 Reserving the HTTP URLs 一节):
netsh http add urlacl url=http://+:80/example user=%USERDOMAIN%\%USERNAME% netsh http add urlacl url=https://+:443/example user=%USERDOMAIN%\%USERNAME%使用提升权限的 PowerShell 时等价写法为:
netsh http add urlacl url=http://+:80/example user=$env:USERDOMAIN\$env:USERNAME netsh http add urlacl url=https://+:443/example user=$env:USERDOMAIN\$env:USERNAME不再需要时,删除预留:
netsh http delete urlacl url=http://+:80/example netsh http delete urlacl url=https://+:443/example另外,根据源码注释与 readme.txt 的安全提示,https://localhost:8443/example的监听 URI 必须先用httpcfg.exe或现代等价命令netsh http add sslcert将 SSL 证书注册到 http.sys,否则 SSL 传输绑定无法生效。
安全注意事项
readme.txt 明确声明:本示例仅用于教学目的,演示 WWSAPI 的用法,未在生产环境测试,未经修改不得直接部署到生产环境。结合源码可以归纳出两个必须改造的点:一是密码校验回调中硬编码的usr1/pwd1应替换为真实认证后端;二是WS_FULL_FAULT_DISCLOSURE会向客户端完整披露 Fault 详情,生产环境应评估是否改用受限的 Fault 披露策略。
关键 API 参考清单
原 README 的 Related topics 中列出的 API 均在本示例源码中有实际调用,整理如下(含调用位置):
| API | 在本示例中的作用 |
|---|---|
WsCreateListener | 创建应答型 HTTP 监听器并绑定安全描述(L188) |
WsOpenListener | 在指定 URI 上开始监听(L201) |
WsCreateChannelForListener | 为监听器创建接受进来的通道(L207) |
WsCreateMessageForChannel | 为通道创建请求/应答两个消息对象(L218) |
WsAcceptChannel | 接受客户端连接(L245) |
WsReceiveMessage | 接收消息并按描述反序列化消息体(L265) |
WsSendReplyMessage | 回发正常应答消息(L408) |
相关的安全与支撑结构还包括WS_SECURITY_DESCRIPTION、WS_USERNAME_MESSAGE_SECURITY_BINDING、WS_SSL_TRANSPORT_SECURITY_BINDING、WS_SECURITY_BINDING、WS_ERROR(富错误信息)与WS_HEAP(反序列化数据堆)等,它们共同构成了本示例所演示的 Windows Web Services API 核心技术栈。
- 示例工程
【免费下载链接】Windows-classic-samples
This repo contains samples that demonstrate the API used in Windows classic desktop applications.
相关推荐
XiaoMusic 完整使用教程:Docker 部署小爱音箱音乐播放,语音点播任意歌曲
XiaoMusic 完整使用教程:Docker 部署小爱音箱音乐播放,语音点播任意歌曲 XiaoMusic 是一个免费开源的音乐服务,能让小爱音箱脱离内置平台限
示例工程Windows Web Services API:基于 SSL 传输安全的 HTTP 计算器服务托管示例详解(Windows-classic-samples)
Windows Web Services API:基于 SSL 传输安全的 HTTP 计算器服务托管示例详解(Windows classic samples)
示例工程Windows-classic-samples 实战:使用 Windows Web Services API 构建基于 Kerberos over SSL 的 HTTP 客户端
Windows classic samples 实战:使用 Windows Web Services API 构建基于 Kerberos over SSL 的
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考