ClickOnce 许可证 HTTPHandler






4.63/5 (7投票s)
允许 ClickOnce 应用程序进行许可证验证的 HTTPHandler。
引言
我最近一直在开发一个打算公开销售的小型应用程序,并面临着如何为单个用户部署它的困境。我最初的方法是使用 MSI,但这似乎有点笨拙,尤其是在更新方面,并且需要我学习很多关于安装程序的知识,而这些知识我并不真正关心。ClickOnce 似乎是一个简单得多的替代方案,所以我最近重新发布了该应用程序,采用了 ClickOnce 部署。然而,ClickOnce 的一个缺点是,一旦你在互联网上发布了它,任何能够导航到该地址的人都可以获得它;似乎没有许可的概念。由于我正在销售该应用程序,这似乎是 ClickOnce 的一个相当重大的缺陷,我希望使某人付费获取我的应用程序变得至少有点挑战性。
经过大量的研究和一些开发,我创建了这个 HTTP 处理程序,它为 ClickOnce 应用程序提供了一定程度的许可,我想分享它,希望它能为某人部署应用程序节省一些时间。我应该指出,这段代码并不是打算直接编译并部署到您的生产环境中作为 ClickOnce 许可的解决方案。安全,尤其是基于 Web 的安全,不是我的强项,我提供的代码仅是基础实现。我为我的这个 HTTP 处理程序的部署做了一些修改,您可能也需要这样做。至少,您应该审查代码并理解其工作原理和局限性。
背景
我希望要求用户拥有有效的许可证密钥才能下载应用程序并执行更新,但我也希望允许用户在没有互联网连接或许可服务器宕机时运行该应用程序。一些研究,特别是 MSDN 上一篇关于管理 ClickOnce 部署的有用文章,建议最简单的方法是在 ClickOnce URL 上提供一个查询字符串,并通过 HTTP 处理程序拦截请求并提供验证。
这其中最大的问题是,ClickOnce 应用程序有一个预签名的清单,其中详细说明了应用程序是从哪里安装的以及从哪里检索更新。带有许可证密钥的查询字符串对于每个用户来说都是不同的,但清单不能轻易更新,因为那样会破坏签名。
“解决方案”,我在这里松散地使用这个词,因为我很快会解释,是使用 HTTP 处理程序在返回清单之前更新和重新签名清单。
这不是一个完整的解决方案的原因是它有一些缺点
- 您需要用于签名清单的密钥,并且该密钥必须位于 HTTP 处理程序可以访问的位置,这可能在某个共享的 Web 服务器上。这对我来说不是一个大问题,因为在这个阶段,我的应用程序已经自签名了,并且我已经采取了一些措施来尝试保护密钥。
- Visual Studio 为您的 ClickOnce 应用程序生成的启动程序setup.exe在安装完先决条件后将无法启动 ClickOnce 安装程序,因为 URL 不正确。在 HTTP 处理程序中动态生成启动程序的几次尝试失败后,我诉诸于使用 MSBuild(从命令行)生成一个通用的独立启动程序,该启动程序在完成后不会尝试启动 ClickOnce 安装程序。这意味着从启动程序到 ClickOnce 安装程序没有平滑的过渡,但目前我可以接受。
我此前已经创建了一个简单的许可服务器来生成和验证许可证密钥(当有人购买时,我的支付提供商会调用它)。我将其实现为一个 WCF REST(近似)服务,并且,在我阅读了Scott Hanselman 关于异步 IO 的文章之后,我决定将一个异步 GET
请求包装到一个小型辅助类中,然后发送到许可服务器。
我犹豫提供代码片段,因为我在这里并没有真正添加新东西,如果您阅读了链接的文章和相关的源代码,您基本上得到的就是这些,但组合在一起并包含一些许可证验证,以及一个可配置的“即时部署”类型的模块。但是,为了拓宽本文的受众并提供更多见解,我将逐步介绍基本过程
- 从查询字符串中提取许可证密钥
string licenceKey = context.Request.QueryString["licenceKey"]; if (string.IsNullOrWhiteSpace(licenceKey)) { logger.Fatal("Request: {0} - Empty licence and/or application key provided.", requestId); context.Response.Redirect(ConfigurationManager.AppSettings ["LicensingErrorRedirectUrl"]); }
- 通过异步请求到许可服务器来验证密钥。如果许可失败,则重定向到配置的页面。
ValidateLicenceKeyAsync(licenceServerAddress, licenceKey, applicationId) .ContinueWith(task => { if (string.IsNullOrWhiteSpace(task.Result)) { logger.Error("Request: {0} - No response returned from Licensing Server", requestId, licenceKey); result = false; } result = task.Result == CreateHexMd5Digest(applicationKey + bool.TrueString); }) .Wait();
在
ValidateLicenseKeyAsync
内部是一个普通的异步HttpWebRequest
,它被包装在一个 Task 中。validationRequest.BeginGetResponse(iar => { try { using (HttpWebResponse response = (HttpWebResponse)validationRequest.EndGetResponse(iar)) { using (StreamReader responseStreamReader = new StreamReader(response.GetResponseStream())) { string licenceResponse = responseStreamReader.ReadToEnd(); tcs.SetResult(licenceResponse); //set the result //of the Task } } } catch (Exception ex) { tcs.SetException(ex); } //Set the exception //in the Task }, null);
- 更新并重新签名应用程序清单。这几乎直接取自 Brian Noyes 在 MSDN 上的 ClickOnce 文章附带的源代码。但是,需要
X509KeyStorageFlags.MachineKeySet
来确保密钥从本地文件加载,而不是尝试从用户存储加载。//Generate the new ClickOnce deployment URL with licence key string providerUrl = string.Format("{0}?licenceKey={1}", context.Request.Url.GetLeftPart (UriPartial.Path), licenceKey) manifest.DeploymentUrl = providerUrl; //You'll need to give the AppPool user write permission //to the temp manifest directory ManifestWriter.WriteManifest(manifest, manifestPathOut); X509Certificate2 cert = new X509Certificate2 (certPath, certFilePassword, X509KeyStorageFlags.MachineKeySet); SecurityUtilities.SignFile(cert, null, manifestPathOut);
- 在响应中返回清单
context.Response.WriteFile(tempManifestPath); context.Response.ContentType = "application/x-ms-application";
Using the Code
我提供了 HTTP 处理程序的源代码,它应该可以按原样编译和运行。它包含一个非常简单的许可验证,仅作为示例,我建议您用更实质性的内容替换它。您可能还希望加密作为查询字符串传递的许可证密钥和应用程序 ID。
我还提供了一个示例 web.config 文件,其中包含 HTTP 处理程序使用的配置值。我有一个 MVC 3 网站用于我的应用程序,我也是在这里托管 ClickOnce 文件,所以 HTTP 处理程序的所有配置值都包含在网站的 web.config 文件中。
应用设置
ValidateLicence
-True
启用许可验证,false
关闭它。LicenceServerAddress
- 许可服务器的地址,包括应用程序 ID 和许可证密钥的查询字符串。这将用于string.Format()
来添加查询字符串参数值。ApplicationId
- 用于验证许可证的应用程序的唯一 ID,这假定许可服务器持有多个应用程序的许可证,并且在您的情况下可能不是必需的。ApplicationKey
- 我提供了一个非常基本的许可证密钥验证,它假定服务器返回一个 MD5 摘要,该摘要是此处配置的ApplicationKey
与指示许可证密钥是否有效的某个值的组合。因此,此密钥将与许可服务器共享。CertificatePath
- 用于签名清单的证书的路径。TempManifestDirectory
- 生成和保存许可证密钥特定清单的目录和文件名。{0}
将被替换为一个 GUID,该 GUID 应该对于每个请求都是唯一的。此目录中的清单可以删除,或者可以更新 HTTP 处理程序以重新使用它们来处理具有相同许可证密钥的请求。LicensingErrorRedirectUrl
- 如果在验证许可证时出现错误或验证失败,用户将被重定向到此 URL。
<appSettings>
<add key="ValidateLicence" value="true"/>
<add key="LicensingServerAddress"
value="HTTP://WWW.EXAMPLE.COM/VALIDATE?APPID={0}&LICENCE={1}" />
<add key="ApplicationId" value="APPLICATION_ID" />
<add key="ApplicationKey" value="PRIVATE_APPLICATION_KEY" />
<add key="CertificatePath" value="~/PRIVATE_CERTIFICATE.pfx"/>
<add key="TempManifestDirectory" value="~/TEMP/APPLICATION{0}.application"/>
<add key="LicensingErrorRedirectUrl" value="~/LICENCE_FAILED"/>
</appSettings>
以下是处理程序的注册,以及一个用于阻止服务 .pfx 文件以及隐藏存储 .pfx 文件所在目录的隐藏段的处理程序。
<system.webserver>
<handlers>
<add name="pfx-blocking" path="*.pfx" verb="*"
type="System.Web.HttpForbiddenHandler"
resourcetype="File" requireaccess="Script">
<add name="ClickOnceLicensing" path="*.application" verb="*"
type="ClickOnceLicensingModule.ClickOnceLicensing, ClickOnceLicensingModule">
</add></add></handlers>
<security>
<requestfiltering>
<hiddensegments>
<add segment="PRIVATE_KEY_DIRECTORY">
</add></hiddensegments>
</requestfiltering>
</security>
</system.webserver>
历史
- 2011 年 4 月 2 日:初次发布
- 2011 年 4 月 20 日:文章更新