假设我正在考虑设计一个WCF服务,其主要目的是提供可供三个完全不同的应用程序使用的广泛服务:面向公众的Web站点、内部Windows窗体应用程序和无线移动设备。该服务的目的有两个:(1)将与业务流程相关的代码合并到一个中心位置;(2)锁定对遗留数据库的访问,最终一劳永逸地将其隐藏在一套服务之后。
目前,这三个应用程序中的每一个都有自己的持久层和域层,它们对同一数据库的视图略有不同。它们不是所有三个应用程序都与数据库对话,而是与WCF服务对话,启用来自某些客户端的新功能(显然,移动选择器当前不能触发进程发送电子邮件),并集中通知系统(而不是每五分钟轮询数据库以获取新订单的计划任务,只需在其中一个客户端调用AcceptNewOrder()
服务方法时ping开销分页系统)。总而言之,到目前为止,这听起来相当合理。
然而,在总体设计方面,当谈到安全性时,我被难住了。Windows窗体应用程序当前只使用Windows主体;员工存储在Active Directory中,在应用程序启动时,他们可以作为当前Windows用户登录(在这种情况下不需要密码),也可以提供他们的域名和密码。移动客户端没有任何用户的概念;它与数据库的连接是硬编码的字符串。并且该网站有数千名用户存储在遗留数据库中。那么,如何实现身份模型并配置WCF端点来处理此问题呢?
就Windows窗体应用程序而言,这不是什么大问题: WCF代理可以启动一次,并且可以在内存中挂起,因此我只需要客户端凭据一次(如果代理出现故障,可以再次提示输入它们)。移动客户端可以是特殊情况,并使用X509证书进行身份验证。但是我该怎么处理这个网站呢?
在网站的例子中,允许匿名访问某些服务。对于需要在假设的“客户”角色中进行身份验证的服务,我显然不希望在每次请求时都对它们进行身份验证,原因有两个:
我能想到的唯一解决方案是将Web站点视为一个受信任的子系统。WCF服务需要来自网站的特定X509证书。该网站在内部使用Forms身份验证(在返回布尔结果的服务上调用AuthenticateCustomer()
方法),可以向凭据列表中添加其他声明,例如"joe@example.com作为客户登录“。然后,可以通过某种方式在具有该声明的服务上构造一个自定义IIdentity对象和IPrincipal,该服务确信网站已经正确地对客户进行了身份验证(它将知道该声明没有被篡改,至少因为它将提前知道该网站的证书)。
所有这些都准备就绪后,WCF服务代码将能够表示[PrincipalPermission.Demand(Role=MyRoles.Customer)]
或[PrincipalPermission.Demand(Role=MyRoles.Manager)]
之类的内容,并且Thread.CurrentPrincipal
将具有表示用户的内容(客户的电子邮件地址或员工的可分辨名称,这两个名称对于日志记录和审计都很有用)。
换句话说,每个服务将存在两个不同的端点:一个接受众所周知的客户端X509证书(用于移动设备和网站),另一个接受Windows用户(用于员工)。
抱歉,这太长了。所以问题是:这一切有意义吗?建议的解决方案有意义吗?我是不是把事情搞得太复杂了?
发布于 2008-12-09 01:52:43
好吧,既然我已经花了几个小时尝试各种方法,我想我将尝试一下我自己的问题。
我的第一种方法是在WCF服务和面向公众的Web站点( Web站点是服务的使用者/客户端)之间设置基于证书的身份验证。使用makecert
生成一些测试证书,将它们放到Personal
、Trusted People
和Trusted Root Certification Authorities
中(因为我不想费心针对我们域的证书服务生成真正的证书),一些配置文件修改,很好,我们都准备好了。
为了避免网站必须维护用户的用户名和密码信息,我们的想法是,一旦用户通过表单身份验证登录到网站,除了实际用于保护消息安全的UserNameSecurityToken
之外,网站还可以将用户名(可通过HttpContext.Current.User.Identity.Name
访问)作为可选的X509CertificateSecurityToken
进行传递。如果找到了可选的用户名安全令牌,那么WCF服务会说:“嘿,这个受信任的子系统说这个用户经过了正确的身份验证,所以让我为该用户设置一个MyCustomPrincipal
,并将其安装在当前线程上,以便实际的服务代码可以检查它。”如果不是,则会安装一个匿名版本的MyCustomPrincipal
。
所以我花了五个小时来实现这一点,在various blogs的帮助下,我做到了。(我花了大部分时间调试这样一个问题:我让每个配置和支持类都是正确的,然后在启动主机之后安装我的自定义授权,而不是在启动之前,所以我的努力都没有真正生效。有时我讨厌电脑。)我有一个TrustedSubsystemAuthorizationPolicy
,它执行X509证书验证,安装一个匿名MyCustomPrincipal
,一个TrustedSubsystemImpersonationAuthorizationPolicy
,如果它看到匿名的受信任子系统主体已经安装,它接受一个带有空密码的用户名令牌并安装一个客户角色MyCustomPrincipal
,还有一个UserNameAuthorizationPolicy
,它对其他不使用X509证书的端点执行基于用户名和密码的常规验证。它起作用了,而且很棒。
但。
当我摆弄Web站点将用来与此服务对话的生成的客户端代理代码时,出现了刺伤自己眼睛的时刻。在生成的ClientBase<T>
对象的ClientCredentials
属性上指定UserName
非常简单。但主要问题是凭据是特定于ChannelFactory
的,而不是特定的方法调用。
你看,more expensive () up a WCF client proxy比you might think更简单。我自己编写了一个快捷的应用程序来测试性能: new ()设置一个新的代理并调用一个方法10次大约需要6秒,而new()设置一次代理并只调用该方法10次大约需要1/5秒。这只是一个令人沮丧的性能差异。
因此,我可以只为客户端代理实现一个池或缓存,对吗?不,这并不容易解决:客户端凭据信息处于通道工厂级别,因为它可能用于保护传输,而不仅仅是消息,而且一些绑定会在服务调用之间保持实际传输的打开。由于客户端凭据对于代理对象是唯一的,这意味着我必须为当前Web站点上的每个用户提供一个唯一的缓存实例。这可能是大量的代理对象驻留在内存中,并且与我一开始试图避免的问题非常接近( @#$@# )!而且由于我无论如何都要接触Endpoint
属性来为可选的支持用户名令牌设置绑定,所以我不能利用微软在.NET 3.5中“免费”添加的automatic channel factory caching。
回到起点:我的第二种方法,也是我认为现在将最终使用的一种方法,是坚持使用客户端网站和X509服务之间的WCF证书安全性。我只需在我的消息中发送一个自定义的"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
和用于审计目的的日志记录现在变得轻而易举。
我希望这能在未来帮助一些可怜的灵魂。
发布于 2009-04-02 18:14:39
对于匿名公共访问,使用基本的basichttpbinding,并在web.config文件中包含以下内容
https://stackoverflow.com/questions/345414
复制相似问题