且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

ASP.NET网站+ Windows Forms App + WCF服务:客户端凭据

更新时间:2022-10-25 08:23:33

嗯,我想我现在就用自己的问题我花了几个小时玩各种方法。



我的第一种方法是在WCF服务和面向公众的网站之间建立基于证书的身份验证(该网站是该服务的消费者/客户端)。使用 makecert 生成的一些测试证书,将它们转入个人 Trusted People 受信任的根证书颁发机构(因为我无法为我们的域的证书服务生成真实的证书),一些配置文件修改,以及很好,我们都设置了。



为了防止网站不必为用户维护用户名和密码信息,这个想法是一旦用户登录到Web网站通过表单身份验证,网站可以传递用户名(可通过 HttpContext.Current.User.Identity.Name 访问)作为可选的 UserNameSecurityToken 除了实际用于保护邮件的 X509CertificateSecurityToken 之外。如果找到可选的用户名安全令牌,那么WCF服务会说嘿,这个信任的子系统说这个用户被正确认证,所以让我设置一个 MyCustomPrincipal 该用户将其安装在当前线程上,以便实际的服务代码可以检查这个。如果没有,那么将安装 MyCustomPrincipal 的匿名版本。



所以我去了五在各种 博客,我能够做到。 (我花费了大部分时间来调试一个问题,在那里我有一个配置和支持类正确,然后安装我的自定义授权后,我开始主持人,而不是以前,所以我的努力​​实际上没有有一天我讨厌电脑。)我有一个 TrustedSubsystemAuthorizationPolicy 做了X509证书验证,安装一个匿名的 MyCustomPrincipal ,一个 TrustedSubsystemImpersonationAuthorizationPolicy ,它接受具有空白密码的用户名令牌,并安装了客户角色 MyCustomPrincipal ,如果它看到已经安装了匿名受信任的子系统主体,还有一个 UserNameAuthorizationPolicy ,对未使用X509证书的其他端点进行了常规的用户名和密码验证。它的工作,这是美好的。



但是。



刺激自己的眼球当我正在使用网站将用于与此服务通话的生成的客户端代理代码时,就会出现这个时刻。在生成的 ClientBase< T> $ c的 ClientCredentials 属性中指定 UserName $ c>对象很简单。但是主要的问题是凭证特定于 ChannelFactory ,而不是特定的方法调用。



new()创建一个WCF客户端代理是更昂贵你可能认为。我写了一个快速而肮脏的应用程序来测试自己的性能:new()创建一个新的代理和调用一个方法十次花费了大约6秒,而new()启动代理一次,只调用方法10次成本约1/5秒钟。这只是令人沮丧的性能差异。



所以我可以为客户端代理实现一个池或缓存,对吧?好的,不,它不容易解决:客户端凭据信息在通道工厂级别,因为它可能用于保护传输,而不仅仅是消息,一些绑定在服务调用之间保持实际的传输打开。由于客户端凭据对代理对象是唯一的,这意味着我必须为网站上的每个用户拥有唯一的缓存实例。这可能是很多代理对象坐在内存中,而且@#$ @#接近于我首先想避免的问题!由于我必须触摸 Endpoint 属性来设置可选支持用户名令牌的绑定,所以我无法利用自动频道工厂缓存微软在.NET 3.5中添加了免费。



回到绘图板:我的第二个方法,我认为我'现在最终使用,是坚持客户端网站和WCF服务之间的X509证书安全性。我会在我的邮件中发送一个自定义的UserNameSOAP头,WCF服务可以检查SOAP头,确定它是否来自受信任的子系统(如网站),如果是这样,请安装 MyCustomPrincipal 以与之前类似的方式。



Codeproject Google上的随机人员是很棒的事情,因为他们帮助我快速运行,即使在当涉及到配置中的自定义端点行为时,出现了一个奇怪的WCF错误。通过在客户端和服务端实现消息检查员 - 一个添加UserName头,一个用于读取并安装正确的主体 - 这个代码在一个我可以简单忘记的地方。由于我不必触摸 Endpoint 属性,所以我可以免费获得内置的频道工厂缓存。而且由于访问该网站的任何用户的 ClientCredentials 是一样的(实际上,它们始终是X509证书 - 只有消息本身中的UserName头的值更改),添加客户端代理缓存或代理池是非常微不足道的。



所以这就是我最后做的。 WCF服务中的实际服务代码可以执行诸如

  
//方案1:X509Cert +自定义UserName标题对于网站客户...
Console.WriteLine({0},Thread.CurrentPrincipal.Identity.Name); //打印出来,例如joe@example.com
Console.WriteLine({0},Thread.CurrentPrincipal.IsInRole(MyRoles.Customer)); //打印出True

//方案2:我的自定义UserNameSecurityToken身份验证产生了一个员工...
Console.WriteLine({0},Thread.CurrentPrincipal.Identity 。名称); //打印出来,比如CN = Nick,DC = example,DC = com
Console.WriteLine({0},Thread.CurrentPrincipal.IsInRole(MyRoles.Employee)); //打印出True

//方案3:网站不会传入UserName头...
Console.WriteLine({0},Thread.CurrentPrincipal .Identity.Name); //打印没有
Console.WriteLine({0},Thread.CurrentPrincipal.IsInRole(MyRoles.Guest)); //打印出True
Console.WriteLine({0},Thread.CurrentPrincipal.IsInRole(MyRoles.Customer)); //打印出False

这些人如何认证无关紧要,居住在SQL Server中,有些则生活在Active Directory中: PrincipalPermission.Demand 和用于审计的日志记录已经很快了。



我希望这将有助于一些可怜的灵魂。


Let's say that I'm considering designing a WCF service whose primary purpose is to provide broad services that can be used by three disparate applications: a public-facing Web site, an internal Windows Forms application, and a wireless mobile device. The purpose of the service is twofold: (1) to consolidate code related to business processes in a central location and (2) to lock down access to the legacy database, finally and once and for all hiding it behind one suite of services.

Currently, each of the three applications has its own persistence and domain layers with slightly different views of the same database. Instead of all three applications talking to the database, they would talk to the WCF service, enabling new features from some clients (the mobile picker can't currently trigger processes to send e-mail, obviously) and centralizing notification systems (instead of a scheduled task polling the database every five minutes for new orders, just ping the overhead paging system when the AcceptNewOrder() service method is invoked by one of these clients). All in all, this sounds pretty sane so far.

In terms of overall design, however, I'm stumped when it comes to security. The Windows Forms application currently just uses Windows principals; employees are stored in Active Directory, and upon application startup, they can login as the current Windows user (in which case no password is required) or they can supply their domain name and password. The mobile client doesn't have any concept of a user; its connection to the database is a hardcoded string. And the Web site has thousands of users stored in the legacy database. So how do I implement the identity model and configure the WCF endpoints to deal with this?

In terms of the Windows Forms application, this is no great issue: the WCF proxy can be initiated once and can hang around in memory, so I only need the client credentials once (and can prompt for them again if the proxy ever faults). The mobile client can just be special cased and use an X509 certificate for authentication against the WCF service. But what do I do about the Web site?

In the Web site's case, anonymous access to some services is allowed. And for the services that require authentication in the hypothetical "Customer" role, I obviously don't want to have to authenticate them on each and every request for two reasons:

  • I need their username and password each time. Storing this pair of information pretty much anywhere--the session, an encrypted cookie, the moon--seems like a bad idea.
  • I would have to hit the users table in the database for each request. Ouch.

The only solution that I can come up with is to treat the Web site as a trusted subsystem. The WCF service expects a particular X509 certificate from the Web site. The Web site, using Forms Authentication internally (which invokes an AuthenticateCustomer() method on the service that returns a boolean result), can add an additional claim to the list of credentials, something like "joe@example.com is logged in as a customer." Then somehow a custom IIdentity object and IPrincipal could be constructed on the service with that claim, the WCF service being confident that the Web site has properly authenticated the customer (it will know that the claim hasn't been tampered with, at least, because it'll know the Web site's certificate ahead of time).

With all of that in place, the WCF service code would be able to say things like [PrincipalPermission.Demand(Role=MyRoles.Customer)] or [PrincipalPermission.Demand(Role=MyRoles.Manager)], and the Thread.CurrentPrincipal would have something that represented a user (an e-mail address for a customer or a distinguished name for an employee, both of them useful for logging and auditing).

In other words, two different endpoints would exist for each service: one that accepted well-known client X509 certificates (for the mobile devices and the Web site), and one that accept Windows users (for the employees).

Sorry this is so long. So the question is: Does any of this make sense? Does the proposed solution make sense? And am I making this too complicated?

Well, I reckon that I'll take a stab at my own question now that I've spent a few hours playing with various approaches.

My first approach was to set up certificate-based authentication between the WCF service and the public-facing Web site (the Web site is a consumer/client of the service). A few test certs generated with makecert, plop them into the Personal, Trusted People, and Trusted Root Certification Authorities (because I couldn't be bothered to generate real ones against our domain's certificate services), some config file modifications, and great, we're all set.

To prevent the Web site from having to maintain username and password information for users, the idea is that once a user is logged into the Web site via Forms Authentication, the Web site can pass just the username (accessible via HttpContext.Current.User.Identity.Name) as an optional UserNameSecurityToken in addition to the X509CertificateSecurityToken that is actually used to secure the message. If the optional username security token is found, then the WCF service would say "Hey, this trusted subsystem says that this user is properly authenticated, so let me set up a MyCustomPrincipal for that user and install it on the current thread so that actual service code can inspect this." If it wasn't, then an anonymous version of MyCustomPrincipal would be installed.

So off I went for five hours trying to implement this, and with the help of various blogs, I was able to do it. (I spent most of my time debugging a problem where I had every single configuration and supporting class correct, and then installed my custom authorizations after I started the host, not before, so none of my effort was actually taking effect. Some days I hate computers.) I had a TrustedSubsystemAuthorizationPolicy that did the X509 certificate validation, installing an anonymous MyCustomPrincipal, a TrustedSubsystemImpersonationAuthorizationPolicy that accepted a username token with a blank password and installed a customer-role MyCustomPrincipal if it saw that the anonymous trusted subsystem principal was already installed, and a UserNameAuthorizationPolicy which did regular username and password based validation for the other endpoints where X509 certificates aren't being used. It worked, and it was wonderful.

But.

The stab-myself-in-the-eyeballs moment came when I was fiddling with the generated client proxy code that the Web site would use to talk to this service. Specifying the UserName on the ClientCredentials property of the generated ClientBase<T> object was easy enough. But the main problem is that credentials are specific to a ChannelFactory, not a particular method invocation.

You see, new()ing up a WCF client proxy is more expensive than you might think. I wrote a quick-and-dirty app to test performance myself: both new()ing up a new proxy and calling a method ten times took about 6 seconds whereas new()ing up a proxy once and calling only the method 10 times cost about 3/5ths of one second. That is just a depressing performance difference.

So I can just implement a pool or a cache for the client proxy, right? Well, no, it's not easily worked around: the client credentials information is at the channel factory level because it might be used to secure the transport, not just the message, and some bindings keep an actual transport open between service calls. Since client credentials are unique to the proxy object, this means that I would have to have a unique cached instance for each user currently on the Web site. That's potentially a lot of proxy objects sitting in memory, and pretty @#$@# close to the problem that I was trying to avoid in the first place! And since I have to touch the Endpoint property anyway to set up the binding for the optional supporting username token, I can't take advantage of the automatic channel factory caching that Microsoft added "for free" in .NET 3.5.

Back to the drawing board: my second approach, and the one that I think that I'll end up using for now, is to stick with the X509 certificate security between the client Web site and the WCF service. I'll just send a custom "UserName" SOAP header in my messages, and the WCF service can inspect that SOAP header, determine if it came from a trusted subsystem such as the Web site, and if so, install a MyCustomPrincipal in a similar manner as before.

Codeproject and random people on Google are wonderful things to have because they helped me get this up and running quickly, even after running into a weird WCF bug when it comes to custom endpoint behaviors in configuration. By implementing message inspectors on the client side and the service side--one to add the UserName header, and one to read it and install the correct principal--this code is in one place where I can simply forget about it. Since I don't have to touch the Endpoint property, I get the built-in channel factory caching for free. And since the ClientCredentials are the same for any user accessing the Web site (indeed, they are always the X509 certificate -- only the value of the UserName header within the message itself changes), adding client proxy caching or a proxy pool is much more trivial.

So that's what I ended up doing. Actual service code in the WCF service can do things like


    // 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"

It doesn't matter how these people got authenticated, or that some are living in SQL server or that some are living in Active Directory: PrincipalPermission.Demand and logging for auditing purposes is now a snap.

I hope this helps some poor soul in the future.