WCF的安全审核——记录谁在敲打你的门
WCF所谓的安全审核就是针对认证和授权所做的针对EventLog的日志记录。我们不但可以设置进行审核的事件(认证成功/失败,授权成功或失败),还可以选择记录信息被写入的EventLog类型,即应用程序日志(Application)还是安全日志(Security)。WCF的安全审核是通过ServiceSecurityAuditBehavior服务行为实现的。
一、ServiceSecurityAuditBehavior服务行为
针对WCF安全审核的编程只涉及ServiceSecurityAuditBehavior服务行为。下面给出了定义在ServiceSecurityAuditBehavior中具体审核行为进行控制的三个可读写的属性。
1: public sealed class ServiceSecurityAuditBehavior : IServiceBehavior
2: {
3: //其他成员
4: public AuditLogLocation AuditLogLocation { get; set; }
5: public AuditLevel MessageAuthenticationAuditLevel { get; set; }
6: public AuditLevel ServiceAuthorizationAuditLevel { get; set; }
7: public bool SuppressAuditFailure { get; set; }
8: }
属性AuditLogLocation代表的是日志信息被写入的EventLog类型,该属性的类型是一个具有如下定义的AuditLogLocation枚举。其中Application和Security分别代表应用程序日志和安全日志。如果选择Default,则最终日志被写入的位置决定于当前的操作系统。如果支持写入安全日志,则选择安全日志类型,否则选择应用程序日志类型。Default是默认选项。
1: public enum AuditLogLocation
2: {
3: Default,
4: Application,
5: Security
6: }
MessageAuthenticationAuditLevel和ServiceAuthorizationAuditLevel两个属性分别代表针对认证和授权审核的级别。所谓审核的级别在这里指的应该在审核事件(认证和授权)在成功或者失败的情况下进行日志记录。审核级别通过具有如下定义的AuditLevel枚举表示。Success和Failure代表分别针对认证/授权成功和失败进行审核日志。SuccessOrFailure则意味着不管认证/授权是成功还是失败,都会进行审核日志。None为默认值,表示不进行审核日记记录。
1: public enum AuditLevel
2: {
3: None,
4: Success,
5: Failure,
6: SuccessOrFailure
7: }
布尔类型的SuppressAuditFailure属性表示审核日志失败是否会影响应用本身。在默认的情况下该属性值为True,意味着为认证和授权进行审核日志的时候出现的异常不会对应用(服务)本身造成任何影响。
既然是服务行为,我们就可以通过将创建的ServiceSecurityAuditBehavior添加到服务的行为列表的方式来进行安全审核的控制。当然我们还是推荐采用配置的方式来进行安全什么的相关设置。服务行为ServiceSecurityAuditBehavior对应的配置节是<serviceSecurityAudit>。在下面的配置中,我定义了一个包含了ServiceSecurityAuditBehavior的服务行为,并对其四个属性进行了显式设置。
1: <configuration>
2: <system.serviceModel>
3: <behaviors>
4: <serviceBehaviors>
5: <behavior ...>
6: <serviceSecurityAudit auditLogLocation ="Application"
7: messageAuthenticationAuditLevel ="Failure"
8: serviceAuthorizationAuditLevel="SuccessOrFailure"
9: suppressAuditFailure="true"/>
10: </behavior>
11: </serviceBehaviors>
12: </behaviors>
13: </system.serviceModel>
14: </configuration>
二、安全审核的实现
WCF最终进行安全审核的控制信息是从基于某个终结点的分发上下文中获取的。如下面的代码片段所示,对于定义在ServiceSecurityAuditBehavior中的四个属性,在DispatchRuntime中具有相应的定义。WCF在认证和授权成功或者失败的时候,会根据该运行时这四个属性进行相应安全审核日志。
1: public sealed class DispatchRuntime
2: {
3: //其他成员
4: public AuditLogLocation SecurityAuditLogLocation { get; set; }
5: public AuditLevel MessageAuthenticationAuditLevel { get; set; }
6: public AuditLevel ServiceAuthorizationAuditLevel { get; set; }
7: public bool SuppressAuditFailure { get; set; }
8: }
而作为服务行为的ServiceSecurityAuditBehavior,最终的目的就是将定义在自身的这四个属性赋值给分发上下文。而具体操作定义在ApplyDispatchBehavior方法上,整个逻辑大体上如下面的代码所示。
1: public sealed class ServiceSecurityAuditBehavior : IServiceBehavior
2: {
3: //其他成员
4: public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
5: ServiceHostBase serviceHostBase)
6: {
7: foreach (ChannelDispatcher channelDispatcher in
8: serviceHostBase.ChannelDispatchers)
9: {
10: foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
11: {
12: DispatchRuntime dispatchRuntime = endpointDispatcher.DispatchRuntime;
13: dispatchRuntime.SecurityAuditLogLocation = this.AuditLogLocation;
14: dispatchRuntime.MessageAuthenticationAuditLevel = this.MessageAuthenticationAuditLevel;
15: dispatchRuntime.ServiceAuthorizationAuditLevel = this.ServiceAuthorizationAuditLevel;
16: dispatchRuntime.SuppressAuditFailure = this.SuppressAuditFailure;
17: }
18: }
19: }
20: }
三、实例演示:如何实施安全审核
接下来我们通过一个简单的实例来演示如何进行针对认证和授权的安全审核,并且看看在认证或者授权成功或者失败的情况下,会有怎样的日志信息被记录下来。
基于认证的安全审核
先来演示针对认证的安全审核。我们还是直接使用一直在使用的计算服务的例子,服务契约和服务的定义我们已经很熟悉了,我们现在只来介绍采用的服务端的配置。从下面的配置可以看出,被寄宿的服务具有为一个基于WS2007HttpBinding的终结点,该绑定采用默认的Windows认证。通过服务行为,我们将安全审核的AuditLogLocation和MessageAuthenticationAuditLevel分别设置为Application和SuccessOrFailure。
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service name="Artech.WcfServices.Service.CalculatorService"
5: behaviorConfiguration="authenticationAudit">
6: <endpoint address="http://127.0.0.1:3721/calculatorservice"
7: binding="ws2007HttpBinding"
8: contract="Artech.WcfServices.Service.Interface.ICalculator"/>
9: </service>
10: </services>
11: <behaviors>
12: <serviceBehaviors>
13: <behavior name="authenticationAudit">
14: <serviceSecurityAudit auditLogLocation ="Application"
15: messageAuthenticationAuditLevel ="SuccessOrFailure"/>
16: </behavior>
17: </serviceBehaviors>
18: </behaviors>
19: </system.serviceModel>
20: </configuration>
客户端利用如下的代码进行服务的调用,两次服务调分别采用Foo和Bar两个本地Windows帐号进行,其中基于帐号Foo给定的密码是正确的,而基于帐号Bar给定的密码是错误的。而辅助方法Invoke旨在避免避免认证失败导致的异常是程序终止。
1: ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorService");
2: NetworkCredential credential = channelFactory.Credentials.Windows.ClientCredential;
3: credential.UserName = "Foo";
4: credential.Password = "Password";
5: ICalculator calculator = channelFactory.CreateChannel();
6: Invoke(calculator);
7:
8: channelFactory = new ChannelFactory<ICalculator>("calculatorService");
9: credential = channelFactory.Credentials.Windows.ClientCredential;
10: credential.UserName = "Bar";
11: credential.Password = "InvalidPass";
12: calculator = channelFactory.CreateChannel();
13: Invoke(calculator);
由于我们通过配置将应用在服务上的ServiceSecurityAuditBehavior服务行为的AuditLogLocation和MessageAuthenticationAuditLevel分别设置为Application和SuccessOrFailure,意味着不论是认证成功或者失败都会进行安全审核日志。而审核日志最终会被写入EventLog的应用程序日志。当程序执行后,在事件查看器的应用程序节点,你会发现具有如下图所示的4条新的日志(之前的日志在程序运行前被清空)。
下面列出了这4条日志的内容。其中前3条基于认证成功的信息(Information)日志,最后一条是基于认证失败的错误(Error)日志。[源代码从这里下载]
1: Security negotiation succeeded.
2: Service: http://127.0.0.1:3721/calculatorservice
3: Action: http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTR/Issue
4: ClientIdentity: Jinnan-PC\Foo; S-1-5-21-3534336654-2901585401-846244909-1006
5: ActivityId: <null>
6: Negotiation: SpnegoTokenAuthenticator
7:
8: Message authentication succeeded.
9: Service: http://127.0.0.1:3721/calculatorservice
10: Action: http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/SCT
11: ClientIdentity: Jinnan-PC\Foo; S-1-5-21-3534336654-2901585401-846244909-1006
12: ActivityId: <null>
13:
14: Message authentication succeeded.
15: Service: http://127.0.0.1:3721/calculatorservice
16: Action: http://www.artech.com/ICalculator/Add
17: ClientIdentity: Jinnan-PC\Foo; S-1-5-21-3534336654-2901585401-846244909-1006
18: ActivityId: <null>
19:
20: Security negotiation failed.
21: Service: http://127.0.0.1:3721/calculatorservice
22: Action: http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTR/Issue
23: ClientIdentity:
24: ActivityId: <null>
25: Negotiation: SpnegoTokenAuthenticator
26: Win32Exception: The Security Support Provider Interface (SSPI) negotiation failed.
基于授权的安全审核
接下来我们演示授权的安全审核,并查看分别在授权成功和失败的情况下分别由怎样的日志被写入到EventLog中。我们首先按照如下的方式在服务类型CalculatorService的Add方法上应用PrincipalPermissionAttribute特性使该方法只有在管理员才有权限调用。
1: public class CalculatorService : ICalculator
2: {
3: [PrincipalPermission(SecurityAction.Demand,Role="Administrators")]
4: public double Add(double x, double y)
5: {
6: return x + y;
7: }
8: }
然后我们将服务端配置中关于安全审核的相关配置进行了如下的修改。我们只关心授权相关的安全审核,所以将messageAuthenticationAuditLevel属性替换成serviceAuthorizationAuditLevel。
1: <configuration>
2: <system.serviceModel>
3: ...
4: <behaviors>
5: <serviceBehaviors>
6: <behavior name="authenticationAudit">
7: <serviceSecurityAudit auditLogLocation ="Application"
8: serviceAuthorizationAuditLevel ="SuccessOrFailure"/>
9: </behavior>
10: </serviceBehaviors>
11: </behaviors>
12: </system.serviceModel>
13: </configuration>
客户端在进行服务调用的时候需要为帐号Bar指定正确的密码。和前面一样,这里的帐号Foo被预先添加到管理员用户组中,而Bar则没有,所以只有第一次服务调用才是被成功授权的。
1: ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorService");
2: NetworkCredential credential = channelFactory.Credentials.Windows.ClientCredential;
3: credential.UserName = "Foo";
4: credential.Password = "Password";
5: ICalculator calculator = channelFactory.CreateChannel();
6: Invoke(calculator);
7:
8: channelFactory = new ChannelFactory<ICalculator>("calculatorService");
9: credential = channelFactory.Credentials.Windows.ClientCredential;
10: credential.UserName = "Bar";
11: credential.Password = "Password";
12: calculator = channelFactory.CreateChannel();
13: Invoke(calculator);
当客户端完成两次服务调用后,如下两条基于授权的审核日志被写入应用程序日志。虽然只有第一次服务调用才是真正被授权的操作,但是从日志的内容我们却发现两条均是“授权成功”的审核日志。[源代码从这里下载]
1: Service authorization succeeded.
2: Service: http://127.0.0.1:3721/calculatorservice
3: Action: http://www.artech.com/ICalculator/Add
4: ClientIdentity: Jinnan-PC\Foo; S-1-5-21-3534336654-2901585401-846244909-1006
5: AuthorizationContext: uuid-528e86ce-a4f4-48b3-9a2e-b713e1dea539-1
6: ActivityId: <null>
7: ServiceAuthorizationManager: <default>
8:
9: Service authorization succeeded.
10: Service: http://127.0.0.1:3721/calculatorservice
11: Action: http://www.artech.com/ICalculator/Add
12: ClientIdentity: Jinnan-PC\Bar; S-1-5-21-3534336654-2901585401-846244909-1007
13: AuthorizationContext: uuid-528e86ce-a4f4-48b3-9a2e-b713e1dea539-2
14: ActivityId: <null>
15: ServiceAuthorizationManager: <default>
实际对于安全审核中所谓的授权失败指的是调用ServiceAuthorizationManager的CheckAccess方法返回False的情况。在默认的情况下,ServiceAuthorizationManager的CheckAccess方法总是返回True,所以授权总是会“成功”。
为了迎合安全审核对“授权失败”的判断,我在Service项目中创建了如下一个简单的自定ServiceAuthorizationManager。在重写的CheckAccessCore方法中,如果当前用户是Foo(Jinnan-PC\Jinnan)就发返回True,否则返回False。
1: using System.ServiceModel;
2: namespace Artech.WcfServices.Service
3: {
4: public class MyServiceAuthorizationManager : ServiceAuthorizationManager
5: {
6: protected override bool CheckAccessCore(OperationContext operationContext)
7: {
8: string userName = operationContext.ServiceSecurityContext.PrimaryIdentity.Name;
9: return string.Compare(userName, @"Jinnan-PC\Foo",true) == 0;
10: }
11: }
12: }
然后通过如下的配置将这个自定义的MyServiceAuthorizationManager应用到ServiceAuthorizationBehavior服务行为上。再次运行我们的程序,将会得到分别代表授权成功和失败的两条审核日志,并且在日志中还包含了我们自定的ServiceAuthorizationManager类型(ServiceAuthorizationManager: MyServiceAuthorizationManager)。[源代码从这里下载]
1: <configuration>
2: <system.serviceModel>
3: ...
4: <behaviors>
5: <serviceBehaviors>
6: <behavior name="authenticationAudit">
7: <serviceAuthorization serviceAuthorizationManagerType="Artech.WcfServices.Service.MyServiceAuthorizationManager, Artech.WcfServices.Service">
8: </serviceAuthorization>
9: <serviceSecurityAudit auditLogLocation ="Application"
10: serviceAuthorizationAuditLevel ="SuccessOrFailure"/>
11: </behavior>
12: </serviceBehaviors>
13: </behaviors>
14: </system.serviceModel>
15: </configuration>
授权成功/失败审核日志:
1: Service authorization succeeded.
2: Service: http://127.0.0.1:3721/calculatorservice
3: Action: http://www.artech.com/ICalculator/Add
4: ClientIdentity: Jinnan-PC\Foo; S-1-5-21-3534336654-2901585401-846244909-1006
5: AuthorizationContext: uuid-91ff29c8-84cb-4c1a-837c-0913c12c0be5-1
6: ActivityId: <null>
7: ServiceAuthorizationManager: MyServiceAuthorizationManager
8:
9: Service authorization failed.
10: Service: http://127.0.0.1:3721/calculatorservice
11: Action: http://www.artech.com/ICalculator/Add
12: ClientIdentity: Jinnan-PC\Bar; S-1-5-21-3534336654-2901585401-846244909-1007
13: AuthorizationContext: uuid-91ff29c8-84cb-4c1a-837c-0913c12c0be5-2
14: ActivityId: <null>
15: ServiceAuthorizationManager: MyServiceAuthorizationManager
16: FaultException: Access is denied.