用WSE在Web服务中验证用户身份(2)

四、基本的用户名令牌认证

  在我们数字签名SOAP消息之前,必须先弄清楚谁正在签名。因此,我们将探讨一下用户名令牌(UsernameToken)的概念,同时了解WSE如何允许我们验证用户名令牌。

  为了在Web服务中使用WSE验证用户名/密码,我们需要知道WSE在这方面为我们提供了什么?WS-Security定义了一个UsernameToken元素,它提供了基本用户名/密码验证的方法。如果你有使用HTTP的经验,那么你会发现UsernameToken与Basic Authentication非常类似。有三种用户名令牌,但是通常情况下我们只对最后两种最感兴趣:

<!--明文密码-->
<UsernameToken>
<Username>user1</Username>
<Password Type="wsse:PasswordText">suangywang</Password>
</UsernameToken>

  这种方法使用明文密码。我们不难想象,在服务器上将进行核对数据库,验证用户名与密码,看是否有匹配的用户名/密码对这一系列验证操作。

<!--密码摘要-->
<UsernameToken>
<Username>user1</Username>
<Password Type="wsse:PasswordDigest">
QSMAKo67+vzYnU9TcMSqOFXy14U=
</Password>
</UsernameToken>

  这种方法发送一个密码摘要(digest)代替明文密码。使用密码摘要,密码就不会通过网络发送,这样黑客就不太可能算出Web服务的密码。密码摘要是用散列函数计算的。这个过程只是单向的,意味着将函数反向并找到对应于摘要的消息是不可能的,因为该过程以这样一种方式实现,所以找到散列到同一摘要的两条不同密码在计算上难以实现。但是黑客可以发送散列密码,然后冒充原始发送人被验证。为了避免这个问题,Web Services Security Addendum(Web服务安全补遗)已经增加一个辅助的保护措施。补遗中规定必须发送密码的摘要版本,而不仅仅发送散列密码。这个摘要信息包含一个密码散列,标识请求的唯一的Nonce和创建时间。因此绝对不会出现相同的两个密码散列。如下所示是修正后的用户名令牌UsernameToken。

<!--修正后的用户名令牌-->
<wsse:UsernameToken
xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility"
wsu:Id="SecurityToken-59845323-5dcb-4a6b-a7fb-94a0d7357a20">
<wsse:Username>User1</wsse:Username>
<wsse:Password Type="wsse:PasswordDigest">
gpBDXjx79eutcXdtlULIlcrSiRs=
</wsse:Password>
<wsse:Nonce>
h52sI9pKV0BVRPUolQC7Cg==
</wsse:Nonce>
<wsu:Created>2003-6-20T21:16:50Z</wsu:Created>
</wsse:UsernameToken>

  虽然每个合法请求都有一个不同的散列,但是你也必须防止恶意用户把其他用户的合法请求中的整个UsernameToken拿出放入自己的非法请求中。你可以使用Timestamp(时间戳标头)来最小化这种危险。时间戳标头用来表示消息的创建时间和过期时间,指明消息的周期以及何时可以认为该消息失效。 例如,你可能想指定消息在40秒以后失效,并且超过40秒服务器就不会接收UsernameToken。但是机器之间的时钟同步问题可能会造成有效的请求被拒绝的情况。所以使用时间戳也并不是一个尽善尽美的解决方法。为了解决这个问题,Web服务可以保存一张最近收到的UsernameToken的Nonce值的表,如果收到的一个请求的Nonce值已经被使用了,那么就绝对不会接受这个请求。如果你接收几个使用相同Nonce的请求,那么你要考虑把这几条请求全部丢弃,因为很有可能先到的请求是非法请求。还要了解到Nonce核对技术并不能防止恶意用户截获合法的输入信息,并把原始信息中的UsernameToken加入自己的消息,然后发送到目的地。这时就需要为消息添加数字签名或安全证书,以保护其不受攻击。数字签名和安全证书的相关知识在本文中不会涉及,请读者查阅相应文献。

  所有的散列保护都需要消息发送端和接收端知道用户的密码。在客户端,人们期望系统能够提示用户输入密码。而在服务器端,需要保存带有有效用户名/密码对的表,以供系统查找。我们下面将介绍WSE如何使用一个Password Provider(密码提供者)机制来解决这两个问题。

  五、IPasswordProvider接口

  WSE定义了一个Microsoft.Web.Services.Security.IPasswordProvider接口类,我们必须实现这个类来注册一个密码提供者。这个接口有一个方法GetPassword,它接收一个Microsoft.Web.Services.Security.UsernameToken作为输入参数,该方法返回指定用户的密码。其思想是你可以使用任何你想用的机制保存有效的用户名/密码对,然后提供了一个实现IPasswordProvider接口的类,来让WSE访问你的特定密码存储机制。你甚至可以执行你自己的UsernameToken的摘要(Digest)和散列(Hash)的组合,甚至使用一个共享的密码,以进一步控制你的认证基础结构。

  为了把你特定的Password Provider(密码提供者)告诉WSE,你必须配置合适的WSE设置。首先要添加一个Microsoft.Web.Services元素到应用程序的配置文件中的配置元素中。还要指定可以读懂特定配置信息的WSE类。可以把下面的configSections添加到机器上的Machine.config或单独的Web.config中。

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

  在本例中,我们将使用Northwind数据库Employees表的一个修改版本来进行查询任务。因为PasswordProvider接口需要返回一个与UsernameToken对象的密码部分匹配的实际密码,所以通常,我们只需要使用WSE加密我们的用户名和密码,然后再通过网络传送给Web服务。

  如果你在Solution Explorer中选中你的工程并在其上点击右键,你将看到在底部增加了一个新的菜单“WSE Settings”,你可以在其中设置所有重要的配置和其它使用WSE的配置:

  这可让我们很容易的设置Password Provider Implementation(密码提供者实现)元素,Decryption Key Provider Implementation(解密钥提供者)元素,X.509 Certificate(X.509 证书)设置,甚至是我们希望使用的Binary Security Tokens(二进制安全令牌)。此外,其他的选项卡还可以配置用于WSE管道的输入输出过滤器,配置路由,启动诊断功能等等。虽然它不能做我们想做的每件事,但这是WSE易用化的一个良好的开端。

  PasswordProvider安全元素是web.config中的<configuration>父元素的一个子元素,它告诉WSE你使用哪个类来实现PasswordProvider接口:

<microsoft.web.services>
 <security>
  <!-- NAMESPACE . CLASSNAME , ASSEMBLYNAME -->
  <passwordProvider type="WSESecurity.WSEPasswordProvider, WSESecurity" />
 </security>
</microsoft.web.services>

  让我们看看在本例中如何实现它:

namespace WSESecurity
{
 public class WSEPasswordProvider : IPasswordProvider
 {
  public string GetPassword(UsernameToken token)
  {
   try
   {
    SqlConnection cn = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["SqlConn"].ToString());
    cn.Open();
    SqlCommand cmd = new SqlCommand("SELECT Username, password from Employees where username ='" + token.Username + "'",cn);
    SqlDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
    dr.Read();
    return dr["password"].ToString();
   }
   catch(Exception ex)
   {
    throw new Exception (ex.Message);
   }
  }
 }
}

  上面我们给出的代码可以完全实现IPasswordProvider接口,通过用户名/密码来验证一个用户,当然了,还可以把它做得更复杂一些,这请读者们自己去完成。实际上,我们在编程的过程中基本没有写太多用户验证的代码,大部分工作都由WSE暗中处理了
原文地址:https://www.cnblogs.com/wzyexf/p/408469.html