提高Windows Communication Foundation (WCF) 应用程序负载能力的方法之一就是通过把它们部署到负载均衡的服务器场中. 其中可以使用标准的负载均衡技术, Windows 网络负载均衡(NLB)的软件(例如 Application Request Routing), 或者硬件(F5)实现NLB的功能. 随着这些NLB场景变得越来越复杂, 对WCF的架构带来了越来越多的挑战. 本文仅对wsHttpBinding+Message Security+Windows Authentication在NLB的环境下最常见的文件进行讨论..
介绍 :
通过阅读本文, 您可以了解到:
- NLB下wsHttpBinding+Message Security+Windows Authentication常见的异常.`
- WCF Message Security的Windows Authentication是如何进行的.
- 提供几种典型的WCF Security的配置方式.
预备知识 :
WCF wsHttpBinding默认采用Message Security模式. 相比Transport Security模式,它有很多优点。例如:提供了End-to-End security, 允许选择性的加密部分消息,与传输协议无关可以用于任何传输协议,提供更多的安全选择。更多内容可以参考 :
http://msdn.microsoft.com/en-us/library/ff648863.aspx
http://msdn.microsoft.com/en-us/library/ms733137(v=vs.110).aspx
Message Security 下有多种客户端验证方式。 其中Windows authentication是比较常用的Credential Type,并且是默认值。更多内容可以参考:
http://msdn.microsoft.com/en-us/library/ms731346(v=vs.110).aspx
常见问题 :
在Standalone的服务器上WCF Security能够很好的工作. 但是在NLB的环境上, 由于错误的配置方式, 可能会遇到下面的常见错误 :
WsHttpBinding+Message Security+Windows Authentication是如何工作的 :
要了解这两个错误是如何发生的, 首先需要了解正常情况下wsHttpBinding Message Security是如何进行Windows 验证的.
一般情况下, wsHttpBinding按照SPNEGO来引导一个SecurityContextToken(SCT)的生成, 并且将SCT缓存到服务器端. STCs 是基于对称密钥生成的,能够非常有效的签注/加密(signing/encrypting)消息。 当确定客户端会向服务器端发送多次请求的时候,这是一种非常好的提供效率的方式。通过观察WCF TRACE,一般来说会看到这样的过程。首先客户端和服务端生成一个SCT,然后进行一个SPNEGO的过程(有可能是多次请求才能完成),最后客户端和服务端交换SCT并且使用SCT加密WCF的调用。
下面比较详细的列举出整个步骤
- 客户端首先进行SP-Nego 握手, 这个过程完成后会生成一个SCT。
1) 客户端先发送一个Request Security Token(RST)。RST的目的是生成SP-Nego TokenType,它包含了一个SP-Nego块.
2) 服务端返回一个Request Security Token Response (RSTR).
3) 如果需要的话,客户端会将RSTR再次发送给服务端.
4) 服务端将一个对称密钥包裹在STC里, 又将STC包裹在Request Security Token Response Collection (RSTRC). 然后把RSTRC发送给客户端.
一般来说3)和4)可能会发生多次, 一般情况下可能会有4次, 2次客户端2次服务端.
- 客户端向服务端请求另外一个SCT用于消息层面的security。这个SCT是一个长生命是周期,并且真正代表了客户端和服务端之间的session key.
1) 客户端用SCT加注并且加密RST, 并把RST发送到服务端.
2) 服务器端用SCT加注并加密一个RSTR并发送会客户端. 这个RSTR中包含了一个SCT.
这里仅发生一次RST/RSTR.
- 客户端在得到 2 所产生的SCT之后,开始执行WCF调用。
1) 客户端使用上一步所得到的SCT来加密消息. 进行WCF调用.
2) 服务端执行完成之后, 使用同一的SCT来加密消息并返回给客户的. 在这里addressing headers会被签注, 消息体会被签注并且加密.
问题分析 :
问题一 :
The SecurityContextSecurityToken with context-id=urn:uuid:a02a1879-3297-4dee-8035-68eb30ed4195 (key generation-id=) is not registered.
at System.ServiceModel.Security.WSSecureConversation.SecurityContextTokenEntry.ReadTokenCore(XmlDictionaryReader reader, SecurityTokenResolver tokenResolver)
at System.ServiceModel.Security.WSSecurityTokenSerializer.ReadTokenCore(XmlReader reader, SecurityTokenResolver tokenResolver)
at System.IdentityModel.Selectors.SecurityTokenSerializer.ReadToken(XmlReader reader, SecurityTokenResolver tokenResolver)
at System.ServiceModel.Security.ReceiveSecurityHeader.ReadToken(XmlReader reader, SecurityTokenResolver tokenResolver, IList1 allowedTokenAuthenticators, SecurityTokenAuthenticator& usedTokenAuthenticator)
at System.ServiceModel.Security.ReceiveSecurityHeader.ReadToken(XmlDictionaryReader reader, Int32 position, Byte[] decryptedBuffer, SecurityToken encryptionToken, String idInEncryptedForm, TimeSpan timeout)
at System.ServiceModel.Security.ReceiveSecurityHeader.ExecuteFullPass(XmlDictionaryReader reader)
at System.ServiceModel.Security.StrictModeSecurityHeaderElementInferenceEngine.ExecuteProcessingPasses(ReceiveSecurityHeader securityHeader, XmlDictionaryReader reader)
at System.ServiceModel.Security.ReceiveSecurityHeader.Process(TimeSpan timeout)
at System.ServiceModel.Security.MessageSecurityProtocol.ProcessSecurityHeader(ReceiveSecurityHeader securityHeader, Message& message, SecurityToken requiredSigningToken, TimeSpan timeout, SecurityProtocolCorrelationState[] correlationStates)
at System.ServiceModel.Security.SymmetricSecurityProtocol.VerifyIncomingMessageCore(Message& message, String actor, TimeSpan timeout, SecurityProtocolCorrelationState[] correlationStates)
at System.ServiceModel.Security.MessageSecurityProtocol.VerifyIncomingMessage(Message& message, TimeSpan timeout, SecurityProtocolCorrelationState[] correlationStates)
at System.ServiceModel.Channels.SecurityChannelListener1.ServerSecurityChannel1.VerifyIncomingMessage(Message& message, TimeSpan timeout, SecurityProtocolCorrelationState[] correlationState)
at System.ServiceModel.Channels.SecurityChannelListener1.SecurityReplyChannel.ProcessReceivedRequest(RequestContext requestContext, TimeSpan timeout)
at System.ServiceModel.Channels.SecurityChannelListener1.ReceiveRequestAndVerifySecurityAsyncResult.ProcessInnerItem(RequestContext innerItem, TimeSpan timeout)
at System.ServiceModel.Channels.SecurityChannelListener1.ReceiveItemAndVerifySecurityAsyncResult2.OnInnerReceiveDone()
at System.ServiceModel.Channels.SecurityChannelListener1.ReceiveItemAndVerifySecurityAsyncResult2.InnerTryReceiveCompletedCallback(IAsyncResult result)
at System.ServiceModel.Diagnostics.Utility.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
at System.ServiceModel.Channels.InputQueue1.AsyncQueueReader.Set(Item item)
at System.ServiceModel.Channels.InputQueue1.Dispatch()
at System.ServiceModel.Channels.InputQueue1.OnDispatchCallback(Object state)
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.Invoke2()
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.OnSecurityContextCallback(Object o)
at System.Security.SecurityContext.Run(SecurityContext securityContext, ContextCallback callback, Object state)
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.Invoke()
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.ProcessCallbacks()
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.CompletionCallback(Object state)
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.ScheduledOverlapped.IOCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* nativeOverlapped)
at System.ServiceModel.Diagnostics.Utility.IOCompletionThunk.UnhandledExceptionFrame(UInt32 error, UInt32 bytesRead, NativeOverlapped* nativeOverlapped)
at System.Threading.IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped pOVERLAP)
这个错误在服务端和客户端均能看到, 但大多数情况下是在服务端发生, 并且将这一的错误饭或到了客户端. 这个错误的字面意思就是找不到SCT. 但是既然能够肯到STC的UUID, 那么说明这个SCT确实存在.
参照上面STC生成的过程可以看到, STC是服务端和客户端进过多次的交涉最终才能形成. 这个STC只能在这个服务端和客户端之间传递, 并不能分享给第三方. 这是出于安全的考虑.
这个问题最常见于NLB的环境中.如果NLB设备或者软件没有启用Sticky Session. 在有多台WCF服务器的情况下, 客户端发起的请求很有可能被转移到另外一台服务器上. 由于STC并不会被第三方获取, 那么另外一个服务器上不会有相同的SCT. 因此服务端会认为这个STC并没有被注册.
问题二:
<ExceptionType>System.ServiceModel.Security.SecurityNegotiationException, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>
<Message>Cannot find the negotiation state for the context 'uuid-954e94c0-6e42-4816-b53f-bc75e18a9534-2'.</Message>
<StackTrace>
at System.ServiceModel.Security.NegotiationTokenAuthenticator`1.ProcessRequestCore(Message request)
at System.ServiceModel.Security.NegotiationTokenAuthenticator`1.NegotiationHost.NegotiationSyncInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)
at System.ServiceModel.Dispatcher.ChannelHandler.DispatchAndReleasePump(RequestContext request, Boolean cleanThread, OperationContext currentOperationContext)
at System.ServiceModel.Dispatcher.ChannelHandler.HandleRequest(RequestContext request, OperationContext currentOperationContext)
at System.ServiceModel.Dispatcher.ChannelHandler.AsyncMessagePump(IAsyncResult result)
at System.ServiceModel.Dispatcher.ChannelHandler.OnAsyncReceiveComplete(IAsyncResult result)
at System.ServiceModel.Diagnostics.Utility.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
at System.ServiceModel.Channels.InputQueue`1.AsyncQueueReader.Set(Item item)
at System.ServiceModel.Channels.InputQueue`1.Dispatch()
at System.ServiceModel.Channels.InputQueue`1.OnDispatchCallback(Object state)
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.Invoke2()
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.OnSecurityContextCallback(Object o)
at System.Security.SecurityContext.Run(SecurityContext securityContext, ContextCallback callback, Object state)
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.Invoke()
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.ProcessCallbacks()
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.CompletionCallback(Object state)
at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.ScheduledOverlapped.IOCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* nativeOverlapped)
at System.ServiceModel.Diagnostics.Utility.IOCompletionThunk.UnhandledExceptionFrame(UInt32 error, UInt32 bytesRead, NativeOverlapped* nativeOverlapped)
at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)
</StackTrace>
这个问题较多情况是发生在SPNego的阶段. 从上面介绍的步骤能够看到, SPNego的整个阶段很长,需要服务端和客户端进行多次的交流. 这这个交流的过程中, 任何一次请求被NLB递交到一个错误的服务器上都有可能造成这样的错误.
解决方案 :
造成上面问题的主要原因之一就是NLB的设备或者软件将客户端的请求递交到了一台错误的的服务器上. 要解决这一的问题, 最好的方法就是启用NLB上的Sticky Session. 在某些情形下, 如果NLB并不支持这种做法, 那么可以通过修改WCF配置来绕过这写问题.
为了解决第一个问题, 需要禁用Establishing Security Context. 禁用后, STC并不会被缓存起来, 也不会使用对称密钥, 而是改用非对称密钥, 而且每次都需要重新生成STC. 这种做法的副作用是降低WCF的性能.
<bindings> <wsHttpBinding> <binding name=”MyServiceBinding”> <security mode=”Message” > <message establishSecurityContext=”false” clientCredentialType=”Windows” /> </security> </binding> </wsHttpBinding> </bindings>
在解决第一个问题后, 仍然可能会遇到第二个问题. 因为默认情况下会服务端和客户端仍然会Negotiate Service Credential. 在这个过程中, 同样有可能会被NLB的行为中断从而引发问题. 可以通过禁用NegotiateServiceCredential来绕过这个问题.
<bindings> <wsHttpBinding> <binding name=”MyServiceBinding”> <security mode=”Message” >
<message establishSecurityContext=”false” NegotiateServiceCredential=”false” clientCredentialType=”Windows” /> </security> </binding> </wsHttpBinding> </bindings>
将属性设置为 true 需要客户端和服务支持 WS-Trust 和 WS-SecureConversation。 将属性设置为 false 时不需要 WS-Trust 或 WS-SecureConversation 受支持。对于 Windows 凭据,将此属性设置为 false 将导致基于 KerberosToken 的身份验证。 这要求客户端和服务都是 Kerberos 域的一部分。 此模式可与实现 OASIS Kerberos 令牌配置文件的 SOAP 堆栈交互操作。 将此属性为 true 会引起通过 SOAP 消息进行 SPNego 交换的 SOAP 协商. 如果将此属性设置为 false,并且将绑定配置为使用 Windows 作为客户端凭据类型,则必须将服务帐户与服务主体名称 (SPN) 相关联。 为此,请在 NETWORK SERVICE 帐户或 LOCAL SYSTEM 帐户下运行服务。 也可以使用 SetSpn.exe 工具为服务帐户创建一个 SPN。 不论何种情况,客户端都必须使用<servicePrincipalName> 元素中的正确 SPN,或者通过使用 EndpointAddress 构造函数来应用正确的 SPN。
除此之外, 还有下面的集中组合可以适用于NLB的环境中. 其中, BasicHttpBinding和wsHttpBinding均可以相互替换
• BasicHttpBinding - TransportCredentialOnly - Windows
• BasicHttpBinding - TransportCredentialOnly - Ntlm
• BasicHttpBinding - TransportWithMessageCredential - UserName
• BasicHttpBinding - TransportWithMessageCredential - Certificate
• wsHttpBinding - Transport - Ntlm
l BasicHttp – TransportCredentialOnly – Windows
Service – web.config
<basicHttpBinding> <binding name="basicN"> <security mode="TransportCredentialOnly"> <transport clientCredentialType="Windows"/> </security> </binding> </basicHttpBinding> <endpoint address="" binding="basicHttpBinding" contract="WindowsAuthTestService.IService1" bindingConfiguration="basicN" />
Client – app.config
<bindings> <basicHttpBinding> <binding name="BasicHttpBinding_IService1"> <security mode="TransportCredentialOnly"> <transport clientCredentialType="Windows" /> </security> </binding> </basicHttpBinding> </bindings> <client> <endpoint address="http://localhost/WindowsAuthTestService/Service1.svc" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService1" contract="ServiceReference1.IService1" name="BasicHttpBinding_IService1" /> </client>
l BasicHttp – TransportCredentialOnly – Ntlm
Service – web.config
<basicHttpBinding> <binding name="basicN"> <security mode="TransportCredentialOnly"> <transport clientCredentialType="Ntlm"/> </security> </binding> </basicHttpBinding> <endpoint address="" binding="basicHttpBinding" contract="WindowsAuthTestService.IService1" bindingConfiguration="basicN" />
Client – app.config
<bindings> <basicHttpBinding> <binding name="BasicHttpBinding_IService1"> <security mode="TransportCredentialOnly"> <transport clientCredentialType="Ntlm" /> </security> </binding> </basicHttpBinding> </bindings> <client> <endpoint address="http://pupanda-win8.fareast.corp.microsoft.com/WindowsAuthTestService/Service1.svc" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService1" contract="ServiceReference1.IService1" name="BasicHttpBinding_IService1" /> </client>
l BasicHttp – TransportWithMessageCredential – UserName
Service – web.config
<bindings> <basicHttpBinding> <binding name="b0"> <security mode="TransportWithMessageCredential"> <message clientCredentialType="UserName"/> </security> </binding> </basicHttpBinding> </bindings> <serviceBehaviors> <behavior name="sb"> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/> <serviceDebug includeExceptionDetailInFaults="true"/> <serviceCredentials> <userNameAuthentication userNamePasswordValidationMode="Windows" /> <serviceCertificate findValue="11 12 32 12" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySerialNumber"/> </serviceCredentials> </behavior> </serviceBehaviors>
Client – app.config
<bindings> <basicHttpBinding> <binding name="BasicHttpBinding_IService1"> <security mode="TransportWithMessageCredential" /> </binding> </basicHttpBinding> </bindings> <client> <endpoint address="https://server/TransportMessageService/Service1.svc" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService1" contract="ServiceReference1.IService1" name="BasicHttpBinding_IService1" /> </client>
BasicHttp – TransportWithMessageCredential – Certificate
Service – web.config
<basicHttpBinding> <binding name="b0"> <security mode="TransportWithMessageCredential"> <message clientCredentialType="Certificate"/> </security> </binding>
Client – app.config
<bindings> <basicHttpBinding> <binding name="BasicHttpBinding_IService1"> <security mode="TransportWithMessageCredential"> <message clientCredentialType="Certificate"/> </security> </binding> </basicHttpBinding> </bindings> <client> <endpoint address="https://server2/TransportMessageService/Service1.svc" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService1" behaviorConfiguration="cl" contract="ServiceReference1.IService1" name="BasicHttpBinding_IService1" /> </client> <behaviors> <endpointBehaviors> <behavior name="cl"> <clientCredentials> <clientCertificate findValue="31 7c d9 40 b5 ac b0 8e 99 99 00 00 12" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySerialNumber"/> </clientCredentials> </behavior> </endpointBehaviors> </behaviors>
l wsHttpBinding – Transport – Ntlm
Service – web.config
<wsHttpBinding> <binding name="wsTransportBinding"> <security mode="Transport"> <transport clientCredentialType="Ntlm" /> </security> </binding> </wsHttpBinding> Client – app.config <bindings> <wsHttpBinding> <binding name="WSHttpBinding_IService1"> <security mode="Transport"> <transport clientCredentialType="Ntlm" /> </security> </binding> </wsHttpBinding> </bindings> <client> <endpoint address="https://server/TransportMessageService/Service1.svc" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1" contract="ServiceReference1.IService1" name="WSHttpBinding_IService1"> <identity> <servicePrincipalName value="host/pupanda-server.fareast.corp.microsoft.com" /> </identity> </endpoint> </client>
参考文档 :
http://msdn.microsoft.com/en-us/library/vstudio/hh273122(v=vs.100).aspx
http://blogs.msdn.com/b/socaldevgal/archive/2007/08/15/why-is-wshttpbinding-secure-by-default-how-does-it-work.aspx
http://docs.oasis-open.org/ws-sx/ws-secureconversation/200512/ws-secureconversation-1.3-os.html