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

WCF 客户端服务器应用程序,带自定义身份验证、授权、加密和压缩 – 第 2 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (37投票s)

2011 年 3 月 8 日

CPOL

7分钟阅读

viewsIcon

100251

downloadIcon

4

HTTP - 无 IIS;身份验证 - 无 SSL/X509 证书;加密 - 请求使用 RSA+AES,响应使用 AES;压缩 - 请求/响应均使用 GZip。

目录

本文档详细介绍了由于篇幅原因未在 第 1 部分 中处理的实现。

概述

我们有 3 个部分,将产生 3 个项目 – ClientServerCommon(服务器和客户端的通用库)。

首先,我们快速浏览一下类,并解释它们的作用——尽管我认为这已经很清楚了。


(点击图表放大)

让我们从通用部分开始。
ServerInfo 包含服务器的日期/时间以及公钥。
Credentials 包含 UserName/Password/Expires 属性,这些属性会被序列化并发送到服务器;其他属性(关于日期/时间)是 static 属性,在开始时从服务器信息初始化,并在请求前不久用于设置 Expires 属性。

现在移到服务器的 AppServer static 类;它的作用是创建/启动/停止服务,该服务将为客户端提供 ServerInfo

现在是客户端,我们有 AppClient static 类;它的作用是获取 ServerInfo;它还托管客户端的凭据。

身份验证

我们在客户端开始,将凭据添加到消息头。我们通过使用一个实现 IClientMessageInspectorBehaviorExtensionElement 来实现这一点,如下所示:

public class ClientMessageInspector : BehaviorExtensionElement,
			IClientMessageInspector, IEndpointBehavior
{
    public object BeforeSendRequest(ref Message request, IClientChannel channel)
    {
        request.Headers.Add(AppClient.Credentials.ToMessageHeader());
        return null;
    }
    //... other methods ...
}

以及在配置文件(App.config)中:

<behaviorExtensions>
  <add name="ChallengeClientMessageInspector"
	type="Challenge.Client.ClientMessageInspector, Client" />
</behaviorExtensions>

接下来是编码机制,其中涉及加密和压缩,但我们稍后会讨论。

现在到服务器:我们需要从 ServiceAuthenticationManager 派生一个类,并重写 Authenticate 方法[3];在这里,如果 CheckCredentials 成功并且没有抛出任何异常,我们就从凭据中进行用户身份验证。

public class ChallengeAuthenticationManager : ServiceAuthenticationManager
{
    public override ReadOnlyCollection<IAuthorizationPolicy>
	Authenticate(ReadOnlyCollection<IAuthorizationPolicy> authPolicy,
	Uri listenUri, ref Message message)
    {
        Credentials credentials = Credentials.FromMessageHeader(message);
        CheckCredentials(credentials);

        ChallengeIdentity identity = new ChallengeIdentity(credentials.UserName);
        IPrincipal user = new ChallengePrincipal(identity);
        message.Properties["Principal"] = user;

        return authPolicy;
    }

    public void CheckCredentials(Credentials credentials)
    {
        if (credentials.Expires < DateTime.Now)
            throw new AuthenticationException("Credentials expired!");

        // check the user and password against a database;
        // if not match
        // throw new AuthenticationException("Incorrect credentials!");
    }
    //... other methods ...
}
<behavior name="ChallengeBehavior">
  <serviceAuthenticationManager
   serviceAuthenticationManagerType=
	"Challenge.Server.ChallengeAuthenticationManager,
	Server"/>
</behavior>

现在让我们解释 ChallengeIdentityChallengePrincipal:前者实现 IIdentity,后者实现 IPrincipal

两者都非常容易实现;但是,我将在下面重点介绍 ChallengePrincipal,因为我们的授权就在其中。

Authorization

通过授权,我们可以确定已验证的用户中哪些有权执行操作合同,权限如下所示:

[PrincipalPermission(SecurityAction.Demand, Role = "Admin")]
public int Sum(int a, int b)
{
    return a + b;
}

仅当已验证用户属于“Admin”角色时,才允许其执行上述方法——这是使用 IsInRole 方法检查的。

public class ChallengePrincipal: IPrincipal
{
    IIdentity identity;
    string[] roles = null;

    public bool IsInRole(string role)
    {
        EnsureRoles();
        return roles != null ? roles.Contains(role) : false;
    }

    protected virtual void EnsureRoles()
    {
        // get the roles for the identity from a database (or other source)
        // and cache them for subsequent requests

        // here we'll add a few roles as example
        roles = new string[] { "User", "Admin", "Manager" };
    }
    //... other methods ...
}

为了使授权生效,我们需要创建自己的授权策略(并在配置文件中指定),如下所示:

public class ChallengeAuthorizationPolicy : IAuthorizationPolicy
{
    public bool Evaluate(EvaluationContext evaluationContext, ref object state)
    {
        IPrincipal user = OperationContext.Current.IncomingMessageProperties
			["Principal"] as IPrincipal;
        evaluationContext.Properties["Principal"] = user;
        evaluationContext.Properties["Identities"] = new List<IIdentity>
				{ user.Identity };

        return false;
    }
    //... other methods ...
}
<behavior name="ChallengeBehavior">
  <serviceAuthorization principalPermissionMode="Custom" >
    <authorizationPolicies>
      <add policyType='Challenge.Server.ChallengeAuthorizationPolicy, Server' />
    </authorizationPolicies>
  </serviceAuthorization>
</behavior>

加密

加密/解密由通用的 Cryptographer/ClientCryptographer/ServerCryptographer 提供;需要为服务器和客户端提供单独的加密器,因为它们执行不同的加密/解密。

客户端 - 加密请求
  • 获取消息的 ID,将其与密钥关联,并一直保存到收到服务器的响应——我们需要该密钥来解密来自服务器的响应。
  • 获取要加密的元素(如果只加密凭据,则为 Credentials 元素;如果加密整个消息,则为第一个节点),并加密该元素。
  • 使用服务器的 public 密钥[2] 加密 AES 密钥,并将其添加到加密节点(使用名称 KeyElementName——它对于服务器和客户端都是常量)。
  • 将加密元素的 ID 设置为消息的 ID(以便在解密之前知道消息的 ID)。
  • 用加密节点替换原始节点。
public static void Encrypt(XmlDocument xmlDoc, string elementToEncrypt) //[8]
{
    XmlNodeList elementsToEncrypt = xmlDoc.GetElementsByTagName(elementToEncrypt);
    if (elementsToEncrypt.Count == 0)
        return;
    AesCryptoServiceProvider aesServiceProvider =
				new AesCryptoServiceProvider();
    aesServiceProvider.KeySize = 256;
    aesServiceProvider.GenerateKey();

    XmlNode idNode = xmlDoc.GetElementsByTagName("a:MessageID")[0];
    string id = idNode.InnerText;
    AesKeys.Add(id, aesServiceProvider.Key);

    XmlElement xmlElementToEncrypt = (XmlElement)elementsToEncrypt[0];
    EncryptedXml encryptedXml = new EncryptedXml();
    byte[] encryptedElement = encryptedXml.EncryptData
			(xmlElementToEncrypt, aesServiceProvider, Content);

    EncryptedData encryptedData = new EncryptedData();
    encryptedData.Type = EncryptedXml.XmlEncElementUrl;
    encryptedData.EncryptionMethod =
		new EncryptionMethod(EncryptedXml.XmlEncAES256Url);

    EncryptedKey encryptedKey = new EncryptedKey();
    encryptedKey.CipherData = new CipherData
     (EncryptedXml.EncryptKey(aesServiceProvider.Key, RsaServiceProvider, Content));
    encryptedKey.EncryptionMethod =
		new EncryptionMethod(EncryptedXml.XmlEncRSA15Url);
    encryptedData.KeyInfo = new KeyInfo();
    encryptedKey.KeyInfo.AddClause(new KeyInfoName(KeyElementName));
    encryptedData.KeyInfo.AddClause(new KeyInfoEncryptedKey(encryptedKey));
    encryptedData.CipherData.CipherValue = encryptedElement;
    encryptedData.Id = id;
    EncryptedXml.ReplaceElement(xmlElementToEncrypt, encryptedData, Content);
}
服务器 - 解密请求
  • 将传入文档视为已加密的文档;
  • KeyElementName(正如我之前所说,它对于客户端和服务器都是常量)与服务器的私钥关联,该私钥将用于解密客户端 AES 密钥;
  • 将消息的 ID 与密钥关联——这在加密响应时用于查找客户端密钥;
  • 将密码添加到禁止列表中(或者,如果已存在,则抛出 SecurityException);
  • 解密文档。
public static void Decrypt(XmlDocument xmlDoc) //[8]
{
    XmlNodeList encryptedElements = xmlDoc.GetElementsByTagName("EncryptedData");
    if (encryptedElements.Count == 0)
        return;

    EncryptedXml encryptedXml = new EncryptedXml(xmlDoc);
    encryptedXml.AddKeyNameMapping(KeyElementName, RsaServiceProvider);

    EncryptedData eData = new EncryptedData();
    XmlElement encryptedElement = (XmlElement)encryptedElements[0];
    eData.LoadXml(encryptedElement);

    SymmetricAlgorithm a = encryptedXml.GetDecryptionKey
		(eData, eData.EncryptionMethod.KeyAlgorithm);

    //here the aes service provider gets the client key![6]
    AesKeys.Add(eData.Id, a.Key);

    string keyHash = a.Key.ComputeHash();
    if (AesBannedKeys.ContainsKey(keyHash))
        throw new SecurityException("Password reuse before ban expiration!");
    else
        AesBannedKeys.Add(keyHash, DateTime.Now.AddMilliseconds
			(AesBannedKeysExpiresTimeSpan));

    encryptedXml.DecryptDocument();
}
服务器 - 加密响应
  • 找到要加密的元素。
  • 根据消息的 ID(RelatesTo 元素)获取在请求中保存的密钥。
  • 加密消息。
  • 将消息的 ID 设置为加密数据(因此客户端知道要使用哪个密钥——如果只发送同步请求,那么我们只需要使用最后一个密钥,但我们必须同时考虑两种情况)。
  • 用加密的元素替换原始元素。
public static void Encrypt(XmlDocument xmlDoc, string elementToEncrypt) //[9]
{
    XmlNodeList elementsToEncrypt = xmlDoc.GetElementsByTagName(elementToEncrypt);
    if (elementsToEncrypt.Count == 0)
        return;

    AesCryptoServiceProvider aesServiceProvider = new AesCryptoServiceProvider();

    XmlNode idNode = xmlDoc.GetElementsByTagName("a:RelatesTo")[0];
    string id = "";
    if (idNode != null)
    {
        id = idNode.InnerText;
    }
    if (AesKeys.ContainsKey(id))
    {
        aesServiceProvider.Key = AesKeys[id];
    }

    XmlElement xmlElementToEncrypt = (XmlElement)elementsToEncrypt[0];

    EncryptedXml encryptedXml = new EncryptedXml();
    byte[] encryptedElement = encryptedXml.EncryptData
		(xmlElementToEncrypt, aesServiceProvider, Content);

    EncryptedData encryptedData = new EncryptedData();
    encryptedData.Type = EncryptedXml.XmlEncElementUrl;
    encryptedData.EncryptionMethod = new EncryptionMethod
				(EncryptedXml.XmlEncAES256Url);
    encryptedData.CipherData.CipherValue = encryptedElement;
    encryptedData.Id = id;
    EncryptedXml.ReplaceElement(xmlElementToEncrypt, encryptedData, Content);
}
客户端 - 解密响应
  • 找到加密的元素并将其加载为加密数据。
  • 根据加密数据的 ID 获取 AES 算法的密钥。
  • 解密加密的元素并将其替换到文档中。
public static void Decrypt(XmlDocument xmlDoc) //[9]
{
    XmlNodeList encryptedElements = xmlDoc.GetElementsByTagName("EncryptedData");
    if(encryptedElements.Count == 0)
        return;
    XmlElement encryptedElement = (XmlElement)encryptedElements[0];

    EncryptedData encryptedData = new EncryptedData();
    encryptedData.LoadXml(encryptedElement);

    AesCryptoServiceProvider aesServiceProvider = new AesCryptoServiceProvider();
    if (AesKeys.ContainsKey(encryptedData.Id))
    {
        aesServiceProvider.Key = AesKeys[encryptedData.Id];
    }

    EncryptedXml encryptedXml = new EncryptedXml();
    encryptedXml.ReplaceData(encryptedElement,
	encryptedXml.DecryptData(encryptedData, aesServiceProvider));
}

由于我们要在消息编码器中执行此操作,因此我们无法获取 XML 文档形式的消息,而是获取一个数组段;因此,对于客户端和服务器,我们需要对数组段执行加密(实际上是将数组段转换为 XML 文档,然后应用上述方法)。

public static ArraySegment<byte> EncryptBuffer(ArraySegment<byte> buffer,
	BufferManager bufferManager, int messageOffset,
	string elementToEncrypt = "s:Envelope")
{
    byte[] bufferedBytes;
    byte[] encryptedBytes;
    XmlDocument xmlDoc = new XmlDocument();

    using (MemoryStream memoryStream = new MemoryStream
		(buffer.Array, buffer.Offset, buffer.Count))
    {
        xmlDoc.Load(memoryStream);
    }

    ClientCryptographer.Encrypt(xmlDoc, elementToEncrypt);
    encryptedBytes = Encoding.UTF8.GetBytes(xmlDoc.OuterXml);
    bufferedBytes = bufferManager.TakeBuffer(encryptedBytes.Length);
    Array.Copy(encryptedBytes, 0, bufferedBytes, 0, encryptedBytes.Length);
    bufferManager.ReturnBuffer(buffer.Array);

    ArraySegment<byte> byteArray = new ArraySegment<byte>
	(bufferedBytes, messageOffset, encryptedBytes.Length);
    return byteArray;
}

创建一个新的 AesCryptoServiceProvider[1] 并生成一个新密钥。

压缩

对于压缩/解压缩,我们将使用 Microsoft 的示例;解压缩方法是相同的;我更改了压缩,因为存在一个严重的错误[7]

public static ArraySegment<byte> CompressBuffer
	(ArraySegment<byte> buffer, BufferManager bufferManager, int messageOffset)
{
    byte[] bufferedBytes, compressedBytes;
    using (MemoryStream memoryStream = new MemoryStream())
    {
        memoryStream.Write(buffer.Array, 0, messageOffset);

        using (GZipStream gzStream = new GZipStream
		(memoryStream, CompressionMode.Compress, true))
        {
            gzStream.Write(buffer.Array, messageOffset, buffer.Count);
        }

        compressedBytes = memoryStream.ToArray();
        bufferedBytes = bufferManager.TakeBuffer(compressedBytes.Length);

        Array.Copy(compressedBytes, 0, bufferedBytes, 0, compressedBytes.Length);
        bufferManager.ReturnBuffer(buffer.Array);
    }
    //ArraySegment<byte> byteArray = new ArraySegment<byte>
    //(bufferedBytes, messageOffset, bufferedBytes.Length - messageOffset);//bug here
    ArraySegment<byte> byteArray =
	new ArraySegment<byte>(bufferedBytes, messageOffset, compressedBytes.Length);
    return byteArray;
}

编码

仅仅通过查看附加的代码可能不容易理解这一点;因此,我将通过一些修改来展示它,使其更容易理解。

首先,我将只展示客户端的编码(没有必要展示服务器,因为它类似);其次——我将通用部分与客户端结合起来(因为实际上一切都是这样开始的——先考虑客户端,然后是服务器,之后是它们相同的ส่วน,因此“common”项目应运而生)。

首先,我们有基于 abstractMessageEncoderClientEncoder,其中最重要的方法是 WriteMessageReadMessage;在这里,我们使用 加密压缩 章节中介绍的方法;ContentCompressionContentEncryption 属性由创建编码器的类初始化,实际上是配置文件中 contentCompressioncontentEncryption 属性的传播(稍后,我们将看到如何从 App.config 中检索它)。

public class ClientMessageEncoder : MessageEncoder //[4]
{
    public override ArraySegment<byte> WriteMessage(Message message,
	int maxMessageSize, BufferManager bufferManager, int messageOffset)
    {
        ArraySegment<byte> buffer = innerEncoder.WriteMessage
		(message, maxMessageSize, bufferManager, messageOffset);
        switch (ContentEncryption)
        {
            case ContentEncryptionType.All:
                {
                    buffer = ClientCryptographer.EncryptBuffer(buffer,
				bufferManager, messageOffset);
                    break;
                }
            case ContentEncryptionType.Credentials:
                {
                    buffer = ClientCryptographer.EncryptBuffer(buffer,
				bufferManager, messageOffset,
				ContentEncryptionType.Credentials.ToString());
                    break;
                }
        }

        if (ContentCompression != ContentCompressionType.None)
            buffer = CompressBuffer(buffer, bufferManager, messageOffset);

        return buffer;
    }

    public override Message ReadMessage(ArraySegment<byte> buffer,
		BufferManager bufferManager, string contentType)
    {
        ArraySegment<byte> workingBuffer = buffer;

        if (ContentCompression != ContentCompressionType.None)
            buffer = DecompressBuffer(buffer, bufferManager);

        if (ContentEncryption != ContentEncryptionType.None)
            buffer = ClientCryptographer.DecryptBuffer(buffer, bufferManager);

        Message returnMessage = innerEncoder.ReadMessage(buffer, bufferManager);
        returnMessage.Properties.Encoder = this;
        return returnMessage;
    }
    //... other methods ...
}

ClientMessageEncoder 由一个工厂编码器使用:

public class ClientMessageEncoderFactory : MessageEncoderFactory //[13]
{
    MessageEncoder encoder;
    public ClientMessageEncoderFactory(MessageEncoderFactory messageEncoderFactory)
    {
        encoder = new ClientMessageEncoder(messageEncoderFactory.Encoder);
    }
    //... other methods ...
}

ClientMessageEncoderFactory 由绑定元素在 CreateMessageEncoderFactory 方法中使用。

public class ClientMessageEncodingBindingElement :
			MessageEncodingBindingElement //[12]
{
    public override IChannelFactory<TChannel>
	BuildChannelFactory<TChannel>(BindingContext context)
    {
        context.BindingParameters.Add(this);
        var property = GetProperty<XmlDictionaryReaderQuotas>(context);
        property.MaxStringContentLength = Int32.MaxValue; // [14]
        return context.BuildInnerChannelFactory<TChannel>();
    }

    public override MessageEncoderFactory CreateMessageEncoderFactory()
    {
        ClientMessageEncoderFactory factory =
		new ClientMessageEncoderFactory
		(innerBindingElement.CreateMessageEncoderFactory());
        ClientMessageEncoder encoder = factory.Encoder as ClientMessageEncoder;
        encoder.ContentCompression = ContentCompression;
        encoder.ContentEncryption = ContentEncryption;
        return factory;

    }
    //... other methods ...
}

并且上面的绑定元素由扩展元素使用;在这里,我们可以从配置中获取属性(例如 contentEncryption),并从中将它们传播到消息编码器。

public class ClientMessageEncodingElement : BindingElementExtensionElement
{
    [ConfigurationProperty("contentEncryption", DefaultValue = "Credentials")]
    public string ContentEncryption
    {
        get { return (string)base["contentEncryption"];}
        set { base["contentEncryption"] = value; }
    }
    protected override BindingElement CreateBindingElement()
    {
        ClientMessageEncodingBindingElement bindingElement =
			new ClientMessageEncodingBindingElement();
        this.ApplyConfiguration(bindingElement);
        return bindingElement;
    }
    //... other methods ...
}

配置应如下所示:

<extensions>
  <bindingElementExtensions>
    <add name="ClientMessageEncoding"
	type="Challenge.Client.ClientMessageEncodingElement, Client" />
  </bindingElementExtensions>
</extensions>
    ...
<bindings>
  <customBinding>
    <binding name="ChallengeMessageEncoding">
      <ClientMessageEncoding contentEncryption="All" contentCompression="GZip" />
      <httpTransport/>
    </binding>
  </customBinding>
</bindings>

现在,您已经了解了“它是如何制作的”,您可以开始 使用代码,然后 实现扩展

注释

  1. 该解决方案使用 .NET 4.0。
  2. 请忽略与源代码管理(TFS)相关的警告。
  3. 您可能需要重新添加一些引用:System.ConfigurationSystem.ServiceModelSystem.IdentityModelSystem.SecuritySystem.Runtime.SerializationIonic.Zip(最后一个在 Common 项目中;其他随 .NET 4 一起提供)。
  4. 这些引用适用于文章的两个部分以及附加的代码(带有数字的注释)。
  5. 启动服务器需要管理员权限。
  6. 要使用 Microsoft 的 zip 实现并消除对 Ionic.Zip.dll 的需求,只需在 Common 项目的 Encoding.cs 中将“using Ionic.Zlib;”更改为“using System.IO.Compression;”;然后您可以删除该引用和 DLL。
  7. 为避免重复,代码仅附加到文章的第一部分。
  8. 如果您想直接测试附加 zip 文件中的 server.execlient.exe,您需要先取消阻止 server.exe.configclient.exe.config(右键单击,属性,取消阻止);否则,您将收到配置错误。

参考文献

[1] RSACryptoServiceProvider 信息
[2] AesCryptoServiceProvider 信息
[3] 自定义 WCF 身份验证
[4] 自定义消息编码器:自定义文本编码器
[5] 加密助手
[6] 如何从 RSA+AES 加密的 XML 中获取 AES 加密密钥
[7] WCF GZip 压缩错误
[8] 如何:使用非对称密钥加密 XML 元素
[9] 如何:使用对称密钥加密 XML 元素
[10] 通用解码器
[11] WCF ClearUsernameBinding
[12] Microsoft 示例
[13] Microsoft 代码 - 编码器/工厂
[14] 使用 GZipEncoder 和自定义绑定解决 WCF 压缩的 XmlDictionaryReaderQuotas 错误
[15] 中间人攻击

历史

  • 2011-03-08 版本 1.0.0 - 初始发布
  • 2011-03-18 版本 1.1.0 - 添加了带有密码禁止列表的代码
  • 2011-03-24 版本 1.1.1 - 小文本更改
© . All rights reserved.