最近在研究BizTalk 和 WCF,想到以前没有实现的一个关于安全方面的案例,特意抽时间做了一个Demo。仅供参考。
在BizTalk2006R2时,可以使用WSE3实现各种安全协议,只需要写一个WSEPolicy,然后添加各种安全认证的方式即可。使用WCF以后,可以支持更多的安全配置,而且在解析消息的效率方面也比通常的Web Service高一些。所以使用WCF Service已经是趋势。
废话不多说,先说一下BizTalk发布WCF的几种类型。通常情况下,可以选择BasicHttp,WSHttp和CustomIsolate的发布方式。Basic发布的Service和Soap Servie差不多,可以支持早期的SOAP 方式的调用。WSHttp全面支持WS-*,对安全方面要求较高的可以采用这种方式。如果还需要有自定义的方式的话,就是用Custom的方式好了,这种模式下可以使用自定义的Behavior,自定义的Binding,各种好处都不在话下。本示例主要使用WSHttp实现简单的安全认证和授权。
首先用BizTalk WCF服务发布向导发布一个WSHttp的Service,启用元数据终结点并创建接收位置,将Schema作为WCF服务发布,创建Web服务,并允许匿名访问。这时候BizTalk会在IIS下面创建一个Web应用,并且在BizTalk Application里面会创建相应的接收位置接受来自Web应用的消息。
这时候创建的WCF-WSHttp接收端口的传输属性下面,安全模式为"Message" ,消息客户端凭据类型为"Windows"。此时的WCF Service和一般的Service没有任何区别,不需要使用安全口令就可以调用和执行。我们启用接收端口之后就可以在IE浏览刚刚发布的Web Service。
此时的Web.Config包含的ServiceModel如下:
<!-- The <system.serviceModel> section specifies Windows Communication Foundation (WCF) configuration. --> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name="ServiceBehaviorConfiguration"> <serviceDebug httpHelpPageEnabled="true" httpsHelpPageEnabled="false" includeExceptionDetailInFaults="false" /> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="false" /> </behavior> </serviceBehaviors> </behaviors> <services> <!-- Note: the service name must match the configuration name for the service implementation. --> <service name="Microsoft.BizTalk.Adapter.Wcf.Runtime.BizTalkServiceInstance" behaviorConfiguration="ServiceBehaviorConfiguration"> <endpoint name="HttpMexEndpoint" address="mex" binding="mexHttpBinding" bindingConfiguration="" contract="IMetadataExchange" /> <!--<endpoint name="HttpsMexEndpoint" address="mex" binding="mexHttpsBinding" bindingConfiguration="" contract="IMetadataExchange" />--> </service> </services> </system.serviceModel>
接下来我们就要对这个Web服务和BizTalk的接收端口动手术了。
我们首先将BizTalk的接收端口的安全性里面的安全模式改为TransportWithMessageCredential,并且将消息客户端凭据类型改为UserName。同时将ServiceModel里面的HttpsGetEnable设置为"True"。这时候我们的Web服务就需要使用Https的安全链接来访问了。
更新Web服务的引用,重新调用该服务会出现为提供Credential的异常。那么,这个Credential该如何设置,设置成什么呢?
我们来看一下WCF的ServiceModel,定义Credential的地方在serviceBehavior里面,我们添加一个serviceCredential,采用简单的userNameAuthentication,选用Custom的validateMode,ValidateType将采用我们自定义的UserNamePasswordValidator。代码如下:
<serviceCredentials> <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="CustomAuthBehaviors.CustomValidator,CustomAuthBehaviors"/> </serviceCredentials>
这个CustomValidator我们怎么写呢?
由于我们指定了UserNamePasswordValidateMode为Custom,所以需要复写UserNamePasswordValidator类的Validate(string userName, string password),这个方法的解释如下:
// 摘要:当在派生类中重写时,验证指定的用户名和密码。 // 参数: // userName:要验证的用户名。 // password:要验证的密码。 public abstract void Validate(string userName, string password);
我们这样实现这个方法:
public class CustomValidator : UserNamePasswordValidator { public override void Validate(string userName, string password) { string username = System.Configuration.ConfigurationSettings.AppSettings.Get("username"); string pwd = System.Configuration.ConfigurationSettings.AppSettings.Get("password"); if (userName != username || password != pwd) { throw new SecurityTokenException("wrong password or username"); } } }
这个username和password将会配置在Web.Config的AppSetting中。
我们将这个类库签名,编译,并加入GAC,同时Copy到Web服务的Bin文件夹中。注意:CustomAuthBehaviors.CustomValidator,CustomAuthBehaviors(CustomAuthBehaviors为dll名和NameSpace,CustomValidator为继承的类名)。
重新启动IIS,在客户端程序中输入ClientCredentials,就可以成功的调用该WCF Service了。到这一步我们已经成功的实现了Authentication了。接下来就是要实现Authorization了。
我们注意到WCF的serviceBehaviors里面有个节点叫serviceAuthorization,这个里面就是我们可以定义Authorization的地方了。我们添加了一个这样的节点如下:
<serviceAuthorization serviceAuthorizationManagerType="CustomAuthBehaviors.CustomServiceAuthorizationManager,CustomAuthBehaviors" />
这里面的CustomServiceAuthorizationManager又是从何而来,如何实现我们所要的方法呢?在WCF的ServiceAuthorizationManager中,有如下解释:
// // 摘要: // 访问所需的消息时,检查给定操作上下文的授权。 // // 参数: // operationContext: // System.ServiceModel.OperationContext。 // // message: // 要接收检查以确定授权的 System.ServiceModel.Channels.Message。 // // 返回结果: // 如果授予了访问权,则为 true;否则为 false。默认值为 true。 [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] public virtual bool CheckAccess(OperationContext operationContext, ref Message message);
看来这个方法可以帮助我们实现根据消息的上下文去实现对操作的授权。在OperationContext这个类型里面我们能获取消息IncomingMessageHeaders的上下文属性,在Message里面我们可以获取到消息的正文。这样我们就可以根据这些属性和内容实现授权了,我们这样实现这个方法:
public class CustomServiceAuthorizationManager : ServiceAuthorizationManager { public override bool CheckAccess(OperationContext operationContext, ref Message message) { string username = operationContext.ServiceSecurityContext.PrimaryIdentity.Name; string action = operationContext.IncomingMessageHeaders.Action; string messageBody = message.ToString(); MemoryStream ms = new MemoryStream(); XmlWriter writer = XmlWriter.Create(ms); message.WriteMessage(writer); writer.Flush(); ms.Position = 0; XPathDocument doc = new XPathDocument(ms); string NodeName = doc.CreateNavigator().MoveToChild("localname", "nameSpace").ToString(); if (username == "UserName" && action == "Operation" && NodeName=="Request") return true; return false; } }
这里面当然可以根据我们的实际需要去判断用户的权限。这样我们的授权方法就实现了。重新编译CustomAuthBehaviors,GAC,iisreset,这时候在从客户端提交消息,就会根据我们在CheckAccess的设定去执行了。如果不能获得授权的话,就会返回未授权的信息了。
至此,我们的WCF的Authentication和Authorization就实现了,当然了,这些只是最简单的实现方法,我们还可以据此实现更高级的应用。
将此方案记录下来,希望能对各位有所帮助。