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

使用 VS2008 中的客户端应用程序服务实现应用程序安全性

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.48/5 (14投票s)

2008 年 7 月 10 日

CPOL

13分钟阅读

viewsIcon

110779

.NET 3.5 中用于 Membership、Role 和 Profile 功能的 ASP.NET 提供程序框架以客户端应用程序服务(Client Application Services)的形式提供给非 ASP.NET 用户。这意味着,例如,在 WinForms 应用程序中,您可以像在 ASP.NET 中一样轻松地实现表单身份验证。

引言

集成应用程序安全性是一项持续的活动。微软提供了多种机制来实现这一点。大多数情况下,它们都基于 Windows 安全子系统。随着基于 Web 的应用程序的出现,Windows 安全子系统并非总是可行或可取的。因此,许多开发人员在其 Web 应用程序中实现了自定义的安全性子系统。微软对此做出了回应,在 ASP.NET 2.0 中内置了一个安全性子系统:ASP.NET Membership 和 Role Provider 框架。这个框架使 ASP.NET 开发人员无需构建自己的安全性系统。

不仅 ASP.NET Web 应用程序,WinForms 客户端应用程序也需要安全性,而且不总是基于 Windows 安全子系统。在这里,出现了与 ASP.NET 应用程序类似的趋势,即构建自定义的安全性系统并将其集成到 WinForms 应用程序中。许多人看到了 ASP.NET Membership 和 Role Provider 框架的潜力,并尝试将其集成到他们的 Windows 应用程序中。现在,通过 Visual Studio 2008 和 .NET 3.5,微软通过以客户端应用程序服务(Client Application Services)的形式为非 ASP.NET 客户端(例如 WinForms 应用程序)提供更便捷的访问此安全子系统的方式,对此做出了回应。

本文将不讨论也属于客户端应用程序服务一部分的配置文件服务。

安全需求

让我们退一步,讨论应用程序中的典型安全需求。

组织内管理的信息需要因各种原因受到保护。这种保护可以通过多种方式进行组织和强制执行。管理这些信息的应用程序只是安全的一个层面。本文将重点介绍这种应用程序级别的安全性。

核心需求之一是只允许授权人员使用相关应用程序。因此,我们需要某种东西来表示人员,以及某种他们可以用来证明自己身份的东西。一旦身份验证成功,该人员就可以使用该应用程序。在大多数应用程序中,用户无需反复进行身份验证。在整个应用程序使用过程中(或可能存在时间限制),他将被信任。有时,应用程序的特定部分仅限于特定人员,或者更确切地说,是具有组织内特定职能的人员。

还有许多其他安全需求

  • 用户问责制(审计)。
  • 传输或存储过程中数据的机密性和完整性(加密)。
  • 数据的连续性和可用性。
  • 安全管理。

应用程序安全需求

因此,用户需要一个“身份识别”来向安全子系统证明自己。这个“身份识别”将区分他与其他用户。此身份必须是相关应用程序安全子系统的一部分。会员资格将决定用户是否可以使用该应用程序。一个常用的标识符是用户帐户。

在使用受保护的应用程序之前,用户必须有机会向应用程序(即安全子系统)提供其用户帐户。常见的机制是登录屏幕。应用程序将强制执行此屏幕是唯一的入口。

但在授予在登录屏幕上提供用户帐户的用户访问应用程序的权限之前,我们要确保用户实际上是用户帐户的“所有者”。大多数情况下,“秘密”会与用户帐户关联。用户必须提供此“秘密”以及他们的用户帐户。安全子系统将验证秘密是否与用户帐户匹配。

有时,密码会受到一系列格式规则的约束,以使密码猜测稍微困难一些。有时用户在输入密码时会出错。安全系统可以根据算法决定给予用户第二次登录的机会。

一旦用户通过身份验证并进入应用程序,我们通常希望避免(在大多数情况下)用户在每次使用应用程序的某个部分之前都必须登录系统。换句话说,我们希望在身份验证后,“保留”一些与安全相关的信息。“例如,这可以用于存储与用户帐户关联的“权限”。

为了使用户帐户-权限关系易于管理,安全子系统可以引入一个安全配置文件。然后,该配置文件将与一组权限关联。每个用户帐户反过来将与一个或多个配置文件关联,而不是直接与单个权限关联。用户在该应用程序中被认为拥有特定的角色。

现在,应用程序的某些部分可能需要不同的安全要求。例如,只有属于管理员角色的用户才能使用应用程序的更新功能。因此,通过角色信息,我们可以在应用程序中引入逻辑来检查特定代码是否对相关用户可用。此外,我们还可以采取“主动”方法,根据特定用户在该应用程序中的角色显示或隐藏某些 GUI 元素,而不是“被动”响应。

有时我们的应用程序会与其他系统(例如安全子系统)交换数据。我们不总是希望在网络上传输明文密码。为了保持数据的机密性,我们可能需要利用应用程序运行平台提供的其他安全子系统(例如加密)。

微软提供了实现这些逻辑安全机制和模式的方案。例如,基于 Windows 组的标准 .NET 基于角色的授权机制,以及 Enterprise Services (COM+ 基于角色的系统)。Microsoft Patterns and Practices 团队在 Microsoft Enterprise Library 中还有一个特殊的应用程序块,即 Security Application Block。在 Windows 2003 和 XP Service Pack2 中,您可以使用 Microsoft Authorization Manager 框架来构建基于角色的应用程序。

让我们看看如何在 Visual Studio 2008 中使用客户端应用程序服务来实现这些应用程序安全需求。本文未提供任何代码,但您可以在接下来的段落中完成“练习”。

客户端应用程序服务

客户端应用程序服务(Client Application Services)建立在 ASP.NET 2.0 框架中已有的几个功能之上,但现在已提供给非 ASP.NET 应用程序使用。

AspNet20ProviderFramework.JPG

现有的 ASP.NET Membership、Role 和 Personalization 提供程序现在对 WinForms 应用程序开放,这要归功于 `System.Web.Extensions` 命名空间中的代码。这对于服务器端的消费者是正确的。

ClientApplicationServicesOverview.JPG

此外,Visual Studio 2008 得到了增强,可以简化配置方面的工作,从而让 WinForms 应用程序与 Membership 和 Role 服务进行通信。

步骤 1

我们的“练习”的第一步是创建一个“存储库”来管理我们的用户帐户、密码和角色。因为我们不关注 Windows 安全子系统,所以我们将选择 SQL Server 数据库作为我们的存储库。幸运的是,有一个实用程序可用于创建这样的 SQL Server Membership Provider 数据库:*aspnet_regsql.exe*。您可以在文件夹 *\WINDOWS\Microsoft.NET\Framework\v2.0.50727* 中找到此可执行文件。此向导将创建管理用户帐户、密码、角色以及用户/角色关联所需的数据库(表和存储过程)。

第二步

尽管您刚刚创建的数据库中有几个存储过程,但它仍然相对静态。我们需要一个应用程序来管理数据库中的数据。通过管理,我的意思是执行用户、密码和角色关联的 CRUD 操作。幸运的是,我们不必自己编写这些代码。这时 ASP.NET Membership 和 Role Provider 框架就派上用场了。

下一步是创建一个 ASP.NET 3.5 Web 应用程序,它将托管 Membership Provider 和 Role Provider。当您构建 ASP.NET 3.5 Web 应用程序或 Web 服务时,您会在 `web.config` 文件中注意到一些额外的配置条目(稍后在文章中会介绍)。此外,还会添加一个指向 `System.Web.Extensions.dll` 的引用,其中包含 WCF 服务实现,这些实现提供了对 Membership Provider 和 Role Provider 的远程访问。

得益于 Visual Studio 的集成,您可以在 3.5 网站项目中打开 Membership/Role Provider 的管理部分(这也是一个 Web 应用程序,但只能从 Visual Studio 内部调用)。

AspNetConfiguration_small.jpg

一个有价值的替代方案是 Idesign (Juval Löwy) 的 Credential Manager。

IdesignCredentialManager.JPG

步骤 3

那么,您需要在 Web 配置文件中更改什么?

我们需要一个指向 Membership 数据库的 ADO.NET 连接字符串。

<connectionStrings>
  <add name="testCAS" connectionString="Data 
       Source=myMachine;Initial Catalog=testCAS…/>
</connectionStrings>

Membership 和 Role Provider 激活

<system.web>
……
    <membership defaultProvider="TestCASSqlProvider">
        <providers>

            <clear/>
            <add name="TestCASSqlProvider" 
                 type="System.Web.Security.SqlMembershipProvider" 
                 connectionStringName="testCAS" 
                 applicationName="testCAS"/>
        </providers>
    </membership>
    <roleManager enabled="true" defaultProvider="TestCASSqlRoleProvider">
        <providers>

            <clear/>
            <add name="TestCASSqlRoleProvider" 
                 connectionStringName="testCAS" 
                 applicationName="testCAS" 
                 type="System.Web.Security.SqlRoleProvider"/>
        </providers>
    </roleManager>
….
</system.web>

Forms Authentication 激活

<system.web>

….
    <authentication mode="Forms" />
….
</system.web>

身份验证和角色(WCF)服务的激活。

<system.web.extensions>
    <scripting>
        <webServices>
            <authenticationService enabled="true" requireSSL="false"/>

            <roleService enabled="true"/>
        </webServices>
    </scripting>
</system.web.extensions>

步骤 4

现在,是时候配置将使用我们 ASP.NET 应用程序中托管的身份验证和角色服务的 WinForms(或 WPF)应用程序了。通过 Visual Studio 2008 项目属性中的新“Services”选项卡,您可以轻松完成此操作。选中“Forms Authentication”单选按钮将添加对 `System.Web.Extension` 的引用。此程序集包含所有服务器端逻辑以及所有客户端逻辑。还有一个文本框,您可以在其中输入充当身份验证服务和角色服务主机的 ASP.NET Web 应用程序的 URL。您还可以指定一个实现特殊接口的窗体,该窗体将充当登录屏幕。这对于实现某些场景(如初始登录)非常有用。

<system.web>
    <membership defaultProvider="ClientAuthenticationMembershipProvider">
      <providers>
        <add name="ClientAuthenticationMembershipProvider" 
           type="System.Web.ClientServices.Providers.
                       ClientFormsAuthenticationMembershipProvider, 
                 System.Web.Extensions, Version=3.5.0.0, …" 
           serviceUri="http://myMachine/testCAS/Authentication_JSON_AppService.axd" 
           credentialsProvider="TestCASClient.LoginFormCAS,TestCASClient" />

      </providers>
    </membership>
    <roleManager defaultProvider="ClientRoleProvider" enabled="true">
      <providers>
        <add name="ClientRoleProvider" 
             type="System.Web.ClientServices.Providers.ClientRoleProvider, 
                   System.Web.Extensions, Version=3.5.0.0, …" 
             serviceUri="http://myMachine/testCAS/Role_JSON_AppService.axd" …." />
      </providers>

    </roleManager>
  </system.web>

ServicesTab.JPG

步骤 5

别忘了添加对 `System.Web` 的引用,以便使用 Membership 和 Role 命名空间。尽管 `System.Web.Extension` 包含实际代码。客户端代码也使用标准的 Membership 和 Role 命名空间来以编程方式验证用户或确定用户是否属于某个角色。因此,配置就足够了。让我们来写一些代码。

幕后

使用 CAS 的 API 非常直接。如果您曾经使用过 ASP.NET Membership 进行过编程,您会看到……它就是相同的 API。这当然是提供程序模型的核心。因此,例如,让我们看看执行以下语句时会发生什么,

Dim valid As Boolean = _
  System.Web.Security.Membership.ValidateUser("Alice", "AliceHer1pwd!")

Membership Provider 框架将通过指定的配置确定需要实例化哪个具体的实现。在我们的例子中,它将是 `System.Web.ClientServices.Providers` 命名空间中的 `ClientFormsAuthenticationMembershipProvider`。代码将构造并向身份验证服务发送 HTTP POST 请求,以询问提供的用户帐户和密码是否有效。消息正文不包含 SOAP,而是以 JSON 格式编写。

如果执行该语句,HTTP 消息(明确未启用 SSL!)如下所示

POST /testCAS/Authentication_JSON_AppService.axd/Login HTTP/1.1
Content-Type: application/json; charset=utf-8
Host: myStation
Content-Length: 71
Expect: 100-continue
Proxy-Connection: Keep-Alive

{"userName":"alice","password":"AliceHer1pwd!","createPersistentCookie":false}

此 POST 请求发送到的 URI 是从我们刚刚使用 Visual Studio 2008 中的 Services 选项卡创建的 `app.config` 文件中检索到的。Visual Studio 2008 会在指定的 URL 后面添加一部分,并引用我们在 ASP.NET 3.5 应用程序中创建的用于托管这些服务的 Membership(身份验证)和 Role 服务。后缀 `_AppService.axd` 将触发 ASP.NET 应用程序中的 HTTP 处理程序。此处理程序将根据 MIME 类型确定它是 SOAP 请求还是 JSON 请求。根据 URL 的其他部分,将调用身份验证服务或角色服务来处理请求。

<httpHandlers>
<remove verb="*" path="*.asmx"/>
<add verb="*" path="*.asmx" 
  validate="false" 
  type="System.Web.Script.Services.ScriptHandlerFactory, 
        System.Web.Extensions, Version=3.5.0.0, 
        Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add verb="*" 
  path="*_AppService.axd" 
  validate="false" 
  type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, 
        Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add verb="GET,HEAD" 
path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, 
System.Web.Extensions, 
Version=3.5.0.0, 
Culture=neutral, 
PublicKeyToken=31BF3856AD364E35" 
validate="false"/>

</httpHandlers>

深入到代码中,我们最终会进入 `System.Web.Security.SqlMembershipProvider` 命名空间。如果我们查看 SQL 流量,我们会看到以下语句被发送到 SQL Server

检索密码

exec dbo.aspnet_Membership_GetPasswordWithFormat @ApplicationName=N'testCAS', 
    @UserName=N'alice',@UpdateLastLoginActivityDate=1, 
    @CurrentTimeUtc='2008-05-30 07:12:18:760'

密码验证后的一些统计信息

exec dbo.aspnet_Membership_UpdateUserInfo @ApplicationName=N'testCAS', 
    @UserName=N'alice',@IsPasswordCorrect=1,@UpdateLastLoginActivityDate=1……

Web 方法的 HTTP 响应如下所示

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.1
Date: Fri, 30 May 2008 06:55:36 GMT
X-Powered-By: ASP.NET
Set-Cookie: .ASPXAUTH=28818033DF1ACC3638F96196B78D60AA44BB2F734F…..; path=/; HttpOnly
Cache-Control: private, max-age=0
Content-Type: application/json; charset=utf-8
Content-Length: 10

{"d":true}

正文包含我们身份验证请求的答案,仍然是 JSON 格式。`ClientFormsAuthenticationMembershipProvider` 将“分析”此响应并采取相应行动。当用户帐户/密码有效时,身份验证代码还会创建两个对象来表示已登录的用户:`ClientRolePrinciple` 和 `ClientFormsIdentity`。例如,使用 `RolePrincipal`,您可以询问“系统”当前登录的用户是否属于某个角色。这将形成以编程方式编写授权代码(被动或主动)的基础。您可以通过对 `System.Threading.Thread.CurrentPrincipal`(以及 `System.Threading.Thread.CurrentPrincipal.Identity` 用于 `FormsIdentity`)执行强制转换来使用此功能。

Dim RolePrincipal As System.Web.ClientServices.ClientRolePrincipal = _
    DirectCast(System.Threading.Thread.CurrentPrincipal,  _
    System.Web.ClientServices.ClientRolePrincipal)

Dim FormsIdentity As System.Web.ClientServices.ClientFormsIdentity = _
    DirectCast(System.Threading.Thread.CurrentPrincipal.Identity,  _
    System.Web.ClientServices.ClientFormsIdentity)

我想跟踪的另一个场景是,当您询问用户是否属于某个角色时会发生什么。以编程方式,这可以通过 `RolePrincipal.IsInRole(“Manager”)` 来执行。这次 `System.Web.ClientServices.Providers` 命名空间中的 `ClientRoleProvider` 负责建立 Web 请求(同样是 JSON 格式)。这将导致发送以下 HTTP POST 请求。因此,首先,将检索特定用户的所有角色。

POST /testCAS/Role_JSON_AppService.axd/GetRolesForCurrentUser HTTP/1.1
Content-Type: application/json; charset=utf-8
Host: myStation
Cookie: .ASPXAUTH=28818033DF1ACC3638F96196B78D60AA44BB2……
Content-Length: 0

角色服务将触发对 SQL Server 的调用。

exec dbo.aspnet_UsersInRoles_GetRolesForUser @ApplicationName=N'testCAS',
    @UserName=N'alice'

答案将是

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.1
Date: Fri, 30 May 2008 06:55:36 GMT
X-Powered-By: ASP.NET
Cache-Control: private, max-age=0
Content-Type: application/json; charset=utf-8
Content-Length: 31

{"d":["Manager"]}

然后,代码(客户端)将确定方法调用中提供的角色是否在检索到的角色列表中(在我们的例子中,用户只属于一个角色)。此信息将存储在我们的“会话”中,即 RolePrincipal。因此,后续的调用不会导致对服务的调用(但会涉及超时元素!)。

我只向您展示了一些场景。您还可以调查的其他场景包括

  • 注销
  • 离线登录
  • 标识流到后端服务
  • 机密性(SSL 启用的 Web 应用程序)
  • SQL Server 和区域性敏感字符
  • 等等。

结论与建议

我认为大多数应用程序安全需求都可以通过客户端应用程序服务中的实现来解决。

  • 您可以使用非 Windows 用户帐户来识别您的应用程序的用户。
  • 有一个基于角色的安全设置。
  • 您有一个管理程序来管理用户、他们的密码和他们的角色。
  • 密码验证由 SQL Membership Provider 完成。
  • 身份验证后,会维护一个“会话”以保留与安全相关的信息,例如用户的角色。
  • 如果您输入了错误的密码,有一个开箱即用的重试机制。
  • 您可以配置所需的密码强度。
  • 通过在 API 中使用条件语句,您可以根据用户的角色隐藏应用程序的某些部分。
  • 身份验证服务可以配置为使用 SSL(本文中明确禁用)。
  • 等等。

Visual Studio 2008 中的客户端应用程序服务的 Membership 和 Role 服务为 WinForms(或 WPF)开发人员提供了实现最常见安全需求所需的所有工具。通过一些配置工作,您可以解锁大量您不必自己编程的功能。用于访问此系统的 API 非常直接。

所以,下次您需要在 WinForms 应用程序中实现应用程序安全性时,可以尝试一下客户端应用程序服务,看看它是否足够。如果是,它将为您节省大量开发时间。

参考文献与延伸阅读

  • Daniel Moth,使用 Visual Studio 2008 的客户端应用程序服务,http://channel9.msdn.com/posts/DanielMoth/Client-Application-Services-with-Visual-Studio-2008/
  • Mike Taulty's Blog,客户端应用程序服务,http://mtaulty.com/CommunityServer/blogs/mike_taultys_blog/archive/2007/06/03/9463.aspx
  • Juval Lowy,以智能(客户端)方式管理自定义安全凭据,http://www.code-magazine.com/Article.aspx?quickid=0511031
  • 统一 Windows Forms 和 ASP.NET 提供程序以进行凭据管理,Juval Lowy,http://msdn.microsoft.com/en-us/magazine/cc163807.aspx
  • Michele Leroux Bustamante,更少的代码保护 ASP.NET 应用程序,http://msdn.microsoft.com/en-us/library/aa479008.aspx
  • MSDN,客户端应用程序服务,http://msdn.microsoft.com/en-us/library/bb384297.aspx
© . All rights reserved.