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






4.87/5 (45投票s)
HTTP - 无 IIS;身份验证 - 无 SSL/X509 证书;加密 - 请求使用 RSA+AES,响应使用 AES;压缩 - 请求/响应均使用 GZip
目录
引言
随着 Windows Communication Foundation 的出现,构建面向服务的应用程序比以往任何时候都更加容易。涌现了大量关于特殊情况扩展的文章。即便如此,仍有一些情况尚未得到处理。就像我接下来需要解决的以下问题:
- 客户端-服务器应用程序 – HTTP 协议 – 无 IIS
- 身份验证 – 来自数据库的用户名/密码 – 无 SSL/X509 证书
- 授权 – 来自数据库的角色
- 凭据加密(可以选择加密整个请求/响应)
- 请求和响应压缩
逻辑解决方案
针对上述每个要求/限制,我们有:
- 无 IIS – 我们需要自己的 HTTP 服务器 – 这可以通过 WCF 的几行代码轻松实现 – 无需赘述。
- 用户名/密码身份验证 – 默认情况下很容易完成,但需要 X509 证书。因此,我们需要自己的机制:我们将把凭据添加到消息头,并加密它们(或整个消息)。
- 加密 – 我们将使用非对称算法[1](RSA)与公钥/私钥。通常,服务器和客户端需要拥有自己的公钥/私钥集来加密请求和响应,但我们将使用一个技巧来避免客户端拥有自己的密钥集。
使用 RSA,只能加密少量数据(对于 2048 位加密,只能加密 128 字节数据)。因此,它用于加密一个随机生成的密码,该密码将由对称算法[2](AES)用于加密消息。服务器使用其私钥解密客户端的密码,然后使用该密码解密消息。并使用相同的密码加密响应消息。
- 对凭据的额外安全措施 – 即使凭据已加密,监听者也可以获取这些加密的凭据并发起新请求,因此我们将为消息添加一个过期日期。
此外,在到期之前,该密码将被禁止使用——它不能被另一条消息使用。
这样,如果同一条消息立即被发送,它将被拒绝,因为该密码已被禁止;如果它在密码禁用期过后发送,那么身份验证令牌也已过期,并且消息将被拒绝。实际上,该密码将充当一个随机数(nonce)。 - 压缩 – 我们将在发送/接收消息和响应之前使用 GZip 压缩/解压缩。
现在,让我们看看上述通用考虑因素在**客户端-服务器消息流**中是如何体现的。
- 服务器启动;生成(或从磁盘加载)一个新的 RSA 密钥。
- 客户端启动;它向服务器请求公钥和时间;根据服务器时间和客户端时间,它将计算客户端-服务器时间差。
- 客户端准备请求消息
- 凭据被添加到消息头;`Credentials` 令牌将具有 `User/Password/Expires` 属性;`Expires` 将计算为客户端时间 + 客户端-服务器时间差 + 几秒钟;
- 消息(或仅凭据部分)被加密;更确切地说:
- 生成一个随机密钥并将其与消息 ID 一起保存;
- 使用 AES 算法和上面生成的密钥加密消息(或凭据部分);
- 使用服务器的 RSA 公钥加密 AES 密钥,并将其添加到加密的消息中。
- 消息被压缩。
- 服务器接收请求消息。
- 消息被解压缩。
- 消息被解密;更确切地说:
- 通过使用服务器的私钥解密来检索客户端的 AES 密钥;
- 如果密钥在禁止列表中,则抛出 `SecurityException`;如果不在,则将其添加到禁止列表中,并设置禁止过期日期。
- 使用客户端密钥解密消息。
- 客户端的密钥将与消息 ID 一起保存——这将用于使用相同的 ID 加密响应;
- 从消息中提取凭据;将 `Expires` 与服务器当前时间进行比较,如果 `Expires` 更大,则抛出 `AuthenticationException`;
- 身份验证 – 凭据与数据库进行验证;
- 授权 – 已验证用户的角色从数据库/缓存中检索。
- 服务器准备响应消息。
- 消息被加密;更确切地说,消息将使用在请求解密期间保存的客户端密钥(AES)进行加密;
- 消息被压缩。
- 客户端接收响应消息。
- 消息被解压缩。
- 消息使用在加密(3.b.)期间保存的密钥进行解密。
WCF 考虑事项
如何扩展 WCF 以实现上述流程。
在客户端,为了添加凭据,我们将使用一个 `BehaviorExtensionElement`,它实现了 `IClientMessageInspector`,该接口有一个 `BeforeSendRequest` 方法(实现 这里 查看)。
在服务器端,对于身份验证,我们需要一个实现 `IIdentity` 的类,以及一个派生自 `ServiceAuthenticationManager` 并重写 `Authenticate` 方法的类[3](实现 这里 查看)。
对于授权,我们需要分别实现 `IPrincipal` 和 `IAuthorizationPolicy` 的类(实现 这里 查看)。
在客户端和服务器端,对于加密和压缩,我们将需要派生自 `MessageEncoder`[4]、`MessageEncoderFactory`[13]、`MessageEncodingBindingElement`、`BindingElementExtensionElement` 的类;第一个派生自 `MessageEncoder` 的类将执行实际的加密/解密和压缩/解压缩;派生自 `BindingElementExtensionElement` 的类是我们在配置中的入口点;它将使用一个 `MessageEncodingBindingElement`,该元素将使用一个 `MessageEncoderFactory`,后者将使用 `MessageEncoder`(实现 这里 查看)。
虽然压缩在服务器和客户端上是相同的,但客户端的加密与服务器的不同,因此我们将为它们各自定义上述类(当然,相同的部分将放在一个父类中,客户端和服务器版本都将从中派生。
您可能认为一个更好的解决方案是仅对压缩使用消息编码器,并使用格式化程序来加密消息,但这行不通:服务器在反格式化之前进行身份验证,因此我们需要在此之前解密凭据。
实现
我们将有 3 个项目 – Client(客户端)、Server(服务器)和 Common(客户端和服务器通用的库)。
以及以下类 - 所有类都自然地源于上述论证:`Credentials`、`ServerInfo`、`ClientCriptographer` 和 `ServerCryptographer`,`ClientEncoder` 和 `ServerEncoder`。
下图包含主要类。
由于篇幅过长,实现细节将在本文的第 2 部分中详细介绍。
安全注意事项
试图破译凭据是很复杂的——凭据令牌每次请求都会改变,因为 `Expires` 属性会改变——更重要的是,AES 密钥每次请求都会改变。
尝试破译服务器的 RSA 密钥会更容易——但也不够容易。一切实际上都取决于密码分析师拥有的处理能力。由于他没有 “万能破译器”[10] 为他工作,所以破解 RSA 密钥可能需要几个月甚至几年——但这是可行的。然而,如果客户端凭据和 RSA 密钥经常更改,这可能就毫无意义了;(RSA 密钥在服务器每次重启时都会更改)。
真正的问题来自于一个能够控制客户端和服务器之间通信并冒充它们的攻击者;比如“中间人”攻击[15]。为了避免这种攻击,您需要使用 SSL,它通过相互信任的认证机构对服务器进行身份验证。
如何使用代码
只需在整个解决方案中搜索“example”;您可以在那里进行修改/添加。或者,基于示例模型,添加您自己的服务。
您可能还对更改配置文件中的 `ClientMessageEncoding` 和 `ServerMeesageEncoding` 的 `contentEncryption` 和 `contentCompression` 属性感兴趣。
扩展想法
其中大部分与更改编码有关
- 允许仅加密响应或仅加密请求或其中的部分;
- 允许仅压缩响应或请求;
- 将文本编码更改为二进制编码(这意味着将 XML 加密更改为自定义二进制加密);
- 更改加密和压缩的顺序——先加密后压缩并非总是最佳解决方案。
注释
- 该解决方案使用 .NET 4.0。
- 请忽略与源代码控制(TFS)相关的警告。
- 您可能需要重新添加一些引用:`System.Configuration`、`System.ServiceModel`、`System.IdentityModel`、`System.Security`、`System.Runtime.Serialization`,*Ionic.Zip*(后者在 Common 项目中;其他来自 .NET 4)。
- 这些引用适用于文章的两个部分以及附带的代码(数字注释)。
- 您需要管理员权限才能启动服务器。
- 要使用 Microsoft 的 Zip 实现并消除对 `Ionic.Zip.dll` 的依赖,只需在 Common 项目的 `Encoding.cs` 中将“`using Ionic.Zlib;`”更改为“`using System.IO.Compression;`”;然后您可以删除引用和 DLL。
- 为避免重复,代码仅附于文章的第一部分。
- 如果您想直接测试附加 zip 文件中的 `server.exe` 和 `client.exe`,您需要先解除对 `server.exe.config` 和 `client.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-15 版本 1.0.1
- 文本小改动。
2011-03-17 版本 1.1.0
- 更新了安全注意事项,增加了关于中间人攻击的信息。
- 更新了客户端-服务器流 - 4.b. 以将随机客户端密码添加到服务器的禁止列表中。
- 更新了代码以处理修改后的客户端-服务器流。
2011-03-23 版本 1.1.1
- 文本小改动。