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

使用WSE进行Web服务安全入门——第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.48/5 (56投票s)

2004年5月17日

18分钟阅读

viewsIcon

456226

downloadIcon

6837

本文介绍了构建安全 Web 服务的初步步骤。它介绍了 WSE 规范以及基于用户名识别和密码验证的最简单的身份验证机制。

目录

引言

Web 服务被认为是提供网络上易于访问的服务的一种方式。无论底层的网络结构或配置、操作系统、通信机制或实现语言如何,它们都应该易于使用。

虽然有不同的 Web 服务通信方式,但 SOAP 被认为是事实上的标准。SOAP 消息被发送到由 URI 标识的服务终结点,通常由终结点服务处理某个操作,并发送包含结果或错误代码的 SOAP 响应。这可以是简单的 HTTP 之上的 SOAP,甚至是打包在电子邮件中通过 SMTP 传输的 SOAP 消息。

要在业务场景中取得成功,Web 服务必须适用于安全通信。然而,原始 SOAP 规范不包含解决安全问题的方案。SSL 或 IPSec 等其他技术提供标准的传输安全。问题在于:从一个终结点到另一个终结点的 SOAP 连接可以被视为一个逻辑连接,抽象了其物理基础设施。逻辑上是一个端到端连接,物理层可能包含转发 SOAP 消息的各种中间设备。因此,在接收和转发消息的过程中,在传输级别定义的安全信息(例如 SSL 的工作方式)可能会丢失。因此,任何接收者都必须依赖其物理连接点前驱者的安全处理,以及其对数据完整性和机密性的处理。一种解决方法是在消息级别定义安全信息。

一些大公司,如 MicrosoftIBM,组建了一个处理安全问题的团体,最终提供了一些规范。其中最重要、也是其他规范的基础是 Web 服务安全(WS-Security 或 WSS)。

WS-Security

WS-Security 定义了 SOAP 扩展,用于在消息级别实现**客户端身份验证**、**消息完整性**和**消息机密性**。

WS-Security 的目标不是发明新的技术,而是展示如何将现有的安全解决方案与 SOAP 和 Web 服务通信结合使用。它规定了身份验证、签名和加密机制的规则。
一个好处是:WS-Security 可以与其他 Web 服务扩展协同工作。

身份验证

身份验证解决了“谁是调用者?”和“他如何证明自己的身份?”等问题。如果这些问题可以得到解答,那么接收者的任务就是明确调用者是否可信。
身份验证具体防止

  • 假冒攻击:用户必须证明自己的身份,因此假冒他人更加困难。
  • 重放攻击:使用时间戳时,难以重复使用被盗的身份验证信息。
  • 身份拦截:当交换内容被额外加密时,被拦截的身份信息将毫无用处。

请注意,身份验证只是安全任务的一半。一旦知道用户是谁,就必须确定用户可以访问哪些资源。这正是**授权**的作用。

完整性

消息完整性确保接收者收到的数据在传输过程中未被篡改。WS-Security 尝试使用**XML 签名**规范来确保完整性,该规范定义了一种对 XML 进行加密签名的约定。签名使用 <Signature> 元素和配套的子元素作为安全标头的一部分来定义。

签名本身是根据 SOAP 消息内容和安全令牌计算的。消息接收者可以使用相应的解码算法来检查消息的有效性。

机密性

消息机密性是通过消息加密来确保用户能够放心地在传输过程中无法读取数据。在这里,**XML 加密**规范是加密 SOAP 消息一部分的基础。SOAP 消息的任何部分,包括标头、正文块和子结构,都可以被加密。

加密使用发送者和接收者共享的对称密钥,或者使用以加密形式在消息中传输的密钥来实现。

由于签名和消息加密不在本文的讨论范围之内,请参考本系列的第二部分。它将详细介绍这两个主题。

<Security> 标头

WS-Security 的入口点是一个名为 <Security> 的 SOAP 标头元素。它包含实现安全令牌、签名或加密等机制所需的与安全相关的数据和信息。该元素可以出现多次,以便针对不同的接收者(所谓的 SOAP 角色)。接收者可以是最终消息接收者,也可以是中间方。<Security> 标头的目标通过 <role> 元素来宣告。要为不同的接收者定位安全信息,您必须在不同的标头块中实现这些信息,每个块指定一个不同的 <role> 值。重要的是,没有两个标头可以具有相同的 <role> 值或省略 <role>。没有 <role> 值的标头可以被任何人使用。
请注意,没有两个安全标头可以使用相同的角色。但中间方不限于一个安全标头 - 实际上,它可以处理多个标头。

下面是一个使用 <Security> 标头的 SOAP 消息骨架

<SOAP:Envelope xmlns:SOAP="...">
  <SOAP:Header>
    <wsse:Security SOAP:role="..." SOAP:mustUnderstand="...">
      <wsse:UsernameToken>
        ..
      </wsse:UsernameToken>
      ...
    </wsse:Security>
  </SOAP:Header>
  <SOAP:Body Id="MsgBody">
  <!–- SOAP Body data -->
  </SOAP:Body>
</SOAP:Envelope>

因此,我们可以看到,为了添加 WS-Security,只修改了 SOAP 消息的标头元素。所有安全元素都放在 <Security> 元素内。正文保持不变。

安全令牌

身份及其证明是我们感兴趣的第一个主题。毫无疑问,大多数服务提供商都认识到了解谁在与他们交谈,以及是否允许消息发送者访问他们的服务的重要性。同样,客户端也要确保服务提供商的真实性,这也是有兴趣的。但这不在本文的讨论范围之内。
身份验证可以使用安全令牌来完成。WS-Security 允许我们使用任何我们喜欢的安全令牌。明确定义了三种不同的选项:自定义身份验证情况下的用户名/密码身份验证,以及 Kerberos 票证或 X.509 证书形式的二进制身份验证令牌。此外,还可以应用自定义二进制安全令牌。

第一个选项是仅依赖于使用用户名和密码验证的自定义身份验证。WSS 定义了一个名为 <UsernameToken> 的元素,它支持此目的。

<UsernameToken> 包括以下子元素(但不限于):

  • /Username
    与此令牌关联的用户名
  • /Password
    与此令牌关联的用户的密码
  • /Password/@Type
    提供的密码的类型;2 个预定义类型
    • PasswordText
      明文密码
    • PasswordDigest
      密码的摘要,Base64 编码的 UTF8 编码密码的 SHA1 哈希值
  • /Nonce
    令牌的随机数
  • /Created
    令牌创建的日期和时间

以上子元素是理解本文的重要元素。

用户名/密码场景

使用 <UsernameToken> 元素的方式有多种,具体取决于 <Password> 元素的使用方式。最简单的标识方式是仅提供用户名而省略密码。对应的 SOAP 消息的片段如下所示。

<UsernameToken>
  <Username>MyName</Username>
</UsernameToken>

很明显,仅靠这种机制是相当不安全的。没有任何身份证明,它只适用于使用 SSL 等其他身份验证机制的情况,此时用户名仅作为基本的标识手段。

但 WS-Security 为我们提供了实现“更好”安全性的手段。通过将 <password> 元素作为 <UsernameToken> 元素的一部分进行传输,可以实现(尽管是弱)安全身份验证的第一步,因为现在可以证明身份了。SOAP 片段如下所示

<UsernameToken>
  <Username>MyName</Username>
  <Password Type="PasswordText">MyPass</Password>
</UsernameToken>

Type 属性表示密码是以明文形式提供的。因此,如果有人拦截了消息,他们可以轻易地找出密码并进行身份验证。为防止这种情况,密码应始终以哈希值形式发送,使中间方无法看到真实密码。哈希过程遵循 SHA-1 算法,然后以 Base64 编码传输哈希后的密码。

<UsernameToken>
  <Username>MyName</Username>
  <Password Type="PasswordDigest">fm6SuM0RpIIhBQFgmESjdim/yj0=</Password>
</UsernameToken>

现在,问题从密码可读性强的危险转移到有人拦截消息并使用哈希后的密码在其自己的消息中进行身份验证的情况。在这种情况下,恶意拦截者不知道用户的密码本身并没有帮助——他根本不需要。
即使在这种情况下,WS-Security 也提供了一些额外的手段来提高身份验证的安全性:密码不是以明文哈希值形式传输,而是以真实文本密码、随机数和安全令牌创建时间的组合的哈希值形式传输。

Password_Digest = Base64(SHA-1(Nonce + Created + Password))

Nonce 是一个唯一的随机字符串,它现在标识了密码。如果它被正确使用并且为每个发送的 SOAP 消息创建新的随机数,那么密码哈希值就不会相同。

鉴于此,<UsernameToken> 可能如下所示

<UsernameToken>
  <Username>MyName</Username>
  <Password Type="PasswordDigest">fm6SuM0RpIIhBQFgmESjdim/yj0=</Password>
  <Nonce>Pj+EzE2y5ckMDx5ovEvzWw==</Nonce>
  <Created>2004-05-11T12:05:16Z</Created>
</UsernameToken>

在这种场景下,客户端创建密码并传输哈希值。服务器使用内置机制检索与给定用户名相关的密码,计算哈希值并与收到的哈希值进行比较。如果哈希值相同,则授予访问权限。

但仅仅处理上述密码并不是真正安全的。恶意用户可以获取某人的整个 UsernameToken 并将其放入自己的请求中。

防止这种情况的第一种方法是为令牌指定超时值,因此服务器不会接受带有过期时间戳的请求。如果发送者设置了 60 秒的时间戳,而服务器在 <Created> 值之后超过 60 秒收到消息,它将简单地拒绝整个请求。这很容易实现,但也可能存在一些问题,例如由于服务器上的时钟同步问题,导致过期消息被接受。

关于时钟同步问题,WS-Security 提供了 <Timestamp> 标头。它可以用于表示消息的创建和过期时间。这对于消息的创建、接收和处理非常有用。与 <Security> 标头一样,可以指定多个 <Timestamp> 元素并将其定向到不同的角色。<Timestamp> 元素的模式大纲如下

<Timestamp>
  <Created>...</Created>
  <Expires>...</Expires>
</Timestamp>

请注意,子元素的顺序是固定的,因此必须由中间方保留。值得注意的是,虽然像 <Created> 这样的子元素被定义为与 <Timestamp> 标头一起使用,但当需要与时间相关的标记时,它们可以在标头或正文内的任何位置使用。请记住,我们已经在 <UsernameToken> 安全令牌中看到了 <Created> 元素。

下一个也是可能更好的方法来防止 UsernameToken 重放,是在服务器上维护最近收到的请求的 Nonce 值历史记录。具有已使用 Nonce 的请求将不被接受。很明显,Nonce 值只需要保留到请求的时间戳传播的时间,然后就可以删除它们。如果收到多个具有相似 Nonce 值的消息,则都应拒绝,因为拦截者可能延迟了原始消息并先发送了自己的消息。

即使做出了这么多预防措施,也不能保证通信是安全的。仍然可以有一个恶意用户阻止整个消息的传递,并使用 UsernameToken 来认证自己的消息。我们需要为消息添加数字签名,以防范这种情况,因为这可以确保发送者的真实性。

所需的命名空间

请注意,上面的 XML 片段不包含命名空间前缀。
与 WS-Security 相关的重要命名空间及其关联的前缀是

前缀 命名空间 相关元素
wsse http://schemas.xmlsoap.org/ws/2002/07/secext <Security>, <UsernameToken>, <Username>, <Password>, <Nonce>
wsu http://schemas.xmlsoap.org/ws/2002/07/utility <Timestamp>, <Created>, <Expires>
ds http://www.w3.org/2000/09/xmldsig# <Signature>, <SignedInfo>, <SignatureValue>, <KeyInfo>
xenc http://www.w3.org/2001/04/xmlenc# <EncryptedData>, <CipherData>

设置 WSE 环境

WSE 为我们提供的最重要的类是 Microsoft.Web.Services.SoapContext。它为我们提供了处理入站 SOAP 消息的 WS-Security 标头和其他标头的接口,以及为出站 SOAP 消息添加 WS-Security 和其他标头的接口。一个包装器类为 SOAP 请求和响应添加了一个 SoapContext。在服务器端,SOAP 扩展 Microsoft.Web.Services.WebServicesExtension 会验证入站消息,并为 WebMethods 中可访问的请求和响应提供 SOAPContext。

第一步是设置 .NET 应用程序以使用 WSE SOAP 扩展。这可以通过将条目添加到 machine.config 来实现全机器范围的设置,或者通过将条目添加到其 Web.config 文件来限制范围到我们服务的虚拟目录。我们选择后者,因为我们不希望所有应用程序都支持 WSE。因此,只需通过键入以下行来添加一个 /configuration/system.web/webServices/soapExtensionTypes/Add 元素

<webServices>
  <soapExtensionTypes>
  <add type="Microsoft.Web.Services.Web ServicesExtension, 
    Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral, 
    PublicKeyToken=31bf3856ad364e35" priority="1" group="0" 
  />
  </soapExtensionTypes>
</webServices>

请注意,type 属性必须是单行,此处换行是为了方便阅读。

实现

现在我们开始实现一个简单的客户端身份验证场景。在本文的结尾,我们将有一个提供简单方法的 Web 服务。该方法将由一个客户端调用,该客户端将 WS-Security 条目以 <security> 标头的形式添加到其 SOAP 请求中。在服务器端,将首先评估该标头,如果身份验证成功,则授予客户端访问权限。之后,才会调用服务方法。

密码提供程序

我们知道,服务调用者必须在其 SOAP 请求中添加 UsernameToken,接收者必须处理安全令牌以验证发送者的用户名和密码。因此,给定一个用户名,必须有一些机制来检查密码与该用户的匹配程度。WSE 提供了一个称为密码提供程序的机制来处理此任务。

要注册密码提供程序,需要创建一个实现 Microsoft.Web.Services.Security.IPasswordProvider 接口的类。该接口有一个名为 GetPassword 的函数,它接受 Microsoft.Web.Services.Security.UsernameToken 作为输入参数。GetPassword 的任务是返回与 UsernameToken 中给出的用户名相关的密码。您可以使用任何机制来检索密码。最可能的情况是数据库相关的解决方案。

我的示例密码提供程序使用一个非常简单的用户名/密码关联机制。给定一个用户名,它会在数据库中搜索关联的密码,并由 GetPassword 返回。为了提供一个(几乎)可直接使用的示例,我使用了通过 ODBC 访问的简单文本文件作为数据源。您可能需要

来使示例正常工作。安装这些包后,ODBC 连接应该可以正常工作。

然后创建一个简单的 .txt 文件(我将其命名为 pwd.txt),其中包含您的用户名-密码组合,类似于以下模式。

第一行只是标题,可以省略。条目用制表符分隔,但您可以选择其他机制(例如 '|')。将文件放在您的密码提供程序可以访问的文件夹中 - 在我的测试场景中,我只是将其放在 IIS 目录中包含我们的 Web 服务的 bin 目录中。

实现 GetPassword 是密码提供程序需要完成的所有编码工作。因此,创建一个新的托管 C++ 项目(当然,您也可以使用 C#)命名为 PwdProvider。调用命名空间 WS_Security,并创建一个派生自 IPasswordProvider 的类 MyPasswordProvider。添加如下所示的 GetPassword 函数。

String* GetPassword(UsernameToken __gc* token)
{
  if( token == 0 )
    throw new ArgumentNullException();
    
  // get odbc path from app settings and build connection string  
  Object *odbcPath = System::Configuration::
   ConfigurationSettings::AppSettings->get_Item("odbcPath");
  String *connString = String::Format(S"Driver={0};DBQ={1}", 
    S"{Microsoft Text Driver (*.txt; *.csv)}", odbcPath);
  
  // create and open a new odbc connection 
  OdbcConnection *conn = new OdbcConnection(connString);
  conn->Open();
  
  //get name of the datasource and build a command string
  String *odbcFile = System::Configuration::
   ConfigurationSettings::AppSettings->get_Item("odbcFile")->ToString();
  String *cmdString = String::Format(S"SELECT Username, 
    Password FROM {0} WHERE Username='{1}'", 
    odbcFile, token->Username);

  // create a command and execute it
  OdbcCommand *cmd = new OdbcCommand(cmdString, conn);
  OdbcDataReader *dr = cmd->ExecuteReader(CommandBehavior::CloseConnection);
 
  // read the results and return our password
  if( dr->Read() )
    return (String*)dr->get_Item(S"Password");
  else
    throw new ApplicationException("Unable to retrieve password");
}

唯一有趣的一行是

return (String*)dr->get_Item(S"Password");

这里返回与给定用户关联的密码。其他行仅用于 ODBC 访问以检索密码。

检查应用程序配置是否包含指向您的文本文件位置的 odbcPathodbcFile 条目。一个简单的方法是将值添加到我们 Web 服务的 Web.config 中。稍后我们将进行研究。

一个 Web 服务

现在我们已经实现了密码提供程序,让我们来构建一个简单的 Web 服务。

首先通知 WSE 您密码提供程序的存在。这是通过向 Web 服务应用程序配置文件添加一些条目来完成的。第一个条目指定了一个 WSE 类,该类可以理解声明密码提供程序的配置条目。它放置在 Web.config 文件的 /configuration 位置。

<configSections>
  <section name="microsoft.web.services"
    type="Microsoft.Web.Services.Configuration.Web ServicesConfiguration,
      Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral, 
      PublicKeyToken=31bf3856ad364e35" />
</configSections>

同样,type 属性必须在一行中。引入 microsoft.web.services 元素后,必须添加声明您的密码提供程序的元素 /configuration/microsoft.web.services/security/passwordProvider

<microsoft.web.services>
  <security>
    <passwordProvider type="WS_Security.MyPasswordProvider, PwdProvider" />
  </security>
</microsoft.web.services>

<passwordProvider> 元素的 type 属性告诉 WSE 您实现了密码提供程序的类。在我的例子中,该类名为 MyPasswordProvider。它定义在 WS_Security 命名空间中,并且位于名为 PwdProvider 的程序集中。因此,此属性的一般形式是:NAMESPACE.CLASS, ASSEMBLY。

由于我们目前处理的是配置问题,让我们向 .../microsoft.web.services 添加第二个元素:diagnostics 元素。特别是对于测试和调试目的,记录 Web 服务接收和发送的 SOAP 消息始终是有帮助的。现在修改安全元素如下

<microsoft.web.services>
  <security>
    <passwordProvider type="WS_Security.MyPasswordProvider, PwdProvider" />
  </security>
  <diagnostics>
       <trace enabled="true" input="inputTrace.config" 
  output="outputTrace.config" />
  </diagnostics>
</microsoft.web.services>

这些行启用 SOAP 消息的跟踪,其中入站消息存储在 inputTrace.config 中,出站消息存储在 outputTrace.config 中,两者都位于 Web 服务 Dll 所在的文件夹中。

Web 方法

现在我们在服务中添加一个 web 方法。这是必不可少的 HelloWorld,调用时只返回一个字符串。

String __gc* Class1::HelloWorld()
{
  // get access to the SOAP message's context
  SoapContext* sc = HttpSoapContext::RequestContext;
  if( sc == 0 )
    throw new ApplicationException(S"Only SOAP-requests allowed!");
    
  bool valid = false;
  SecurityToken *st = 0;
  
  // iterate through all security tokens
  IEnumerator *ie = sc->Security->Tokens->GetEnumerator();
  while( ie->MoveNext() ) {
    st = (SecurityToken *)ie->get_Current();

 
    // if the securtiy token is a UsernameToken stop iteration and go on
    if( st != 0 && st->GetType()->Equals(__typeof(UsernameToken)) )
    {
      valid = true;
      break;
    }
  }
  
  if( valid == false )
    throw new ApplicationException(S"Invalid or missing security token");

  // return the token's username
  UsernameToken *ut = (UsernameToken*)st;
  return ut->Username;
}

更重要的是这个方法里面发生了什么。

SoapContext* sc = HttpSoapContext::RequestContext
if( sc == 0 )
  throw new ApplicationException(S"Only SOAP-requests allowed!");

用于检索 SOAP 请求的上下文,并验证实际上是否收到了 SOAP 消息。现在我们可以访问 SOAP 消息的 WS-Security 功能了。

下一步是遍历 SOAP 上下文的安全令牌部分。与 SOAP 消息相关的所有安全令牌都包含在 Tokens 集合中,该集合是 Security 类的成员。该类表示添加到 SOAP 消息的安全标头。它通过我们检索到的 SoapContext 访问

IEnumerator *ie = sc->Security->Tokens->GetEnumerator();

我们的目标是找到一个 UsernameToken,其中包含用于验证的用户名和密码。找到后,我们简单地返回发送者的用户名。

现在还剩下最后一个操作。我们仍然需要为 ODBC 数据源添加配置文件的条目。为简单起见,我们仅使用 Web 服务的 Web.config 文件。插入一个 <appSettings> 元素,并为 odbcPathodbcFile 添加一个 <add> 元素

<configuration>
...
  <appSettings>
    <add key="odbcPath" value="YOUR_FOLDER_PATH" />
    <add key="odbcFile" value="YOUR_FILE" />
  </appSettings>
...
</configuration>

现在构建 Web 服务并将其添加到 IIS 的 Web 发布文件夹。另外,让 PwdProvider.dll 可供服务使用,只需将其添加到 Web 服务的 bin 子文件夹即可。

客户端应用程序

为了完成场景,让我们构建一个利用服务器端实现的用户名验证(以密码提供程序和我们的 WebService 的形式)的客户端。

为了调用我们的 Web 服务,我们使用 Visual Studio 的内置功能来创建一个隐藏实际通信的代理。这可以很容易地通过选择 **Project->Add Web Reference** 来完成,该操作会调用 wsdl.exe 来为 Web 服务代理创建源文件。然后 wsdl.exe 创建一个 .cs 文件,其中包含执行所有 Web 服务调用的类。现在是我们必须介入的地方。Wsdl.exe 默认从 System.Web.Services.Protocols.SoapHttpClientProtocol 派生生成的类。为了能够使用 WSS 方法访问安全标头,代理类必须继承 Microsoft.Web.Services.WebServicesClientProtocol。这使我们可以访问 RequestSoapContextResponseSoapContext,从而允许我们处理 SOAP 消息的 WS-Security 标头。

现在创建一个方法来调用 Web 服务。这里的主要任务是创建一个新的 UsernameToken,其中包含我们的用户名和密码。

UsernameToken *ut = new UsernameToken("Otto",
  "Meier", PasswordOption::SendHashed);

这些行创建一个 <UsernameToken> 元素作为安全标头的一部分,并向其添加 <Username><Password> 字段。WSE 会自动附加 <Nonce><Created> 元素,并使用后三个元素计算密码的哈希值。
之后,该令牌将被添加到代理的 RequestSoapContext 属性。

ws->RequestSoapContext->Security->Tokens->Add(ut);

该示例假定 WebService 类名为 SimpleWebService。您可以在下方看到该方法的整体代码。

// create instance of the web service proxy
SimpleWebService *ws = new SimpleWebService();

// create a new UsernameToken and at it to the service' SOAP context
UsernameToken *ut = new UsernameToken("Otto", 
  "Meier", PasswordOption::SendHashed);
ws->RequestSoapContext->Security->Tokens->Add(ut);

Console::WriteLine(ws->HelloWorld());

现在请记住关于安全问题(拦截器使用令牌并认证自己的消息)的说法,并使用 <Created> 元素来限制请求的有效时间。正如我所说,这很容易实现——只需将以下行添加到上面的代码中

ws->RequestSoapContext->Timestamp->Ttl = 60000;

现在,如果服务器在创建消息后 60 秒收到消息,WSE 将自动拒绝该请求。

更多信息

如果您对这个主题感兴趣并正在寻找更多信息,可以尝试以下网站

安装示例

要测试包含的示例,您需要系统上运行 IIS。然后,构建 Visual Studio 项目。如果配置正确,Web 服务将自动添加到您的 IIS Web 文件夹中。PwdProvider.dllPwd.txt 必须放在其 bin 文件夹中,以便 WSE 找到它们。请注意,文件 WebService.dll 需要与 WSClient.exe 位于同一文件夹中,以便 Web 服务客户端可以找到它。然后,只需启动 WSClient.exe,如果一切按预期工作,“Otto”将由服务返回。

展望

本文介绍了 WS-Security 提供的基本身份验证机制,即用户名/密码客户端身份验证。本文系列的下一部分将进一步介绍 X.509 证书和 SOAP 消息(整体或部分)的数字签名。也许还会涉及其他一些内容——这取决于我未来几天……或几周的空闲时间 :)

© . All rights reserved.