Windows Web Services 安全请求-应答 TCP 服务端示例解析:基于 SSPI 传输安全的 WWSAPI 实现

原创2026-10-15 18:31:451,604 阅读
文章标签:示例工程

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 的两个核心组合能力:

  1. TCP 传输:使用 WS_TCP_CHANNEL_BINDING 与 WS_CHANNEL_TYPE_DUPLEX_SESSION 建立长会话通道,监听地址为 net.tcp://localhost/example;
  2. 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 原样编译运行。

构建步骤

  1. 用 Visual Studio 打开 RequestReplyTcpServerWithWindowsTransportSecurity.sln(Visual Studio Express 2013 for Windows 8.1 或更高版本任意 SKU);
  2. 按 F7(Visual Studio 2013 中为 F6),或从菜单 Build → Build Solution;
  3. 产物输出到默认的 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:. 生成契约代码。

运行步骤

  1. 构建成功后,按 F5(带调试运行)或 Ctrl+F5(不带调试运行),也可从 Debug 菜单选择对应选项;
  2. 服务端启动后会阻塞在 WsAcceptChannel,等待客户端连接;
  3. 建议同时运行配套的 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 传输安全的请求-应答通信全链路。

登录后查看全文
Windows-classic-samples