ASP.NET 网站 + Windows 窗体应用程序 + WCF 服务:客户端凭据

Posted

技术标签:

【中文标题】ASP.NET 网站 + Windows 窗体应用程序 + WCF 服务:客户端凭据【英文标题】:ASP.NET Web Site + Windows Forms App + WCF Service: Client Credentials 【发布时间】:2010-09-25 14:46:59 【问题描述】:

假设我正在考虑设计一个 WCF 服务,其主要目的是提供可供三个不同应用程序使用的广泛服务:面向公众的网站、内部 Windows 窗体应用程序和无线移动设备。该服务的目的是双重的:(1) 将与业务流程相关的代码整合到一个中心位置;(2) 锁定对遗留数据库的访问,最终一劳永逸地将其隐藏在一套服务后面。

目前,这三个应用程序中的每一个都有自己的持久层和域层,对同一数据库的视图略有不同。不是所有三个应用程序都与数据库通信,而是与 WCF 服务通信,启用来自某些客户端的新功能(显然,移动选择器当前无法触发发送电子邮件的进程)和集中通知系统(而不是计划任务每​​五分钟轮询一次数据库以获取新订单,当这些客户端之一调用AcceptNewOrder() 服务方法时,只需 ping 开销分页系统)。总而言之,到目前为止,这听起来很合理。

然而,就整体设计而言,我对安全性感到困惑。 Windows 窗体应用程序当前仅使用 Windows 主体;员工存储在 Active Directory 中,在应用程序启动时,他们可以作为当前 Windows 用户登录(在这种情况下不需要密码),也可以提供他们的域名和密码。移动客户端没有用户的概念;它与数据库的连接是一个硬编码的字符串。该网站有成千上万的用户存储在遗留数据库中。那么如何实现身份模型并配置 WCF 端点来处理这个问题呢?

就 Windows 窗体应用程序而言,这不是什么大问题:WCF 代理可以启动一次,并且可以在内存中徘徊,所以我只需要一次客户端凭据(如果代理有过,可以再次提示它们故障)。移动客户端可以只是特殊情况,并使用 X509 证书对 WCF 服务进行身份验证。但是我该怎么处理这个网站呢?

就网站而言,允许匿名访问某些服务。对于在假设的“客户”角色中需要身份验证的服务,我显然不想在每个请求上都对它们进行身份验证,原因有两个:

我每次都需要他们的用户名和密码。将这对信息存储在几乎任何地方——会话、加密的 cookie、月亮——似乎是个坏主意。 对于每个请求,我都必须访问数据库中的用户表。哎哟。

我能想到的唯一解决方案是将网站视为受信任的子系统。 WCF 服务需要来自网站的特定 X509 证书。该网站在内部使用 Forms Authentication(它在返回布尔结果的服务上调用 AuthenticateCustomer() 方法),可以向凭据列表添加额外的声明,例如“joe@example.com 以顾客。”然后以某种方式可以在具有该声明的服务上构造自定义 IIdentity 对象和 IPrincipal,WCF 服务确信网站已正确验证了客户(它会知道声明没有被篡改,至少,因为它会提前知道网站的证书)。

所有这些都到位后,WCF 服务代码将能够说出诸如 [PrincipalPermission.Demand(Role=MyRoles.Customer)][PrincipalPermission.Demand(Role=MyRoles.Manager)] 之类的内容,而 Thread.CurrentPrincipal 将具有代表用户的内容(客户的电子邮件地址或员工的专有名称,它们都对日志记录和审计很有用)。

换句话说,每个服务将存在两个不同的端点:一个接受众所周知的客户端 X509 证书(用于移动设备和网站),另一个接受 Windows 用户(用于员工)。

对不起,这太长了。所以问题是:这些有意义吗?建议的解决方案有意义吗?我是不是把这弄得太复杂了?

【问题讨论】:

【参考方案1】:

好吧,既然我已经花了几个小时尝试各种方法,我想我会尝试自己的问题。

我的第一个方法是在 WCF 服务和面向公众的网站之间设置基于证书的身份验证(网站是服务的消费者/客户端)。使用makecert 生成的一些测试证书,将它们放入PersonalTrusted PeopleTrusted Root Certification Authorities(因为我懒得针对我们域的证书服务生成真实证书),一些配置文件修改,太好了,我们都准备好了。

为了防止网站必须为用户维护用户名和密码信息,其想法是一旦用户通过表单身份验证登录到网站,网站就可以只传递用户名(可通过HttpContext.Current.User.Identity.Name 访问) 作为可选的UserNameSecurityToken 以及实际用于保护消息的X509CertificateSecurityToken。如果找到可选的用户名安全令牌,那么 WCF 服务会说“嘿,这个受信任的子系统说这个用户已经过正确的身份验证,所以让我为那个用户设置一个 MyCustomPrincipal 并将它安装在当前线程上,这样实际的服务代码可以检查这一点。”如果不是,则将安装 MyCustomPrincipal 的匿名版本。

所以我花了五个小时尝试实现这一点,在variousblogs 的帮助下,我能够做到。 (我大部分时间都在调试一个问题,我的每个配置和支持类都正确,然后在我启动主机之后安装了我的自定义授权,而不是之前,所以我的努力​​实际上都没有生效。有些日子我讨厌电脑。)我有一个 TrustedSubsystemAuthorizationPolicy 进行 X509 证书验证,安装了一个匿名 MyCustomPrincipal,一个 TrustedSubsystemImpersonationAuthorizationPolicy 接受了一个带有空白密码的用户名令牌并安装了一个客户角色@ 987654342@ 如果它看到匿名受信子系统主体已安装,UserNameAuthorizationPolicy 则对未使用 X509 证书的其他端点进行基于用户名和密码的常规验证。它奏效了,而且很棒。

但是。

当我摆弄生成的客户端代理代码时,网站将使用该代码与该服务通信,这让我目瞪口呆的时刻到来了。在生成的ClientBase<T> 对象的ClientCredentials 属性上指定UserName 非常简单。但主要问题是凭据特定于 ChannelFactory,而不是特定的方法调用。

你看,新建一个 WCF 客户端代理是 more expensive 而不是 you might think。我自己编写了一个快速而肮脏的应用程序来测试性能:new() 建立一个新代理并调用一个方法十次大约需要 6 秒,而 new() 建立一个代理并仅调用该方法 10 次成本大约 3/5 秒。这只是令人沮丧的性能差异。

所以我可以为客户端代理实现一个池或缓存,对吗?好吧,不,它不容易解决:客户端凭据信息处于通道工厂级别,因为它可能用于保护传输,而不仅仅是消息,并且某些绑定在服务调用之间保持实际传输打开。由于客户端凭据对于代理对象是唯一的,这意味着我必须为当前在 Web 站点上的每个用户拥有一个唯一的缓存实例。这可能是很多代理对象位于内存中,并且非常接近我一开始试图避免的问题!而且由于无论如何我都必须触摸Endpoint 属性来设置可选支持用户名令牌的绑定,因此我无法利用Microsoft 在.NET 3.5 中“免费”添加的automatic channel factory caching。

回到绘图板:我的第二种方法,也是我认为我现在将最终使用的方法,是坚持客户端网站和 WCF 服务之间的 X509 证书安全性。我将在我的消息中发送一个自定义的“UserName”SOAP 标头,WCF 服务可以检查该 SOAP 标头,确定它是否来自受信任的子系统(例如网站),如果是,则在其中安装 MyCustomPrincipal方式和以前类似。

Codeproject 和 random people on Google 是很棒的东西,因为它们帮助我快速启动并运行它,即使在 running into a weird WCF bug when it comes to custom endpoint behaviors in configuration 之后也是如此。通过在客户端和服务端实现消息检查器——一个用于添加 UserName 标头,一个用于读取它并安装正确的主体——此代码位于一个我可以简单地忘记它的地方。由于我不必触摸Endpoint 属性,因此我免费获得了内置通道工厂缓存。由于ClientCredentials 对于访问网站的任何用户都是相同的(实际上,它们始终是 X509 证书——只有消息本身中的 UserName 标头的值会发生变化),因此添加客户端代理缓存或代理池是更琐碎。

所以这就是我最终做的。 WCF 服务中的实际服务代码可以做类似的事情


    // Scenario 1: X509Cert + custom UserName header yields for a Web site customer ...
    Console.WriteLine("0", Thread.CurrentPrincipal.Identity.Name); // prints out, say, "joe@example.com"
    Console.WriteLine("0", Thread.CurrentPrincipal.IsInRole(MyRoles.Customer)); // prints out "True"

    // Scenario 2: My custom UserNameSecurityToken authentication yields for an employee ...
    Console.WriteLine("0", Thread.CurrentPrincipal.Identity.Name); // prints out, say, CN=Nick,DC=example, DC=com
    Console.WriteLine("0", Thread.CurrentPrincipal.IsInRole(MyRoles.Employee)); // prints out "True"

    // Scenario 3: Web site doesn't pass in a UserName header ...
    Console.WriteLine("0", Thread.CurrentPrincipal.Identity.Name); // prints out nothing
    Console.WriteLine("0", Thread.CurrentPrincipal.IsInRole(MyRoles.Guest)); // prints out "True"
    Console.WriteLine("0", Thread.CurrentPrincipal.IsInRole(MyRoles.Customer)); // prints out "False"

无论这些人是如何通过身份验证的,或者有些人使用 SQL Server 或有些人使用 Active Directory:PrincipalPermission.Demand 并且用于审计目的的日志记录现在变得轻而易举。

我希望这对将来的一些可怜的人有所帮助。

【讨论】:

很高兴您对自己进行了整理,我的问题略有不同,但我花了大约 5 个小时来研究 WS/WCF-Security。叹息。 如果你觉得你可以阐明我的特殊问题(WCF 安全性和长帖子是什么?哈哈)请做 - ***.com/questions/395892/… Graham 我经历了很多痛苦,但基本上都结束了,感谢您的帖子... @Nicholas Piasecki - 我当然希望你在两个地方都有该代码 - 其中一个就在这里。 “通过在客户端和服务端实现消息检查器——一个添加 UserName 标头,一个读取它并安装正确的主体——这段代码在一个地方,我可以简单地忘记它”:- )【参考方案2】:

对于匿名公共访问,使用 basichttpbinding 并且在 web.config 文件中也有以下内容

【讨论】:

以上是关于ASP.NET 网站 + Windows 窗体应用程序 + WCF 服务:客户端凭据的主要内容,如果未能解决你的问题,请参考以下文章

在 Windows 窗体应用程序中托管 ASP.NET Core API

从 DoubleClick、Web 应用程序而非 Windows 窗体上的 ListBox 获取选定值

异步和等待 - 控制台、Windows 窗体和 ASP.NET 之间的区别

是否有适用于 Windows 窗体的 Snoop (WPF) 或 FireBug (ASP.NET) 之类的东西? [关闭]

ASP.NET Windows 和窗体身份验证

ASP.NET窗体对话框的实现