Windows Web Services 安全请求-应答 TCP 服务端示例解析:基于 SSPI 传输安全的 WWSAPI 实现
Windows Web Services 安全请求-应答 TCP 服务端示例解析:基于 SSPI 传输安全的 WWSAPI 实现
导读
本篇文章基于 Windows-classic-samples 仓库中的 RequestReplyTcpServerWithWindowsTransportSecurity 示例,深入讲解如何用 Windows Web Services API(WWSAPI)实现一个带 Windows SSPI 传输安全的请求-应答(request-reply)TCP 服务端。阅读完本文,你将掌握 net.tcp:// 地址监听、双工会话通道(duplex session channel)的建立与接受、基于消息描述(message description)的自动反序列化分发、应答与 SOAP 故障消息的发送,以及配套客户端在单机联调时关于 NTLM/Kerberos 安全属性的关键配置。
示例定位:安全传输的请求-应答 TCP 服务端
该示例演示了 WWSAPI 的两个核心组合能力:
- TCP 传输:使用
WS_TCP_CHANNEL_BINDING与WS_CHANNEL_TYPE_DUPLEX_SESSION建立长会话通道,监听地址为net.tcp://localhost/example; - SSPI 传输安全:通过
WS_TCP_SSPI_TRANSPORT_SECURITY_BINDING绑定类型,把 Windows 集成身份验证(NTLM/Kerberos)直接挂在传输层上,替代了更复杂、开销更高的消息级安全(如 WS-Security 签名/加密)。
示例的业务场景是一套"采购订单"(PurchaseOrder)服务:客户端下单(PurchaseOrder)后收到订单确认(OrderConfirmation);客户端查询订单状态(GetOrderStatus)时,服务端对合法订单返回状态(GetOrderStatusResponse),对非法订单则通过 WsSendFaultMessageForError 回送带详细信息的 SOAP 故障(OrderNotFoundFault)。
示例的入口代码位于 RequestReplyTcpServerWithWindowsTransportSecurity.cpp,整个程序是一个控制台应用,其主流程可以用下面这条调用链概括:
WsCreateError → WsCreateHeap → WsCreateListener(带安全描述) → WsCreateChannelForListener
→ WsOpenListener(net.tcp://localhost/example) → WsAcceptChannel
→ WsCreateMessageForChannel ×2 → 循环{ WsReceiveMessage → 处理 → WsSendReplyMessage / WsSendFaultMessageForError → WsResetMessage / WsResetHeap }
→ 清理关闭
安全模型:Windows SSPI 传输安全绑定
与常见的"先建立明文 TCP,再在应用层协商安全"不同,本示例把安全直接内建在传输绑定里。核心代码位于 RequestReplyTcpServerWithWindowsTransportSecurity.cpp:
// declare and initialize an Windows SSPI transport security binding
WS_TCP_SSPI_TRANSPORT_SECURITY_BINDING sspiBinding = {}; // zero out the struct
sspiBinding.binding.bindingType = WS_TCP_SSPI_TRANSPORT_SECURITY_BINDING_TYPE; // set the binding type
// declare and initialize the array of all security bindings
WS_SECURITY_BINDING* securityBindings[1] = { &sspiBinding.binding };
// declare and initialize the security description
WS_SECURITY_DESCRIPTION securityDescription = {}; // zero out the struct
securityDescription.securityBindings = securityBindings;
securityDescription.securityBindingCount = WsCountOf(securityBindings);
三段式结构是 WWSAPI 安全配置的标准套路:
| 结构体 | 作用 | 本示例取值 |
|---|---|---|
WS_TCP_SSPI_TRANSPORT_SECURITY_BINDING |
描述"TCP 传输 + SSPI 安全"这一个绑定 | bindingType = WS_TCP_SSPI_TRANSPORT_SECURITY_BINDING_TYPE,其余字段为零值(服务端不配置凭据,身份来自客户端) |
WS_SECURITY_BINDING* securityBindings[1] |
安全绑定数组,可叠加多个绑定 | 仅包含上面的 SSPI 绑定 |
WS_SECURITY_DESCRIPTION |
安全描述,绑定数组 + 绑定数量 | securityBindingCount = WsCountOf(securityBindings) |
服务端只负责"声明"绑定类型,不需要提供任何凭据;客户端身份由 SSPI 在连接建立时自动完成协商与认证。
与客户端安全绑定的对应关系
配套的 RequestReplyTcpClientWithWindowsTransportSecurity.cpp 展示了客户端侧对称的安全配置,它额外引入了两个关键属性:
// declare and initialize a windows credential
WS_DEFAULT_WINDOWS_INTEGRATED_AUTH_CREDENTIAL windowsCredential = {};
windowsCredential.credential.credentialType = WS_DEFAULT_WINDOWS_INTEGRATED_AUTH_CREDENTIAL_TYPE;
ULONG impersonation = SecurityImpersonation;
BOOL requireServerAuth = FALSE;
WS_SECURITY_BINDING_PROPERTY sspiBindingProperties[2] =
{
{ WS_SECURITY_BINDING_PROPERTY_ALLOWED_IMPERSONATION_LEVEL, &impersonation, sizeof(impersonation) },
{ WS_SECURITY_BINDING_PROPERTY_REQUIRE_SERVER_AUTH, &requireServerAuth, sizeof(requireServerAuth) }
};
WS_DEFAULT_WINDOWS_INTEGRATED_AUTH_CREDENTIAL_TYPE:使用当前登录用户的 Windows 凭据,无需显式账号密码;WS_SECURITY_BINDING_PROPERTY_ALLOWED_IMPERSONATION_LEVEL:将允许的模拟级别从默认值提升为SecurityImpersonation;WS_SECURITY_BINDING_PROPERTY_REQUIRE_SERVER_AUTH:设为FALSE后允许服务端不做服务器身份验证。
源码注释特别提醒了一个单机联调的坑(客户端源码 L77-L80):客户端与服务端在同一台机器上运行时,必须二选一——要么把安全包指定为 WS_WINDOWS_INTEGRATED_AUTH_PACKAGE_KERBEROS,要么把 WS_SECURITY_BINDING_PROPERTY_REQUIRE_SERVER_AUTH 设为 FALSE;因为默认使用的 NTLM 协议不支持服务器身份验证,直接使用默认配置会导致连接失败。
服务契约:PurchaseOrder.wsdl 与 Wsutil.exe 代码生成
消息体的序列化/反序列化完全由 WSDL 契约驱动。契约定义在 PurchaseOrder.wsdl 中,命名空间为 http://example.org,包含五个消息类型:
| XSD 元素 | 字段 | 对应消息 |
|---|---|---|
PurchaseOrderType |
quantity(xsd:int)、productName(xsd:string) |
PurchaseOrder |
OrderConfirmationType |
orderID(xsd:unsignedInt)、expectedShipDate(xsd:string) |
OrderConfirmation |
GetOrderStatusType |
orderID(xsd:unsignedInt) |
GetOrderStatus |
GetOrderStatusResponseType |
orderID、status(xsd:string) |
GetOrderStatusResponse |
OrderNotFoundFaultType |
orderID(xsd:unsignedInt) |
OrderNotFoundFault |
IPurchaseOrder 端口类型定义了 Order 与 OrderStatus 两个操作,并为每个消息通过 wsaw:Action 指定了 WS-Addressing Action,例如 http://example.org/purchaseorder、http://example.org/getorderstatus、http://example.org/ordernotfound 等。这些 Action 在服务端会被映射为 WS_MESSAGE_DESCRIPTION 中用于消息匹配的键值。
WSDL 并非手工翻译成结构体,而是由工程在构建时自动生成。在 RequestReplyTcpServerWithWindowsTransportSecurity.vcxproj 中定义了一个 CustomBuild 步骤:
<CustomBuild Include="PurchaseOrder.wsdl">
<Outputs>PurchaseOrder.wsdl.c; PurchaseOrder.wsdl.h</Outputs>
<Command>Wsutil.exe /wsdl:PurchaseOrder.wsdl /out:.</Command>
</CustomBuild>
构建时 Wsutil.exe(WWSAPI 的元数据编译工具)会生成 PurchaseOrder.wsdl.c / PurchaseOrder.wsdl.h,其中暴露了全局描述符 PurchaseOrder_wsdl.messages.PurchaseOrder、PurchaseOrder_wsdl.messages.GetOrderStatus 等,以及对应的 _PurchaseOrderType、_OrderConfirmationType 结构体——这些正是服务端源码中引用的符号。生成的 PurchaseOrder.wsdl.c 也作为编译单元(ClCompile)参与链接。
服务端实现拆解:从监听器到请求-应答循环
1. 基础设施:错误对象与堆
与 WWSAPI 其他示例一致,程序先创建两个贯穿全局的基础对象(L101-L122):
hr = WsCreateError(NULL, 0, &error);
hr = WsCreateHeap(/*maxSize*/ 2048, /*trimSize*/ 512, NULL, 0, &heap, error);
WS_ERROR用于累积丰富的错误信息(错误字符串、故障细节等),每个可能失败的 API 调用都会传入它;WS_HEAP用于承载反序列化出来的消息体数据,maxSize = 2048、trimSize = 512表示堆的容量上限与收缩阈值。每次循环结束后调用WsResetHeap释放该轮分配,实现内存复用。
2. 创建监听器:绑定通道类型与安全描述
监听器通过 WsCreateListener 创建(L125-L132):
hr = WsCreateListener(
WS_CHANNEL_TYPE_DUPLEX_SESSION, // 双工会话:可双向收发,面向会话
WS_TCP_CHANNEL_BINDING, // TCP 通道绑定
NULL, 0,
&securityDescription, // 把前面构造的 SSPI 安全描述挂进来
&listener, error);
这里安全描述被直接注入监听器,意味着监听器接受的每个通道都自动具备 SSPI 传输安全,无需在通道层重复配置。随后用 WsCreateChannelForListener 为监听器预创建配套通道(L139-L148),该通道继承了监听器的通道类型与安全设置。
3. 打开监听器并接受客户端通道
监听地址以 WS_STRING 形式给出(L151-L162):
WS_STRING uri;
uri.chars = L"net.tcp://localhost/example";
uri.length = (ULONG)::wcslen(uri.chars);
hr = WsOpenListener(listener, &uri, NULL, error);
// Accept a channel from the client
hr = WsAcceptChannel(listener, channel, NULL, error);
net.tcp://localhost/example是 WWSAPI 的 TCP 传输 URI 约定(localhost也可替换为主机名/IP);WsOpenListener开始监听后,WsAcceptChannel阻塞等待客户端接入,完成 SSPI 握手后才返回已认证的通道。
4. 接收消息与自动分发
进入无限循环后,程序先声明"期望收到哪两类消息"(L199-L213):
const WS_MESSAGE_DESCRIPTION* requestMessageDescriptions[] =
{
&PurchaseOrder_wsdl.messages.PurchaseOrder, // contains a _PurchaseOrderType in the body
&PurchaseOrder_wsdl.messages.GetOrderStatus, // contains a GetOrderStatus in the body
};
hr = WsReceiveMessage(channel, requestMessage, requestMessageDescriptions,
WsCountOf(requestMessageDescriptions),
WS_RECEIVE_OPTIONAL_MESSAGE, WS_READ_REQUIRED_POINTER, heap,
&requestBodyPointer, sizeof(requestBodyPointer),
&indexOfMatchedMessageDescription, NULL, error);
关键点:
- 传入消息描述数组后,
WsReceiveMessage会根据消息头中的 Action 自动匹配描述,并把消息体反序列化到堆中,返回indexOfMatchedMessageDescription指示命中第几个描述; WS_RECEIVE_OPTIONAL_MESSAGE允许"无消息可收"时返回WS_S_END,此时循环break退出——这是双工会话通道的正常结束路径(客户端关闭通道);- 反序列化出的数据由指定的
WS_HEAP管理,循环末尾的WsResetHeap会统一释放。
5. 业务处理与应答生成
命中描述后,代码通过 requestMessageDescriptions<a href="https://link.gitcode.com/i/346a1c3d7cb7757be6b63d15939bc7a9" target="_blank">indexOfMatchedMessageDescription] 做类型分派([L228-L317):
- PurchaseOrder 分支:把
requestBodyPointer强转为_PurchaseOrderType*,打印quantity与productName,构造orderConfirmation(expectedShipDate = L"1/1/2006"、orderID = 123),并指定应答描述PurchaseOrder_wsdl.messages.OrderConfirmation; - GetOrderStatus 分支:若
orderID != 123,走故障路径(见下节);否则构造getOrderStatusResponse(status = L"Pending"),指定应答描述GetOrderStatusResponse。
应答最终通过 WsSendReplyMessage 发出(L360-L369):
hr = WsSendReplyMessage(channel, replyMessage, replyMessageDescription,
WS_WRITE_REQUIRED_VALUE, replyBodyPointer, replyBodySize,
requestMessage, NULL, error);
requestMessage 参数让 WWSAPI 自动关联请求/应答的关联 ID(correlation ID),客户端据此把应答匹配回原请求。
6. 故障处理:WsSendFaultMessageForError
当收到未知订单号时,服务端不是返回普通应答,而是构造一个 SOAP 故障(L257-L305):
hr = WsSetFaultErrorDetail(error, &orderNotFoundFaultTypeDescription,
WS_WRITE_REQUIRED_VALUE, &orderNotFound, sizeof(orderNotFound));
static const WS_STRING errorMessage = WS_STRING_VALUE(L"Invalid order ID");
hr = WsAddErrorString(error, &errorMessage);
hr = E_FAIL; // Use a failure code to indicate that a fault should be sent
流程是:先用 WsSetFaultErrorDetail 把结构化故障细节(OrderNotFoundFaultType,Action 为 http://example.com/fault)写进错误对象,再用 WsAddErrorString 附加人类可读的字符串,最后把 hr 置为失败码触发故障分支。随后在 L328-L336 调用:
hr = WsSendFaultMessageForError(channel, replyMessage, error, hr,
WS_FULL_FAULT_DISCLOSURE, requestMessage, NULL, error);
WS_FULL_FAULT_DISCLOSURE表示故障中完整披露错误对象里的细节与字符串(生产环境可改用WS_MINIMAL_FAULT_DISCLOSURE降低信息泄露);- 发送完成后依次
WsResetError、WsResetMessage,让错误对象与消息可复用; - 客户端在 RequestReplyTcpClientWithWindowsTransportSecurity.cpp 中以
WS_E_ENDPOINT_FAULT_RECEIVED识别故障,再用WsGetFaultErrorDetail取出_OrderNotFoundFaultType*并打印Order 321 was not found——前后两端对故障细节描述的定义必须完全一致。
7. 错误打印与资源清理
示例自带一个健壮的错误打印函数 PrintError(L19-L56):先打印 errorCode,若为 E_INVALIDARG 或 WS_E_INVALID_OPERATION 则提示"API 使用错误,多半是程序 bug";随后通过 WsGetErrorProperty(error, WS_ERROR_PROPERTY_STRING_COUNT, ...) 获取错误字符串数量,再逐个用 WsGetErrorString 输出。
退出路径(L399-L445)按逆序释放所有对象:WsCloseListener → WsFreeListener → WsCloseChannel → WsFreeChannel → WsFreeMessage ×2 → WsFreeError → WsFreeHeap,所有释放调用都做了空指针保护。若 hr 失败则先 PrintError 再清理。
构建与运行
系统要求
关联文档明确列出的操作系统要求为:
- 客户端:Windows 7;
- 服务端:Windows Server 2008 R2。
仓库 readme.txt 同时补充说明:在 Windows XP、Windows Vista、Windows Server 2003 / 2008 上运行,可能需要安装包含 WWSAPI 运行时 DLL 的 Windows Update。示例面向 Windows 8.1 / Windows Server 2012 R2(Visual Studio 2013)编写,但在多数情况下可直接用更高版本 Visual Studio 原样编译运行。
构建步骤
- 用 Visual Studio 打开 RequestReplyTcpServerWithWindowsTransportSecurity.sln(Visual Studio Express 2013 for Windows 8.1 或更高版本任意 SKU);
- 按 F7(Visual Studio 2013 中为 F6),或从菜单 Build → Build Solution;
- 产物输出到默认的
Debug或Release目录。
工程在 vcxproj 中的关键配置:
- 支持
Debug/Release × Win32/x64/ARM六种组合,PlatformToolset为v110; - 链接依赖:
webservices.lib;crypt32.lib;rpcrt4.lib;Iphlpapi.lib(webservices.lib为 WWSAPI 主库,crypt32.lib用于证书/加密相关支撑); - 子系统为
Console,字符集为Unicode; - 构建时自动执行
Wsutil.exe /wsdl:PurchaseOrder.wsdl /out:.生成契约代码。
运行步骤
- 构建成功后,按 F5(带调试运行)或 Ctrl+F5(不带调试运行),也可从 Debug 菜单选择对应选项;
- 服务端启动后会阻塞在
WsAcceptChannel,等待客户端连接; - 建议同时运行配套的 RequestReplyTcpClientWithWindowsTransportSecurity 示例进行联调:客户端会循环 100 轮,依次发送 PurchaseOrder、GetOrderStatus(合法订单号 123)与 GetOrderStatus(非法订单号 321),服务端控制台将打印
100, Pencil之类的订单内容,客户端则打印订单确认、Order status for order 123 is: Pending以及故障信息Order 321 was not found。
注意:这是单通道、单会话、阻塞式
WsAcceptChannel的教学示例,一次只服务一个客户端;生产级服务端需要结合WS_ASYNC_STATE/回调模型做并发接入,且该示例在 readme.txt 中明确声明仅用于教学演示,未经生产环境测试。
关键 API 速查
下表汇总了本示例使用到的 WWSAPI 核心函数及其在示例中的职责(函数名对应 README 的 Related topics 列表):
| API | 职责 |
|---|---|
WsCreateListener / WsOpenListener |
创建并打开 TCP 监听器,注入安全描述 |
WsCreateChannelForListener / WsAcceptChannel |
创建通道并接受客户端连接(含 SSPI 握手) |
WsCreateMessageForChannel |
为通道创建请求/应答消息对象 |
WsReceiveMessage |
按消息描述数组接收并反序列化消息 |
WsSendReplyMessage |
发送与请求关联的应答消息 |
WsSendFaultMessageForError |
依据错误对象发送 SOAP 故障消息 |
WsSetFaultErrorDetail / WsGetFaultErrorDetail |
在错误对象中写入/读取结构化故障细节 |
WsGetErrorProperty / WsGetErrorString |
枚举并打印错误对象中的错误字符串 |
WsResetMessage / WsResetHeap / WsResetError |
重置消息、堆与错误对象以便复用 |
WsCloseListener / WsFreeListener 等 |
逆序关闭与释放全部资源 |
总结
RequestReplyTcpServerWithWindowsTransportSecurity 是理解 WWSAPI 服务端编程模型的理想入口:它把"通道类型 + 传输绑定 + 安全描述"的声明式组合、WSDL 驱动的消息自动匹配与反序列化、以及"普通应答 vs SOAP 故障"两条发送路径浓缩在一个约 450 行的控制台程序中。结合 服务端源码、WSDL 契约 与 客户端源码 三份材料对照阅读,即可完整掌握基于 Windows SSPI 传输安全的请求-应答通信全链路。