65.9K
CodeProject 正在变化。 阅读更多。
Home

WCF 4.0 路由服务的自定义身份验证和安全

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (4投票s)

2012 年 9 月 13 日

CPOL

8分钟阅读

viewsIcon

47381

downloadIcon

1325

在路由服务环境中定义安全上下文

引言

我在设计一些中央服务时遇到了 WCF 4 路由功能,这些服务将为我所有的客户端服务提供各种服务。我希望创建一个单一的入口点来通信所有这些服务,并出于可伸缩性原因将它们分开。WCF 路由功能似乎非常令人兴奋,它支持可配置的路由服务,您可以在 WCF 解决方案中使用它。它提供了基于内容的路由、协议桥接和错误处理功能。但在路由服务环境中定义它们的安全性时,我遇到了一些问题。

背景

如今,我正在构建一种架构,其中客户域终端的“服务 L”将为工作站 X、Y、Z 等提供一些服务。“服务 L”有很多东西可以提供给它的客户(例如数据库数据等)。

有趣的是,在某些情况下,它将从提供商端的某些服务中获取服务,这些服务已分发到多个独立的后端服务中,例如“服务 A”、“服务 B”。由于提供商域中的这些服务在外部不可用,因此有一个路由服务“Router service”,它使所有“服务 L”的客户都可以访问后端服务,并充当消费它们的桥梁。因此,所有“服务 L”将成为路由服务的客户端,它通过互联网向其发送请求,然后根据过滤器/策略将其路由到适当的后端服务。

服务 L 和客户端 X/Y/Z 之间的安全已经建立,我将不讨论这部分。我非常关心我剩下的部分,即路由环境中的安全性。

这部分路由是基于 WCF 4.0 路由功能开发的。但是我没有找到将消息转发到后端服务的安全上下文的任何好解决方案。

传输级别和消息级别安全

WCF 允许我们在两个级别启用安全:传输级别或消息级别。让我在这里解释一下这两个级别之间的区别

如果我们使用传输级别安全,那么整个通信(客户端和服务器之间交换的所有信息)都将被加密。

如果我们使用消息级别安全,那么只有 SOAP 消息的内容被加密,而 SOAP 消息的其余部分保持未加密。

因此,在配置文件中,在编写绑定配置时

  • 使用 Security mode=Message 意味着消息(或其部分)被加密和签名,并且可以在不安全的传输上发送。
  • 使用 Security mode=Transport 意味着通信通道被加密,而不仅仅是消息。

传输速度更快,但意味着数据仅在传输过程中是安全的 - 如果您必须使用中间件(例如 WCF 4 路由服务),那么它可以看到明文消息 - 使用消息安全,消息从发送者到接收者都是安全的。传输级别和消息级别安全可以确保隔离、可靠性和证书。但是,并非所有通信都需要同时具备这三个功能。安全性通常是可取的,但可以禁用。加密也可以激活以确保隐私。

为什么不使用传输安全?!!

WCF 路由基于消息级别而不是传输层路由。因此,传输层上的身份验证无法被路由器正确转发,并且没有直接的方法可以让这些安全断言从客户端流向后端(绕过路由服务)。HTTPS/SSL 等传输层解决方案只能用于点对点而不是端到端的情况。因此,您无法在多个节点(客户端、路由器和服务器)之间建立 HTTPS/SSL 连接。

这里,路由服务如 消费者 --> 路由服务 --> 后端服务,那么我需要确保之间启用了传输安全

  • 消费者 -> 路由服务
  • 路由服务 -> 后端服务

据此,消息将在从消费者到路由服务的传输过程中被加密。当消息进入路由服务时,它将被解密,在将消息发送到后端服务时,它将在传输过程中再次被加密,并在后端服务处解密。因此,在这种解决方案中,消息将被加密和解密两次。但由于客户端和服务之间存在中间系统;每个中间点都必须通过新的 SSL 连接转发消息。因此,传输安全不适用于

  • 您正在将消息直接从应用程序发送到 WCF 服务,并且消息不会通过中间系统路由
  • 服务和客户端都位于内网之外。

因此,我将在我的场景中避免使用传输安全。

为什么不使用消息安全?!!

消息安全与传输无关,因此可以与任何传输协议一起使用。当您使用基于消息的安全时,这是在 SOAP 消息上完成的(因此加密发生在服务级别而不是传输级别),这提供了一个端到端的安全解决方案。因此,消费者发送一个加密的消息(默认情况下头部未加密),路由服务只是将加密的消息路由到后端服务,后端服务解密消息。因此,消息安全直接加密和签名消息,中间件不会破坏安全性。但是,与传输安全相比,基于消息的安全会带来性能损失,因为加密后的消息会变得大得多。

因此,使用带有路由器和后端服务中 Windows 凭据的消息安全将起作用,因为它支持委派(Kerberos)。并且客户端需要访问该身份对象。另一方面,它也不允许我转发安全上下文给

  • 用户名和密码
  • X.509
  • 联合凭据

这意味着路由器可以配置为强制执行消息安全,但服务必须配置为禁用安全,并且无法访问用户的身份等安全上下文。

那么,我将做什么

  1. 添加终结点行为的扩展以添加标头以发送用户名和密码(也可以以加密方式发送)。
  2. 创建自定义绑定以在客户端和路由服务之间加密和压缩消息。
  3. 在将消息从路由服务转发到后端服务时,在绑定中使用常规安全。
  4. 创建一个身份验证管理器,通过用户名和密码(针对数据库)进行身份验证,以避免匿名调用。

添加终结点行为的扩展以添加标头以发送用户名和密码

那么,让我们在这里定义终结点行为的扩展

public class CentralSessionEndpointBehavior : IEndpointBehavior
{
    public CentralSessionEndpointBehavior()
    {

    }

    public CentralSessionEndpointBehavior(string user, string password)
    {
        this.User = user;
        this.Password = password;
    }

    public void AddBindingParameters(ServiceEndpoint serviceEndpoint, 
       System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    { }

    public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint, 
      System.ServiceModel.Dispatcher.ClientRuntime behavior)
    {
        //Add the inspector
        behavior.MessageInspectors.Add(new CentralSessionClientMessageInspector
                                      (this.User, this.Password));
    }
    public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint, 
      System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
    { }

    public void Validate(ServiceEndpoint serviceEndpoint)
    { }

    public string User { get; set; }
    public string Password { get; set; }
}

public class CentralSessionClientMessageInspector : 
            BehaviorExtensionElement, IClientMessageInspector
{
    public CentralSessionClientMessageInspector()
    {

    }

    public CentralSessionClientMessageInspector(string user, string password)
    {
        this.User = user;
        this.Password = password;
    }

    public static event EventHandler<CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message>> PreRequestingService;

    private void InvokePreRequestingService(CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message> e)
    {
        EventHandler<CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message>> handler = PreRequestingService;
        if (handler != null) handler(this, e);
    }

    public static event EventHandler<CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message>> PostRequestingService;

    private void InvokePostRequestingService(CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message> e)
    {
        EventHandler<CentralSessionDataEventArgs
                <System.ServiceModel.Channels.Message>> handler = PostRequestingService;
        if (handler != null) handler(this, e);
    }

    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
    private string _user;

    public string User
    {
        get { return _user; }
        set { _user = value; }
    }

    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
    private string _password;

    public string Password
    {
        get { return _password; }
        set { _password = value; }
    }

    public override Type BehaviorType
    {
        get { return typeof(CentralSessionEndpointBehavior); }
    }

    protected override object CreateBehavior()
    {
        return new CentralSessionEndpointBehavior();
    }

    #region IClientMessageInspector Members
    public void AfterReceiveReply
        (ref System.ServiceModel.Channels.Message reply, object correlationState)
    {
        InvokePostRequestingService(new CentralSessionDataEventArgs<Message>(reply));
    }
    public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, 
           System.ServiceModel.IClientChannel channel)
    {
        CredentialHelper.SetSessionData(_user, _password, ref request);
        InvokePreRequestingService(new CentralSessionDataEventArgs<Message>(request));
        return null;
    }

    #endregion
}

public class CentralSessionDataEventArgs<TData> : EventArgs where TData : class
{
    readonly TData _data;

    public CentralSessionDataEventArgs(TData data)
    {
        if (data == null)
        {
            throw new ArgumentNullException("data");
        }
        this._data = data;
    }

    public TData Data
    {
        get { return _data; }
    }

    public override string ToString()
    {
        return _data.ToString();
    }
}

在这里,我将在 BeforeSendRequest 中发送用户名和密码

public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, 
              System.ServiceModel.IClientChannel channel)
{
    CredentialHelper.SetSessionData(_user, _password, ref request);
    InvokePreRequestingService(new CentralSessionDataEventArgs<Message>(request));
    return null;
} 

SetSessionData 方法将在每次从客户端到路由服务的请求发送时添加用户名和密码。这是 CredentialHelper 类的定义

public class CredentialHelper
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private const string HnForUserName = "UserName";

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private const string HNamespaceForUserName = @"http://UserName.url";

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private const string HnForPassword = "Password";

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private const string HNamespaceForPassword = @"http://Password.url";

    public static CentralRequestSession GetSessionData(Message request)
    {
        var user = string.Empty;
        var pass = string.Empty;
        if (request.Headers.Any(h => h.Name.Equals(HnForUserName)))
            user = request.Headers.GetHeader<string>
                   (HnForUserName, HNamespaceForUserName, HnForUserName);
        if (request.Headers.Any(h => h.Name.Equals(HnForPassword)))
            pass = request.Headers.GetHeader<string>
                   (HnForPassword, HNamespaceForPassword, HnForPassword);
        return new CentralRequestSession(user, pass);
    }

    public static void SetSessionData
           (string userName, string password, ref  Message request)
    {
        var userHeader = new MessageHeader<string> 
                         { Actor = HnForUserName, Content = userName };
        //Creating an untyped header to add to the WCF context
        System.ServiceModel.Channels.MessageHeader unTypedHeaderForUser = 
          userHeader.GetUntypedHeader(HnForUserName, HNamespaceForUserName);
        //Add the header to the current request
        request.Headers.Add(unTypedHeaderForUser);

        var passwordHeader = new MessageHeader<string> 
                             { Actor = HnForPassword, Content = password };
        //Creating an untyped header to add to the WCF context
        System.ServiceModel.Channels.MessageHeader unTypedHeaderForPassword = 
          passwordHeader.GetUntypedHeader(HnForPassword, HNamespaceForPassword);
        //Add the header to the current request
        request.Headers.Add(unTypedHeaderForPassword);
    }
}

创建自定义绑定以在客户端和路由服务之间加密和压缩消息

要压缩线路上消息,我使用了来自Microsoft 提供的示例的 Gzip。我做了一些更改,添加了加密/解密代码。所以,我将在这里重点介绍那些代码部分。

因此,在客户端和路由服务两端,我都需要先加密数据,然后在通过线路发送数据时进行压缩

//Helper method to compress an array of bytes
static ArraySegment<byte> CompressBuffer(ArraySegment<byte> buffer, 
  BufferManager bufferManager, int messageOffset, CompressionAlgorithm compressionAlgorithm)
{
    buffer = Cryptographer.EncryptBuffer(buffer, bufferManager, messageOffset);

    var memoryStream = new MemoryStream();
    
    using (Stream compressedStream = compressionAlgorithm == CompressionAlgorithm.GZip ? 
        (Stream)new GZipStream(memoryStream, CompressionMode.Compress, true) :
        (Stream)new DeflateStream(memoryStream, CompressionMode.Compress, true))
    {
        compressedStream.Write(buffer.Array, buffer.Offset, buffer.Count);
    }

    byte[] compressedBytes = memoryStream.ToArray();
    int totalLength = messageOffset + compressedBytes.Length;
    byte[] bufferedBytes = bufferManager.TakeBuffer(totalLength);

    Array.Copy(compressedBytes, 0, bufferedBytes, messageOffset, compressedBytes.Length);

    bufferManager.ReturnBuffer(buffer.Array);
    buffer = new ArraySegment<byte>(bufferedBytes, messageOffset, compressedBytes.Length);
   
    return buffer;
}

在接收时,我需要先解压,然后再解密以获得原始消息

//Helper method to decompress an array of bytes
static ArraySegment<byte> DecompressBuffer(ArraySegment<byte> buffer, 
        BufferManager bufferManager, CompressionAlgorithm compressionAlgorithm)
{
    var memoryStream = new MemoryStream(buffer.Array, buffer.Offset, buffer.Count);
    var decompressedStream = new MemoryStream();
    var totalRead = 0;
    const int blockSize = 1024;
    byte[] tempBuffer = bufferManager.TakeBuffer(blockSize);
    using (Stream compressedStream = compressionAlgorithm == CompressionAlgorithm.GZip ?
        (Stream)new GZipStream(memoryStream, CompressionMode.Decompress) :
        (Stream)new DeflateStream(memoryStream, CompressionMode.Decompress))
    {
        while (true)
        {
            int bytesRead = compressedStream.Read(tempBuffer, 0, blockSize);
            if (bytesRead == 0)
                break;
            decompressedStream.Write(tempBuffer, 0, bytesRead);
            totalRead += bytesRead;
        }
    }
    bufferManager.ReturnBuffer(tempBuffer);

    byte[] decompressedBytes = decompressedStream.ToArray();
    byte[] bufferManagerBuffer = bufferManager.TakeBuffer
                                 (decompressedBytes.Length + buffer.Offset);
    Array.Copy(buffer.Array, 0, bufferManagerBuffer, 0, buffer.Offset);
    Array.Copy(decompressedBytes, 0, bufferManagerBuffer, 
               buffer.Offset, decompressedBytes.Length);

    buffer = new ArraySegment<byte>(bufferManagerBuffer, 
                                    buffer.Offset, decompressedBytes.Length);
    buffer = Cryptographer.DecryptBuffer(buffer, bufferManager);
    bufferManager.ReturnBuffer(buffer.Array);

    return buffer;
}

Cryptographer 类负责在此处加密和解密数据。我将所有 Gzip 和加密内容保存在 GlobalCommonLib 程序集中,这是我在客户端配置文件中定义自定义绑定的样子

在这里,我重点介绍了自定义绑定定义的这部分。我使用了 httpTransport 来启用通过 HTTP 发送/接收数据,这在客户端和路由服务之间被使用。

在路由服务配置文件中,使用相同的配置。看一下服务终结点定义

<services>
    <service behaviorConfiguration="RoutingBehavior" 
     name="System.ServiceModel.Routing.RoutingService">
        <endpoint binding="customBinding" 
           bindingConfiguration="secureCustomBinding"
           name="RoutingEndpoint" 
           contract="System.ServiceModel.Routing.IRequestReplyRouter" />
        <endpoint address="mex"
                  binding="mexHttpBinding"
                  contract="IMetadataExchange" />
    </service>
</services>

这是路由服务端的绑定定义

<extensions>
  <bindingElementExtensions>
    <add name="compression" 
     type="GlobalCommonLib.GZipMessageEncodingElement, GlobalCommonLib"/>
  </bindingElementExtensions>
</extensions>
<bindings>
   <customBinding>
    <binding name="secureCustomBinding" receiveTimeout="infinite" sendTimeout="01:00:00">
      <compression innerMessageEncoding="textMessageEncoding" compressionAlgorithm="GZip"/>
      <httpTransport/>
    </binding>
  </customBinding>
</bindings>

在将消息从路由服务转发到后端服务时,在绑定中使用常规安全

在路由服务中,我定义了基于客户端用于调用服务的 ServiceContract 的路由过滤器。为此,我在这里定义了一个自定义路由过滤器。

<client>
  <endpoint name="ServiceA"
            address="net.tcp://:9092/ServiceA"
            binding="netTcpBinding" bindingConfiguration="securityNetBinding"
            contract="*"/>
  <endpoint name="ServiceB"
            address="net.tcp://:9093/ServiceB"
            binding="netTcpBinding" bindingConfiguration="securityNetBinding"
            contract="*"/>
</client>
<routing>
  <filters>
    <filter name="RegisterServiceAFilter" filterData="IServiceA" 
       customType="RoutingServicePOC.Filter.ActionMessageFilter, RoutingServicePOC"  
       filterType="Custom" />
    <filter name="RegisterServiceBFilter" filterData="IServiceB" 
       customType="RoutingServicePOC.Filter.ActionMessageFilter, RoutingServicePOC" 
       filterType="Custom" />        
  </filters>
  <filterTables>
    <filterTable name="CentralRoutingTable">
      <add filterName="RegisterServiceAFilter" endpointName="ServiceA" priority="0"/>
      <add filterName="RegisterServiceBFilter" endpointName="ServiceB" priority="0"/>
    </filterTable>
  </filterTables>
</routing>
<behaviors>
      <serviceBehaviors>
    <behavior name="RoutingBehavior">
      <routing routeOnHeadersOnly="true" filterTableName="CentralRoutingTable" />
      <serviceMetadata httpGetEnabled="true" />
      <serviceDebug includeExceptionDetailInFaults="true" />
    </behavior>
  </serviceBehaviors>
</behaviors>

我配置了一个 netTcp 绑定,用于路由服务和后端服务之间。

<netTcpBinding>
<binding name="securityNetBinding" receiveTimeout="00:10:00" 
 sendTimeout="00:10:00" maxReceivedMessageSize="2147483647">
  <readerQuotas maxDepth="200" maxStringContentLength="2147483647" maxArrayLength="16384"
    maxBytesPerRead="4096" maxNameTableCharCount="16384" />
  <security mode="Transport">
    <transport protectionLevel="EncryptAndSign" />
  </security>
</binding>
</netTcpBinding>

创建一个身份验证管理器,通过用户名和密码进行身份验证,以避免匿名调用

我在 ServiceCommonLib 中定义了一个自定义身份验证管理器,它被用于后端服务 - 服务 A 和服务 B。

public class CentralUserIdentity : IIdentity
{
    private readonly CentralRequestSession _session;
    public CentralUserIdentity(CentralRequestSession session)
    {
        this._session = session;
    }

    public string Name
    {
        get { return _session.UserName; }
    }

    public string AuthenticationType
    {
        get { return "Central"; }
    }

    public bool IsAuthenticated
    {
        get { return true; }
    }

    public CentralRequestSession Session
    {
        get { return _session; }
    }
}

public class CentralPrincipal : IPrincipal
{
    readonly IIdentity _identity;
    string[] _roles = null;

    public CentralPrincipal(IIdentity identity)
    {
        this._identity = identity;
    }

    public static CentralPrincipal Current
    {
        get
        {
            return Thread.CurrentPrincipal as CentralPrincipal;
        }
    }

    public IIdentity Identity
    {
        get { return _identity; }
    }

    public string[] Roles
    {
        get
        {
            //Findout Role and set here 
            return _roles;
        }
    }

    public bool IsInRole(string role)
    {
        //Findout Role and set here 
        return true;
    }       
}

public class CentralAuthenticationManager : ServiceAuthenticationManager
{
    public override ReadOnlyCollection<IAuthorizationPolicy> Authenticate(
       ReadOnlyCollection<IAuthorizationPolicy> authPolicy, 
                          Uri listenUri, ref Message message)
    {
        var session = CredentialHelper.GetSessionData(message);
        CheckCredentials(session);

        var identity = new CentralUserIdentity(session);
        IPrincipal user = new CentralPrincipal(identity);
        message.Properties["Principal"] = user;

        return authPolicy;
    }

    public void CheckCredentials(CentralRequestSession credentials)
    {
        System.Console.WriteLine
               ("Checking Credentials for {0}..........", credentials.UserName);
        // check the user and password against a database; 
        // if not match 
        // throw new AuthenticationException("Incorrect credentials!");        
        System.Console.WriteLine( "{0} is Valid!!", credentials.UserName);
    }
}

为此,我扩展了 ServiceAuthenticationManager,您需要在配置文件中配置此自定义身份验证管理器。在 Authenticate 方法中,我检索了在发送请求到路由服务时发送的用户名和密码的标头。路由器已将该消息转发到后端服务,在后端服务中,我可以检索这些内容并 against 任何数据库进行身份验证。

var session = CredentialHelper.GetSessionData(message);
CheckCredentials(session);

在 Check Credential 中,您可以进行验证,如果您愿意,可以 against 数据库进行验证。让我们看一下配置文件

Click to enlarge image

在这里,我将自定义身份验证管理器添加到了服务行为配置部分,该部分已作为服务行为在后端服务中使用。

好了,就这些。谢谢大家阅读。祝您有美好的一天!!

参考文献

历史

  • 2012 年 9 月 13 日:初始版本
© . All rights reserved.