使 ClickOnce 适用于 ASP.NET 窗体身份验证






4.36/5 (9投票s)
一种使用 ASP.NET 窗体身份验证保护 ClickOnce 应用程序访问的解决方案。
引言
如果您以前尝试过通过 Internet 保护对在线 ClickOnce 部署应用程序的访问,那么您可能遇到过 ClickOnce 不支持除 Windows 集成之外的任何安全机制,而且即使是 Windows 集成也仅限于内部网。
我们正在工作的开发商店提供了一个需要通过 ClickOnce 部署的企业级应用程序(更具体地说,它是一个 .NET 3.0 WPF XBAP),对于我们的一些客户来说,说“Internet 上的任何人都可以下载我们应用程序的客户端部分,但我们的应用程序安全将阻止他们将该客户端连接到后端服务”是不可接受的。简而言之,我们需要一种方法来提供一些凭据,并防止应用程序甚至部署到未经授权的客户端。
背景
ClickOnce 是一项非常酷的部署技术,但它似乎常常没有得到其父级(即 Microsoft)的多少爱护。尽管这可能有点不公平,因为我们怀疑它的一些缺陷实际上与 ClickOnce 需要如何与 Internet Explorer 和 FireFox 交互有关。
首先,我们需要讨论为什么大多数传统的网站安全方法不适用于 ClickOnce。ClickOnce 的许多操作方式都笼罩在神秘之中,并且没有文档记录,但这是我们认为我们能够从事件序列中收集到的信息。这特定于 ClickOnce XBAPS,但也应该适用于 .NET 2.0/3.0 在线应用程序。
假设我们已启用窗体身份验证并将其设置为保护部署中的所有文件。
注意:本文不是关于如何使用/配置 ClickOnce 的入门指南;如果您不熟悉这些技术的详细信息,我们建议您首先阅读尽可能多的 MSDN 文档,并访问 ClickOnce MSDN 论坛。
- 您从 Internet Explorer 请求部署清单(以及 .NET 3.5 中的 Firefox,但我们现在不讨论这个)。
- IE 会向 Web 服务器发出请求,尝试请求 * .xbap *。
- IIS 返回一个重定向到窗体身份验证登录页面。
- 我们输入登录凭据,然后点击提交。
- 我们的登录凭据流回 IIS/ASP.NET,我们经过身份验证,然后收到 * .xbap *。
- 好的,现在 IE 已经有了 * .xbap *,所以它会将其传递给 ClickOnce。
- ClickOnce 将尝试重新下载 * .xbap *(不确定它为什么这样做,也许 IE 无法直接传递文件内容,或者 ClickOnce 可能只是出于安全原因需要再次重新下载)。
- 这里是事情开始出错的地方,ClickOnce 没有 IE 设置的身份验证 cookie。因此,此请求将失败。
- 假设 ClickOnce 已经能够以某种方式检索到 * .xbap *。对文件的下一次请求(可能是应用程序清单)将不包含任何关于如何针对 IIS/ASP.NET 进行身份验证的安全上下文,因此,该请求将失败。
Microsoft 对此的答复是,您应该将应用程序的客户端部分(下载的部分)保持尽可能精简,并仅使用应用程序安全来保护数据等。但这根本行不通,如果您:
- 您的客户端部署了软件,并且他们不希望未经授权的用户能够下载程序的客户端部分。
- 应用程序客户端部分包含宝贵的知识产权,您不想通过其他方式进行混淆,或者只是简单地将其暴露给互联网。
解决方案
我们真的只希望能够使用 表单身份验证 来阻止未经授权的用户下载应用程序,但我们该如何实现呢?
我们有一种在 IE 发出的初始请求和 ClickOnce 发出的后续请求之间传递信息的方法,那就是通过我们部署的清单。
在 * .xbap * 中,我们有
<dependentAssembly dependencyType="install"
codebase="SOME_PATH/Application.exe.manifest" size="819821">
这指定了应用程序清单的路径(其中又将包含有关如何部署其余文件的信息)。应用程序清单包含应用程序中各个文件的相对路径。(注意:Microsoft 文档说这些可以是绝对路径,而不是相对路径,但这不是真的。我们尝试过,因为它会使事情变得更容易,但不幸的是,文档似乎相互矛盾。)* .xbap * 中的此路径作为应用程序清单中所有相对路径的基准。
那么,我们如何利用这一点呢?ASP.NET 有一个神奇的小功能,称为无 cookie 窗体身份验证。它看起来像这样:
http://SOME_DOMAIN/(F(GARBAGE))/SOME_FILE
其中,* GARBAGE * 实际上是一个加密的窗体身份验证 cookie。ASP.NET 拥有此功能是因为某些设备不支持在 HTTP 会话中传输 cookie,或者不支持在本地存储这些 cookie 的方式。
窗体身份验证 cookie 也可以这样发送
http://SOME_DOMAIN/SOME_FILE?AuthCookieName=ENCRYPTED_COOKIE_DATA
但是,我们猜测有些设备甚至可能不支持保留查询字符串。因此,无 cookie 支持所做的是将身份验证票证转换为 URL 路径中的一个伪目录。这非常适合我们,因为我们实际上无法使用查询字符串,因为一旦我们这样做,ClickOnce 就会将其视为绝对路径并拒绝。
因此,如果我们能,比如说,在 * .xbap * 中放入类似这样的东西
<dependentAssembly dependencyType="install"
codebase="SOME_PATH/(F(GARBAGE))/Application.exe.manifest" size="23459">
其中 * (F(GARBAGE)) * 是一个有效构建的未过期窗体身份验证票据的无 cookie 身份验证令牌,那么 ASP.NET 应该会收到它执行应用程序清单请求和每个后续文件请求的窗体身份验证所需的所有信息。请记住,应用程序清单上的 codebase,* SOME_PATH/(F(GARBAGE)) *,将用作后续相对路径的 URL 基址。
所以,计划是编写一个 `IHttpHandler` 来处理 * .xbap * 的请求,而不是提供所请求的 * .xbap *,而是提取当前身份验证会话的信息,将其注入 * .xbap *,重新签名 * .xbap *,然后将其推送到客户端。太棒了!
当然,还有一个小问题。当 ClickOnce 第一次请求 * .xbap * 时,这个请求需要包含身份验证信息。幸运的是,ClickOnce 似乎使用与 IE 中输入的完全相同的 URL 来请求 * .xbap *,因此,只要我们确保 IE 包含带有身份验证票据的查询字符串,我们就万无一失了。ASP.NET 在这方面再次帮助了我们,因为无 cookie 窗体身份验证可以将 cookie 加密在查询字符串中,并且也具有这种无 cookie 的伪目录功能。
Using the Code
用于无 Cookie 身份验证的 HttpHandler
所以,我们要创建我们的 `IHttpHandler`
Public Class ClickOnceApplicationHandler Implements IHttpHandler
End Class
这就是处理程序的主要功能。这个处理程序基本上只用于 * .xbap *。它调用将修改和重新签名 * .xbap * 的方法,然后将其推送到客户端。
''' <summary>
''' Entry point for the handler, here we determine if we are dealing with the xbap,
''' and modify its values and resign.
''' </summary>
''' <param name="context"></param>
''' <remarks></remarks>
Public Sub ProcessRequest(ByVal context As System.Web.HttpContext) :
Implements System.Web.IHttpHandler.ProcessRequest
Dim _path As String = context.Server.MapPath(context.Request.Path).ToLower
If IO.File.Exists(_path) Then
If _path.EndsWith(".xbap") Then
'Set the correct mime type for xbap
context.Response.ContentType = "application/x-ms-xbap"
'Update the xbap with the cookieless authentication
'session information
Dim _file As String = GenerateXbap(context, _path)
'Write the updated file to the response
context.Response.WriteFile(_file)
context.Response.Flush()
'Clean up the temporary file that has been generated.
Directory.Delete(Path.GetDirectoryName(_file), True)
End If
Else
Throw New HttpException(404, "File not found")
End If
End Sub
`GenerateXbap` 基本上将创建现有 * .xbap * 的临时副本,然后修改路径,如上一节所述,并重新签名。真正的工作是在这里完成的
Public Shared Sub UpdateDeployManifestAppReference( _
ByVal depManifest As DeployManifest, ByVal pContext As HttpContext)
Dim deployManifestPath As String = _
Path.GetDirectoryName(depManifest.SourcePath)
'This should be the AssemblyReference for our main application manifest
Dim _assemRef As AssemblyReference = depManifest.EntryPoint
'Get the cookie value to use for the cookieless authentication.
Dim _val As String = pContext.Request.Cookies.Item( _
FormsAuthentication.FormsCookieName).Value
'Update the target path in the deployment manifest,
'this path will get reused for all the subsequent requests clickonce makes.
_assemRef.TargetPath = String.Format("{0}(X(1)F({1})){2}{3}", _
SiteRoot, _val, PathToManifest, AppManifestName)
End Sub
细心的观察者会注意到 * X(1) * 部分。我们现在将忽略这一点,只关注创建窗体身份验证票证伪目录。另请注意,我们正在获取 ASP.NET 由于用户登录到 * login.aspx * 而已为我们设置的窗体身份验证 cookie。
您可以深入研究随附的代码,了解清单重新签名是如何在这里完成的,因为它已在 其他地方 讨论过,并且它与我们在此讨论的内容无关。
现在,我们需要在 ASP.NET 中注册清单和部署文件,以便它们能够使用窗体身份验证。
在 IIS 7 中,我们这样做(注意:在此示例中我们使用经典管道)
<system.webServer>
...
<handlers>
<add name="Clickonce manifest file"
path="*.xbap" verb="*"
modules="IsapiModule"
scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll"
resourceType="Unspecified"
preCondition="classicMode,runtimeVersionv2.0,bitness32" />
<add name="Clickonce deployment files"
path="*.deploy" verb="*"
modules="IsapiModule"
scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll"
resourceType="Unspecified"
preCondition="classicMode,runtimeVersionv2.0,bitness32" />
<add name="Clickonce manifest files"
path="*.manifest" verb="*"
modules="IsapiModule"
scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll"
resourceType="Unspecified"
preCondition="classicMode,runtimeVersionv2.0,bitness32" />
...
在 IIS 6.0 中,我们通过 Internet 信息服务管理器来完成此操作。请参阅此处。
接下来,我们需要为 * .xbap * 附加我们的处理程序,并为常规部署文件附加 `StaticFileHandler`,因为我们希望它们正常下载。我们只将它们注册到 ASP.NET 中,以便窗体身份验证能够正常工作。
在 `system.web` 中
<httpHandlers>
<add verb="*" path="*.deploy" validate="false"
type="System.Web.StaticFileHandler" />
<add verb="*" path="*.manifest" validate="false"
type="System.Web.StaticFileHandler" />
<add verb="*" path="*.xbap" validate="false"
type="ClickOnceHandler.ClickOnceApplicationHandler,ClickOnceHandler" />
</httpHandlers>
此时,XBAP 和所有引用的文件都将配置为受 ASP.NET 保护。太棒了!当然,又出现了一个问题。
如果用户使用无 cookie 窗体身份验证请求 XBAP,他们将获得一个包含伪目录的唯一 XBAP URL。现在,尽管我们发送回的 XBAP 将具有相同的应用程序标识,但 ClickOnce 会将此唯一 URL 视为新安装的标识符。(注意:我们认为这可能已经改变了,因为几个月前我们看到了不同的行为,但事实就是如此。)如果您不介意用户每次访问部署站点时都必须安装您的应用程序,那这不是问题。这也应该适用于常规 ClickOnce 应用程序,因为部署提供程序每次都会不同。这就是我们嵌入在应用程序清单 codebase 中的 * X(1) * 信息发挥作用的地方。
X(1) 来救援
* X(1) * 基本上告诉 ASP.NET,如果它处于窗体身份验证 cookie 的 AutoDetect 模式,那么我们的“浏览器”不支持 cookie。所以,我们这里的诀窍是让 IE 使用常规 cookie 进行身份验证,然后通过用 * X(1) * 标记 codebase(以及所有后续文件请求)来切换到使用无 cookie 嵌入式目录进行 ClickOnce。狡猾!
我们启用这种双重模式的 * web.config * 看起来像这样
<authentication mode="Forms">
<forms loginUrl="login.aspx" name=".ADAuthCookie" timeout="20"
cookieless="AutoDetect" enableCrossAppRedirects="true"/>
</authentication>
<authorization>
<deny users="?" />
<allow users="*" />
</authorization>
注意 `enableCrossAppRedirects` 设置。这对于身份验证票证从 IE 以正常 cookie 形式传递到 ClickOnce 的无 cookie 查询字符串/嵌入式目录是必需的。
那么,我们是在哪里进行这个切换的呢?在我们登录 * login.aspx * 之后,我们被重定向到的 * .xbap * 的查询字符串中将包含窗体身份验证 cookie。
登录后,当我们重定向到 * Default.aspx * 时,我们点击
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Load
Dim _XbapFileName As String = _
System.Configuration.ConfigurationManager.AppSettings _
.Item("SiteRoot") _
& System.Configuration.ConfigurationManager.AppSettings.Item("XbapFileName")
Response.Redirect(String.Format("{0}?{1}={2}", _XbapFileName, _
FormsAuthentication.FormsCookieName, _
FormsAuthentication.Encrypt(CType(Context.User.Identity, _
FormsIdentity).Ticket)))
End Sub
因此,我们可以确保我们重定向到的页面将带有查询字符串,从而 ClickOnce 对 * .xbap * 的第一次请求将得到身份验证。应用程序清单的 codebase 具有嵌入的窗体身份验证票证目录,我们的 XBAP 已通过窗体身份验证完全保护。太棒了!
关注点
这里的关键思想是,我们可以模拟部署清单以在 IE 和 ClickOnce 引擎之间传递信息,并且 ASP.NET 无 cookie 会话允许我们支持一个“浏览器”上的经过身份验证的会话,该“浏览器”不具备 IE 的相同功能。我们接下来要探讨的是,如果客户端进一步希望在我们的 ClickOnce 部署应用程序前面放置一个进行身份验证的反向代理,我们有什么机制可以让 ClickOnce 向(比如说)ISA 服务器提供安全信息?
微软曾表示无法做到(或至少是不受支持的),但我们认为我们已经说服 ClickOnce(在出色的 ASP.NET 的帮助下)支持表单身份验证,从而将 ClickOnce 的价值提高了数百倍(至少对我们来说)。
谢谢
我们要感谢 ClickOnce 的其他一些勇敢的探险家,因为阅读他们的博客等,帮助我们弄清楚 ClickOnce 的足够多的运作方式,从而实现了这个解决方案