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

WCF REST 服务上的 Digest 身份验证

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (27投票s)

2011年2月27日

CPOL

10分钟阅读

viewsIcon

167810

downloadIcon

3300

本文介绍摘要认证以及一个可用于 WCF REST 服务进行摘要认证并针对任何后端进行验证的库。

目录

引言

这是对我之前一篇关于如何使用 WCF REST 服务进行 BASIC 认证的文章的后续。 那篇文章的解决方案的缺点是,您需要一个 HTTPS 隧道来真正保护用户名和密码的验证过程。虽然可行,但这并非总是可行的解决方案,例如,如果您不想为证书付费,或者在使用 HTTPS 隧道时性能下降。

本文介绍了一种称为摘要认证的不同认证机制,它提供了一种替代方案。这种安全机制比基本认证更安全,并且没有 HTTPS 隧道的缺点。

摘要认证在多个 Web 服务器上可用,并得到多个 Internet 浏览器的支持。在 Internet Information Server 中使用摘要认证的一个缺点是,它会自动与 Active Directory 进行凭据验证。本文介绍了一种实现,该实现允许您使用摘要认证来保护 WCF REST 服务,并针对任何后端进行验证。

摘要认证最初在 RFC 2069 中被描述为 HTTP 基本认证的扩展。后来,验证算法和安全性通过 RFC 2617 得到了改进。这是当前稳定的规范。本文中的实现基于 RFC 2617 规范。摘要认证更安全,因为它使用了 MD5 密码散列 和随机数,以防止密码分析。

摘要通信概述

摘要通信开始于客户端请求 Web 服务器上的资源。如果资源受到摘要认证的保护,服务器将响应 HTTP 状态码 401,表示未经授权。

Digest Authentication Communication

在同一个响应中,服务器在 HTTP 头部指示资源受哪种机制保护。HTTP 头部包含以下内容:“WWW-Authenticate: Digest realm="realm", nonce="IVjZjc3Yg==", qop="auth"”。您首先应该注意到响应中的字符串 **Digest**,服务器在此处指示客户端请求的资源是使用摘要认证保护的。其次,服务器通过 **保护质量 (QOP)** 和我将在本文稍后解释的随机数 (nonce) 来指示客户端使用的摘要认证算法类型。

Internet 浏览器对此的响应是向用户显示一个对话框,用户可以在其中输入用户名和密码。请注意,与基本认证保护的站点不同,此对话框不会显示有关以明文传输凭据的警告。

Digest Authentication Credentials Screen

当用户在此对话框中输入凭据时,浏览器会再次请求服务器上的资源。这次,客户端会将摘要认证相关的附加信息添加到 HTTP 头部。

Digest Authentication Second

服务器会验证这些信息,并将请求的资源返回给客户端。服务器响应的详细信息以及客户端的附加请求将在本文的后续部分进行描述。

摘要式身份验证

当服务器响应未经身份验证的客户端请求时,服务器会在 HTTP 响应的头部添加一个随机数 (nonce) 和一个 qop 键。这两者都是摘要认证的典型特征。首先,将描述随机数,然后是保护质量 (QOP)。

随机数 (Nonce)

Nonce 的意思是“仅使用一次的数字”,它是一个伪随机数,可确保客户端和服务器之间的旧通信无法在重放攻击中重复使用。重放攻击是一种网络攻击,其中重复先前的有效数据传输。这由截获数据并重新传输它的攻击者完成。根据 RFC 2716 规范,Nonce 是服务器指定的数据字符串,每次服务器返回 401 响应时都应唯一生成。发送回客户端的 401 响应包含服务器生成的 Nonce。根据 RFC 2716,客户端应将此 nonce 添加到下一个请求的头部。

生成随机数

Nonce 的格式取决于实现。每个 RFC 2617 摘要认证实现都可以定义自己的 Nonce 格式。但是,应该仔细设计 Nonce 的格式,因为它属于安全质量的一部分。对于我的实现,我选择在 Nonce 中包含日期时间戳和客户端的 IP 地址。实现生成的 Nonce 如下。

Nonce = Base64( Timestamp : PrivateHash)

Nonce 是通过对由时间戳、冒号和一个生成的私有散列连接而成的字符串进行 base64 编码来生成的。在源代码中,这由 `NonceGenerator` 类处理,该类有一个 `Generate` 方法,用于生成 `Nonce` 字符串。

public string Generate(string ipAddress)
{
   double dateTimeInMilliSeconds =
      (DateTime.UtcNow - DateTime.MinValue).TotalMilliseconds;
   string dateTimeInMilliSecondsString =
      dateTimeInMilliSeconds.ToString();
   string privateHash = privateHashEncoder.Encode(
      dateTimeInMilliSecondsString,
      ipAddress);
   string stringToBase64Encode =
      string.Format("{0}:{1}", dateTimeInMilliSecondsString, privateHash);
   return base64Converter.Encode(stringToBase64Encode);
}

MD5 用于生成由时间戳、冒号、客户端 IP 地址、冒号和一个仅服务器知道的私有密钥连接而成的字符串的私有散列。由于使用了 MD5,因此生成过程是单向的。无法从私有散列中重构这些信息。

PrivateHash = MD5Hash( Timestamp : IP Address : Private key)

在源代码中,生成私有散列由 `PrivateHashEncoder` 类中的 `Encode` 方法处理。它使用 `MD5Encoder` 类来实际生成 MD5 散列。

public string Encode(string dateTimeInMilliSecondsString,
    string ipAddress)
{
  string stringToEncode = string.Format(
     "{0}:{1}:{2}",
     dateTimeInMilliSecondsString,
     ipAddress,
     privateKey);
  return md5Encoder.Encode(stringToEncode);
}

验证随机数

每次客户端将 Nonce 发送给服务器时,服务器都会验证这是否是服务器发送给客户端的 Nonce。服务器通过两个步骤验证 Nonce:

此服务器上的实现首先验证此 `PrivateHash` 是否由该服务器生成并返回给该客户端。服务器通过使用 Nonce 中的时间戳和客户端的 IP 地址来生成 `PrivateHash`。如果这不能产生与客户端 Nonce 中的 `PrivateHash` 相同的 `PrivateHash`,则 Nonce 不正确,服务器将响应 401。`NonceValidator` 负责在源代码中验证此 Nonce。

public virtual bool Validate(string nonce,
   string ipAddress)
{
   string[] decodedParts = GetDecodedParts(nonce);
   string md5EncodedString = privateHashEncoder.Encode(
      decodedParts[0],
      ipAddress);
   return string.CompareOrdinal(
      decodedParts[1],
      md5EncodedString) == 0;
}

其次,服务器会检查时间戳是否过旧。服务器为 Nonce 设置了一个固定的超时时间。例如,超时时间为 300 秒。服务器验证 Nonce 中的时间戳是否不超过 300 秒。如果 Nonce 的时间超过 300 秒,服务器将在 HTTP 头部中返回一个指示,表明收到的 Nonce 过期,并附带一个新的 Nonce。RFC 2617 在头部使用一个特殊的键 **Stale**,当 Nonce 过期时,将其设置为 `true`。`NonceValidator` 也负责检查时间戳是否过旧。

public virtual bool IsStale(string nonce)
{
   string[] decodedParts = GetDecodedParts(nonce);
   DateTime dateTimeFromNonce =
      nonceTimeStampParser.Parse(decodedParts[0]);
   return dateTimeFromNonce.AddSeconds(
      staleTimeOutInSeconds) < DateTime.UtcNow;
}

通过在 Nonce 中使用时间戳和 IP 地址,我们可以确保请求是最近的,并且来自请求该资源的客户端。

保护质量 (Quality Of Protection)

摘要认证允许服务器询问客户端应使用哪种算法来加密用户的凭据。摘要认证支持以下保护质量:

  • none = 默认保护,与 RFC 2069 兼容
  • auth = 增强的保护,包括客户端随机数和客户端随机数计数器
  • auth-int = 增强的保护和完整性,包括 auth 的所有内容以及正文内容的散列

请注意,这是服务器的请求,客户端本身可以选择较低安全级别的 qop 算法。如果服务器请求 `auth`,客户端可以使用默认的或 none 的 qop 开始通信。

本文中的实现同时支持 **默认/none** 和 auth。`DefaultDigestEncoder` 类和 `AuthDigestEncoder` 类分别实现了默认和 auth 类型的保护质量。这两个类都继承自 `DigestEncodeBase`,它包含了通用功能。

DigestEncoder

在运行时,服务器实例化这两种类型的编码器,并将它们存储在一个字典中,以 qop 算法作为键。这使得服务器能够在运行时轻松地在不同的编码器类型之间切换。

internal class DigestEncoders :
   Dictionary
{
 public DigestEncoders(MD5Encoder md5Encoder)
 {
  Add(DigestQop.None, new DefaultDigestEncoder(md5Encoder));
  Add(DigestQop.Auth, new AuthDigestEncoder(md5Encoder));
 }

 public virtual DigestEncoderBase GetEncoder(DigestQop digestQop)
 {
  return this[digestQop];
 }
}

无保护质量或默认保护质量

当 Internet 浏览器收到带有摘要认证头部的 401 HTTP 状态码时,它会显示一个用于输入用户名和密码的对话框。当客户端使用与 RFC 2069 兼容的 **默认 qop** 时,客户端按以下方式加密用户名和密码:

HA1 = MD5( username : realm : password)

HA2 = MD5( method : digestURI)

response = MD5( HA1 : nonce : HA2)

从用户名、领域和密码中创建一个 MD5 散列,从 HTTP 方法和客户端请求的资源 URI 中创建一个单独的 MD5 散列。响应是通过 MD5 散列创建的,该散列结合了前两个 MD5 散列和服务器生成的随机数。`DigestEncoderBase` 类包含了生成 HA1 和 HA2 散列的功能。

private string CreateHa1(DigestHeader digestHeader,
   string password)
{
  return md5Encoder.Encode(
    string.Format(
    "{0}:{1}:{2}",
    digestHeader.UserName,
    digestHeader.Realm,
    password));
}

private string CreateHa2(DigestHeader digestHeader)
{
  return md5Encoder.Encode(
    string.Format(
    "{0}:{1}",
    digestHeader.Method,
    digestHeader.Uri));
}

基类 `AuthDigestEncoder` 和 `DefaultDigestEncoder` 负责生成响应。最后一个步骤,即生成响应,是两个派生类不同的地方。**Auth** 算法的响应应以不同的方式生成。Auth 算法在响应中包含 `nonceCount` 和客户端生成的 Nonce。此外,在计算散列之前,会将实际的 qop 字符串连接起来。

response = MD5( HA1 : nonce : nonceCount : clientNonce : qop : HA2)

这就是为什么 Auth 算法比 Default 更安全的原因,服务器会执行额外的检查,以查看客户端是否为每个请求递增了 `nonceCount`。`AuthDigestEncoder` 的 `CreateResponse` 方法生成 Auth 响应。

public override string CreateResponse(
   DigestHeader digestHeader,
   string ha1,
   string ha2)
{
  return
   md5Encoder.Encode(
     string.Format(
     "{0}:{1}:{2}:{3}:{4}:{5}",
     ha1,
     digestHeader.Nonce,
     digestHeader.NounceCounter,
     digestHeader.Cnonce,
     digestHeader.Qop.ToString(),
     ha2));
}

扩展 WCF REST

为了能够将摘要认证与 WCF REST 集成,必须扩展 WCF REST 框架。这可以通过创建自定义 `RequestInterceptor` 来完成。有关更多信息,请参阅我之前在 CodeProject 上关于通过 `RequestInterceptor` 扩展 WCF REST 的文章,其中更详细地解释了此扩展。

检索和存储用户凭据

用户密码作为客户端生成给服务器的响应的一部分进行传输。服务器无法从响应中提取密码。服务器生成一个响应,并检查该响应是否与客户端提供的响应相等。这意味着使用摘要认证存储和检索用户凭据有两种选择。

  • 第一个也是最安全的选择是为每个用户在凭据数据存储中存储 HA1 密钥并使用存储的 HA1 密钥进行验证。这有一个缺点,因为如果用户名、密码或领域发生更改,您必须更改数据存储中的 HA1 密钥。
  • 第二个选择是将用户的密码以可检索原始密码的方式存储在凭据数据存储中。这显然不如第一个选择安全。

使用源代码

如果您想使用提供的源代码通过基本认证保护您自己的 WCF REST 服务,则需要执行以下步骤:

  • 添加对 `DigestAuthenticationUsingWCF` 程序集的引用
  • 创建一个派生自 `MembershipProvider` 的自定义成员资格提供程序
  • 实现 `ValidateUser` 方法以对接您的后端安全存储
  • 创建一个派生自 Membership User 的自定义成员资格用户
  • 实现 `GetUser` 方法以对接您的后端安全存储
  • 创建一个自定义 `DigestAuthenticationHostFactory`,请参阅提供的源代码中的示例
  • 将新的 `DigestAuthenticationHostFactory` 添加到 *.svc* 文件的标记中

关注点

提供的源代码是使用 TDD 开发的,并使用 NUnit 框架来创建和执行测试。单元测试中使用了 Rhino mocks 作为模拟框架。

历史

  • 2011 年 2 月 28 日
    • 初次发布
© . All rights reserved.