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

使用表单身份验证保护您的 Web 服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (10投票s)

2007年12月17日

CPOL

8分钟阅读

viewsIcon

64974

downloadIcon

916

使用 ASP.NET 表单身份验证为你的 Web 服务添加安全级别。

Screenshot -

引言

Web 服务非常棒。它们灵活且跨越语言边界。然而,在大多数情况下,保护 Web 服务公开的功能非常有用。

如果你想为你的 Web 服务添加自定义身份验证机制,你可以

  • 为每个 Web 方法请求用户凭据(非常丑陋)。
  • 编写某种必须先调用的验证代码,该代码会提供一个令牌。该令牌将随每个请求一起发送,Web 服务必须验证该令牌。此方法要求使用者非常了解你的 API,并且需要你实现某种 Web 状态机来强制令牌过期等。
  • 最干净的方法(至少在我看来)是要求使用者(浏览器等)在访问 Web 服务之前进行验证。这是我将在本文中讨论的方法。

在 IIS 上为 Web 服务添加身份验证级别的最简单方法是简单地拒绝对 Web 服务所在目录的匿名访问,然后让 IIS 针对 Active Directory 验证请求。

但是,如果你想以更灵活的方式控制对 Web 服务的访问,而无需将用户添加到 Active Directory,会发生什么?事情就变得棘手了。

ASP.NET 引入了一种名为 FormsAuthentication 的很棒的身份验证机制,它捆绑了用于创建和管理用户(通常是 SQL Server)的控件。这提供了非常灵活的身份验证级别,并将所有授权保留在应用程序范围内。

唯一的问题是它不是真正的 HTTP 身份验证机制,因此 Web 服务使用者无法真正对其进行验证。

这就是 HttpModules 发挥作用的地方,我们将看到如何通过 FormsAuthentication Membership Provider 管理身份验证。

HttpModules

ASP.NET 提供了一种将代码插入 HTTP 请求管道内部的机制。我不会详细介绍它是如何工作的,除了你创建一个实现 IHttpModule 的类,并在 web.config 文件中注册你的模块。从那时起,ASP.NET 会在每次向其发出请求时通知你的模块。

所以我们要做的就是:构建一个 IHttpModule,它将拦截发往我们 Web 服务的任何请求,并向请求添加 HTTP 身份验证。

WWW-Authenticate

在 HTTP 调用中,如果服务器需要登录凭据,它会在响应中添加 WWW-Authenticate 标头。收到响应后,客户端需要提示用户输入登录凭据,然后将这些凭据通过 WWW-Authenticate 标头发送回原始调用的同一地址。

发送 HTTP 凭据有几种方法,但我们将只讨论其中两种

  • Basic - 使用简单的文本帖子
  • Digest - (IE 5.5+ 支持) 使用 MD5 散列

基本身份验证

Basic Login

最简单、显然最容易实现的是 Basic。在此方法中,浏览器将响应编码为包含用户名:密码的 Base64 字符串。然后由我们来解码该字符串并验证请求。

正如你可能已经意识到的,这里没有多少加密,并且凭据可以在普通连接上相对简单地解码。但是,在 HTTP (SSL) 上情况并非如此,因此除非你有 SSL 连接,否则此方法不常用。

摘要式身份验证

Digest Login

在不深入探讨的情况下,在 Digest 模式下,服务器向客户端发送一个唯一的 nonce,客户端反过来必须使用 MD5 散列算法,将用户凭据与提供的 nonce 一起加密。生成的散列发送回服务器,由服务器负责接受凭据。

这种方法在基本连接上更安全;但是,目前仅受 IE 支持。

Using the Code

在添加提供的代码之前,必须将你的 Web 应用程序设置为使用 FormsAuthentication。使用哪个提供程序并不重要,只要它在 FormsAuthentication Membership Provider 下运行即可。

提供的代码包含一个实现 IHttpModule 的类,名为 ServiceIdentityManager

提取该类并将其添加到你的 app_code 目录(或者,创建一个新项目并将其添加到其中;编译然后从你的网站引用该项目)。

注册模块

我们需要做的第一件事是将模块注册到 ASP.NET。这可以通过在 <system.web> 部分下的 web.config 文件中添加以下 XML 片段来完成

<httpModules>
    <add name="IdentityManager" type="ServiceIdentityManager"/>
</httpModules>

这告诉 ASP.NET 它应该加载位于 ServiceIdentityManager 类中的我们的模块。

何时验证

模块注册后,每次向服务器发出新调用时都会初始化它。那时我们连接我们的事件。由于我们正在处理身份验证过程,我们将连接 Init 函数中发送给我们的 HttpApplicationAuthenticateRequest 事件。

从那时起,每当服务器请求身份验证时,我们都会被调用。但是,这对于我们整个 Web 应用程序是全局的,我们可能不希望每次请求都出现登录表单,因此我们必须在此处检查是否需要运行我们的身份验证代码。

Validating

在提供的代码中,我在每次请求时调用 NeedsAuthentication 函数,以测试请求是否指向 Services 目录下的文件。你应该覆盖此方法并添加你自己的逻辑。

提供的代码有一个名为 AuthenticationMode 的属性,它指定模块应使用哪种类型的身份验证(Basic 或 Digest)。它还提供一个名为 TimeoutMinutes 的属性,该属性指定安全票证的有效分钟数。最后,一个名为 Domain 的属性指定将呈现给客户端的身份验证请求的名称。

当一个需要我们进行身份验证的请求被发送到服务器时,代码会检查请求中是否存在 WWW-Authenticate 标头。如果标头不存在,它会根据设置的 AuthenticationMode 发出带有所需标头的响应。

void context_AuthenticateRequest(object sender, EventArgs e)
{
    HttpApplication httpApplication = (HttpApplication)sender;
    //Check if we need to authenticate this request
    if (!NeedsAuthentication(httpApplication))
        return;

    //Check if an authorization ticket is present
    if(string.IsNullOrEmpty(httpApplication.Request.Headers["Authorization"]))
        requestAuthentication(httpApplication);//Request authorization
    else
    {
        string sToken = httpApplication.Request.Headers["Authorization"];
        //Read the token and validate base on its type
        if (sToken.StartsWith("Basic", StringComparison.CurrentCultureIgnoreCase))
            validateBasicAuthentication(httpApplication, sToken);
        else if (sToken.StartsWith("Digest", StringComparison.CurrentCultureIgnoreCase))
            validateDigestAuthentication(httpApplication, sToken);
        else
            requestAuthentication(httpApplication);
            //We can't understand this token, request one we will
    }
}

private void requestAuthentication(HttpApplication httpApplication)
{
    if (AuthenticationMode == AuthenticationModes.Digest)
        requestDigestAuthentication(httpApplication);
    else
        requestBasicAuthentication(httpApplication);
}

身份验证

客户端会被提示并回复凭据。这些凭据会被解码并与 FormsAuthentication Membership Provider 进行验证。

对于 Basic 身份验证,密码以明文形式提供,因此我们使用它来调用

Membership.ValidateUser(userName,password);

并查看凭据是否有效。

对于 Digest 身份验证,问题有点棘手。在 validateDigestAuthentication 方法中,我们基于发送的用户名和我们从 Membership Provider 中提取的密码创建一个新的散列。我们检查此散列是否与客户端发送的散列匹配,如果匹配,我们认为它已验证。

注意:要使 Digest 身份验证正常工作,模块必须能够从 Membership Provider 获取用户的密码。有关如何设置此项的信息,请参阅 MSDN 文章。

成功验证用户后,我们需要将 HttpContext 的 User principle 设置为一个有效的 FormsAuthentication 对象。

这是通过创建一个新的 RolePrinciple 对象来完成的。构造函数需要一个 Identity 对象,我们将其以 FormsIdentity 的形式提供,该对象是使用基于给定用户名的 FormsAuthenticationTicket 创建的。然后将其设置为 HttpContext 的 User Principal 以供进一步使用。

private void setPrinciple(HttpApplication httpApplication, string userName)
{
    //Create a FormsAuthenticationTicket for our roles principle
    RolePrincipal rPrince = new RolePrincipal(new FormsIdentity(
        new FormsAuthenticationTicket(userName, false, TimeoutMinutes)));

    httpApplication.Context.User = rPrince;
}

模块冲突

到目前为止,事情一直很顺利;然而,现在我们遇到了一个棘手的部分。

由于我们正在针对 FormsAuthentication Provider 进行身份验证,因此必须将站点设置为使用此身份验证。但是,当我们向客户端发出 WWW-Authenticate 请求时,响应将不会到达我们的模块。相反,它将被 FormsAuthentication Module 拦截,该模块将尝试验证响应,从而搞乱我们的代码。

那么,我们如何阻止 FormsAuthentication 模块这样做呢?最简单的方法是禁用它。这可以通过在 <system.web> 部分下的 web.config 文件中添加以下 XML 片段来完成

<httpModules>
    <remove name="FormsAuthentication" />
    <add name="IdentityManager" type="ServiceIdentityManager"/> 
</httpModules>

但是如果我们删除了 FormsAuthentication 模块,那么我们站点的其余部分就无法使用 FormsAuthentication 了。糟糕。

在理想情况下,我们会从模块列表中删除 FormsAuthentication 模块,将我们的模块添加到列表中,然后重新添加 FormsAuthentication。你说很简单?嗯,我希望如此。不幸的是,这不会奏效;由于某种原因,FormsAuthentication 模块仍然会在我们的模块之前被调用。

因此,解决方法(我承认这有点hack)是删除 FormsAuthentication 模块,添加我们自己的模块,然后用新的名称重新添加 FormsAuthenticationModule。

<httpModules>
    <remove name="FormsAuthentication" />
    <add name="IdentityManager" type="ServiceIdentityManager"/> 
    <add name="FormsAuthenticationOld" 
      type="System.Web.Security.FormsAuthenticationModule"/> 
</httpModules>

这样就解决了问题。

还剩下什么?

一旦我们实现了上述代码,对位于 Services 目录下的任何资源发出的请求都将被要求发送登录凭据。如果通过 Web 浏览器完成,浏览器将提示用户输入用户名/密码。如果你尝试连接你的 Web 服务代码,那么只需将 Web 服务的 Credentials 属性设置为一个新的 System.Net.NetworkCredential(包含用户名和密码)。

贡献

部分基于 Peter A. Bromberg 的文章 True ASP.NET Digest Authentication With Database

历史

暂无……

© . All rights reserved.