2015年1月

首页2015年1月
13
Jan
0

WCF为每个操作附加身份信息

上文WCF进阶:将消息正文Base64编码中介绍了实现自定义MessageInspector来记录消息和实现自定义Formatter来改写消息,本文介绍一下在WCF中使用SoapHeader进行验证的两种实现方法,同时再次复习自定义Inspector和自定义EndpointBehavior。

在Xml Web Service中能将用户的身份信息如用户名,密码添加到SoapHeader中,从而实现服务调用的身份验证,这种做法是沿用了Http中用户名,密码身份验证,是我们最乐于接受的。而在WCF中因为提供了非常健壮的安全机制,但实现起来真是不够简单。对于多数应用情景来讲,有点大炮打蚊子的感觉。因此好多人在网上询问在WCF中如何象XMl Web Service一样使用SoapHeader来完成用户名,密码身份验证。传统的办法是通过在服务的操作中从OperationContext.Current.IncomingMessageHeaders来获取Header中的内容,而在客户端在OperationContext.Current.OutgoingMessageHeaders中添加MessageHeader。下面的代码片段简要的介绍了这种实现:

在服务端的一个Operation中

public?string?GetData(int?value)

{

System.Text.Encoding?encoding = System.Text.Encoding.GetEncoding("utf-8");

string?username =?"";

string?pwd =?"";

int?index =?OperationContext.Current.IncomingMessageHeaders.FindHeader("username",?"http://tempuri.org");

if?(index >= 0)

{

username =?OperationContext.Current.IncomingMessageHeaders.GetHeader<string>(index).ToString();

}

index =?OperationContext.Current.IncomingMessageHeaders.FindHeader("pwd",?"http://tempuri.org");

if?(index >= 0)

{

pwd =?OperationContext.Current.IncomingMessageHeaders.GetHeader<string>(index).ToString();

}

return?string.Format("You entered: {0}", value);

}

在客户端调代码如下:

Robin_Wcf_Formatter_Svc.Service1Client?svc =?new?Robin_Wcf_Formatter_Svc.Service1Client();

using?(OperationContextScope?scope =?new?OperationContextScope(svc.InnerChannel))

{

MessageHeader?header =?MessageHeader.CreateHeader("username",?"http://tempuri.org",?"robinzhang");

OperationContext.Current.OutgoingMessageHeaders.Add(header);

header =?MessageHeader.CreateHeader("pwd",?"http://tempuri.org",?"robinzhang");

OperationContext.Current.OutgoingMessageHeaders.Add(header);

string?res = svc.GetData(10);

}

 

通过上边的代码实现,已经能在WCF中使用SoapHeader来传递身份信息了。但这种方式需要在每次客户端调用和每个服务操作中都增加类似代码片断。比较麻烦。多数情况下,我们的服务开发好之后,往往只开放给固定的用户用于消费,如果我们的服务的实例模式为PerCall,也就是不保存会话,同时我们又希望能验证调用者的身份信息,我们需要在每个Operation的消息中增加SoapHeader来附加身份信息。这样服务即可保证每一个操作都不被非法调用。阅读完上篇文章,已经了解到通过MessageInspector能拦截消息用于记录或者修改,如果在拦截到消息之后,在消息中增加MessageHeader便可以实现上述需求。为此我们实现了一个实现IClientMessageInspector, IDispatchMessageInspector, IEndpointBehavior三个接口的类,这样该类就承担了两种角色,自定义MessageInspector,自定义EndpointBehavior。这个类的代码如下:

 

using?System;

using?System.Collections.Generic;

using?System.Linq;

using?System.Text;

using?System.ServiceModel.Dispatcher;

using?System.ServiceModel.Description;

using?System.ServiceModel.Channels;

using?System.ServiceModel;

 

namespace?RobinLib

{

public?class?AttachUserNamePasswordBehavior?:?IClientMessageInspector,?IDispatchMessageInspector,?IEndpointBehavior

{

private?static?string?UserName = System.Configuration.ConfigurationSettings.AppSettings["username"];

private?static?string?Password = System.Configuration.ConfigurationSettings.AppSettings["pwd"];

 

public?AttachUserNamePasswordBehavior()

{

}

 

 

#region?IClientMessageInspector 成员

 

public?void?AfterReceiveReply(ref?System.ServiceModel.Channels.Message?reply,?object?correlationState)

{

 

}

 

public?object?BeforeSendRequest(ref?System.ServiceModel.Channels.Message?request, System.ServiceModel.IClientChannel?channel)

{

MessageHeader?userNameHeader =?MessageHeader.CreateHeader("OperationUserName",?"http://tempuri.org", UserName,?false,?"");

MessageHeader?pwdNameHeader =?MessageHeader.CreateHeader("OperationPwd",?"http://tempuri.org", Password,?false,?"");

request.Headers.Add(userNameHeader);

request.Headers.Add(pwdNameHeader);

Console.WriteLine(request);

return?null;

}

 

#endregion

 

#region?IDispatchMessageInspector 成员

 

string?GetHeaderValue(string?key)

{

int?index =?OperationContext.Current.IncomingMessageHeaders.FindHeader(key,?"http://tempuri.org");

if?(index >= 0)

{

return?OperationContext.Current.IncomingMessageHeaders.GetHeader<string>(index).ToString();

}

return?null;

}

 

public?object?AfterReceiveRequest(ref?System.ServiceModel.Channels.Message?request, System.ServiceModel.IClientChannel?channel, System.ServiceModel.InstanceContext?instanceContext)

{

Console.WriteLine(request);

string?username = GetHeaderValue("OperationUserName");

string?pwd = GetHeaderValue("OperationPwd");

if?(username ==?"robinzhang"?&& pwd ==?"111111")

{

}

else

{

throw?new?Exception("操作中的用户名,密码不正确!");

}

return?null;

}

 

public?void?BeforeSendReply(ref?System.ServiceModel.Channels.Message?reply,?object?correlationState)

{

 

}

 

#endregion

 

#region?IEndpointBehavior 成员

 

public?void?AddBindingParameters(ServiceEndpoint?endpoint, System.ServiceModel.Channels.BindingParameterCollection?bindingParameters)

{

 

}

 

public?void?ApplyClientBehavior(ServiceEndpoint?endpoint, System.ServiceModel.Dispatcher.ClientRuntime?clientRuntime)

{

clientRuntime.MessageInspectors.Add(new?AttachUserNamePasswordBehavior());

}

 

public?void?ApplyDispatchBehavior(ServiceEndpoint?endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher?endpointDispatcher)

{

endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new?AttachUserNamePasswordBehavior());

}

 

public?void?Validate(ServiceEndpoint?endpoint)

{

 

}

 

#endregion

}

}

 

象上文一样,将自定义的EndpointBehavior通过代码方式应用到Host和Proxy中

 

服务宿主程序

using?System;

using?System.Collections.Generic;

using?System.Linq;

using?System.Text;

using?System.ServiceModel;

 

namespace?Robin_Wcf_OperationWithToken_Host

{

public?class?Program

{

static?void?Main(string[] args)

{

//服务地址

Uri?baseAddress =?new?Uri("net.tcp://127.0.0.1:8081/Robin_Wcf_Formatter");

ServiceHost?host =?new?ServiceHost(typeof(Robin_Wcf_OperationWithToken_SvcLib.Service1),?new?Uri[] { baseAddress });

//服务绑定

NetTcpBinding?bind =?new?NetTcpBinding();

host.AddServiceEndpoint(typeof(Robin_Wcf_OperationWithToken_SvcLib.IService1), bind,?"");

if?(host.Description.Behaviors.Find<System.ServiceModel.Description.ServiceMetadataBehavior>() ==?null)

{

System.ServiceModel.Description.ServiceMetadataBehavior?svcMetaBehavior =?new?System.ServiceModel.Description.ServiceMetadataBehavior();

svcMetaBehavior.HttpGetEnabled =?true;

svcMetaBehavior.HttpGetUrl =?new?Uri("http://127.0.0.1:8001/Mex");

host.Description.Behaviors.Add(svcMetaBehavior);

}

host.Opened +=?new?EventHandler(delegate(object?obj,?EventArgs?e)

{

Console.WriteLine("服务已经启动!");

});

foreach?(var?sep?in?host.Description.Endpoints)

{

sep.Behaviors.Add(new?RobinLib.AttachUserNamePasswordBehavior());

}

host.Open();

Console.Read();

}

}

}

 

客户端代理

[System.Diagnostics.DebuggerStepThroughAttribute()]

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel",?"3.0.0.0")]

public?partial?class?Service1Client?: System.ServiceModel.ClientBase<Robin_Wcf_OperationWithToken_ClientApp.ServiceReference1.IService1>, Robin_Wcf_OperationWithToken_ClientApp.ServiceReference1.IService1

{

 

public?Service1Client()

{

base.Endpoint.Behaviors.Add(new?RobinLib.AttachUserNamePasswordBehavior());

}

 

public?Service1Client(string?endpointConfigurationName) :

base(endpointConfigurationName)

{

base.Endpoint.Behaviors.Add(new?RobinLib.AttachUserNamePasswordBehavior());

}

 

public?Service1Client(string?endpointConfigurationName,?string?remoteAddress) :

base(endpointConfigurationName, remoteAddress)

{

base.Endpoint.Behaviors.Add(new?RobinLib.AttachUserNamePasswordBehavior());

}

 

public?Service1Client(string?endpointConfigurationName, System.ServiceModel.EndpointAddress?remoteAddress) :

base(endpointConfigurationName, remoteAddress)

{

base.Endpoint.Behaviors.Add(new?RobinLib.AttachUserNamePasswordBehavior());

}

 

public?Service1Client(System.ServiceModel.Channels.Binding?binding, System.ServiceModel.EndpointAddress?remoteAddress) :

base(binding, remoteAddress)

{

base.Endpoint.Behaviors.Add(new?RobinLib.AttachUserNamePasswordBehavior());

}

 

public?string?GetData(int?value)

{

return?base.Channel.GetData(value);

}

 

public?Robin_Wcf_OperationWithToken_ClientApp.ServiceReference1.CompositeType?GetDataUsingDataContract(Robin_Wcf_OperationWithToken_ClientApp.ServiceReference1.CompositeType?composite)

{

return?base.Channel.GetDataUsingDataContract(composite);

}

到此,代码基本实现了,在正式应用的时候,我们只需要为每个客户端创建独立的用户名,密码对,然后将这个信息通过一些渠道告诉服务消费者,服务消费者需要将用户名,密码放到Web.Config中的AppSettings中。而且在正式应用的时候,需要将放置到MessageHeader中的用户名,密码进行加密,而不是明文传输。这样这套机制就能用于生产啦。

 

通过这种办法,我们能为每个操作都设定身份验证,同时不需要更改Operation函数内容和客户端调用方式,我们来看一下运行结果:

 

用户,密码正确情况下的调用

服务器端:

客户端:

 

如果用户名,密码不匹配,服务能正常运行,但客户端会遇到异常

 

13
Jan
0

WCF统一读取管理的模块

[c]

ICollection<BindingElement> bindingElements = new List<BindingElement>();
HttpTransportBindingElement httpBindingElement = new HttpTransportBindingElement();
CryptEncodingBindingElement textBindingElement = new CryptEncodingBindingElement(new BinaryMessageEncodingBindingElement(), key, iv);
bindingElements.Add(textBindingElement);
bindingElements.Add(httpBindingElement);
Binding = new CustomBinding(bindingElements);

/////////////////////

String endPoint = GetEndPoint("WeiXinBaseDataService");
Client = new WeiXinBaseDataClient(Binding, new EndpointAddress(endPoint)); ;

Client.Endpoint.Behaviors.Add(new AttachUserNamePasswordBehavior());

[/c]

 

WCF进阶:扩展bindingElementExtensions支持对称加密传输

??? 前面两篇文章WCF进阶:将编码后的字节流压缩传输WCF 进阶: 对称加密传输都是实现了自定义编码,那两个例子中托管服务或者客户端调用都采用的代码实现,WCF更友好的方式是在app.config或web.config来配置服务的运行和调用,本文是介绍如何在配置文件中配置自定义的BindingElement。

上文WCF 进阶: 对称加密传输中我们实现了一个自定义的BindingElement:CryptEncodingBindingElement,它是一个MessageEncodingBindingElement,在宿主程序中,我们通过代码的方式将其添加到CustomBinding中去,方法为:

ICollection<BindingElement> bindingElements = new List<BindingElement>();
HttpTransportBindingElement httpBindingElement = new HttpTransportBindingElement();
string key = "JggkieIw7JM=";
string iv = "XdTkT85fZ0U=";
CryptEncodingBindingElement textBindingElement = new CryptEncodingBindingElement(new BinaryMessageEncodingBindingElement(), key, iv);
bindingElements.Add(textBindingElement);
bindingElements.Add(httpBindingElement); 
CustomBinding bind = new CustomBinding(bindingElements);

如果是缺省的BindingElement,如TextMessageEncodingElement,HttpTransportElement,ReliableSessionElement,SecurityElement都可以像如下这样的方式进行配置:

<bindings>
  <customBinding>
    <binding name="myBinding">
      <textMessageEncoding/>
      <reliableSession/>
      <security/>
      <httpTransport/>
    </binding>
  </customBinding>
</bindings>

如果要想让我们自定义的BindingElement和上面的一样享受同等待遇的话,我们需要再额外做一些事情,那就是重载BindingElementExtensionElement,在.Net中扩展配置的基类为:ConfigurationElement,它定义声明了一些扩展配置的属性和方法,为了方便WCF重载了ConfigurationElement形成了ServiceModelExtensionElement,而为了进一步的方便扩展BindingElement,又提供了BindingElementExtensionElement的基类,创建自定义的BindingElementExtension,只需要重载BindingElementExtensionElement三个方法:

using System;
using System.ServiceModel.Channels;

namespace System.ServiceModel.Configuration
{
    // 摘要:
    //     为使用计算机或应用程序配置文件中的自定义 System.ServiceModel.Channels.BindingElement 实现提供支持。
    public abstract class BindingElementExtensionElement : ServiceModelExtensionElement
    {
        // 摘要:
        //     初始化 System.ServiceModel.Configuration.BindingElementExtensionElement 类的新实例。
        protected BindingElementExtensionElement();

        // 摘要:
        //     在派生类中重写时,获取表示自定义绑定元素的 System.Type 对象。
        //
        // 返回结果:
        //     一个表示自定义绑定类型的 System.Type 对象。
        public abstract Type BindingElementType { get; }

        // 摘要:
        //     将指定绑定元素的内容应用到此绑定配置元素。
        //
        // 参数:
        //   bindingElement:
        //     一个绑定元素。
        //
        // 异常:
        //   System.ArgumentNullException:
        //     bindingElement 为 null。
        public virtual void ApplyConfiguration(BindingElement bindingElement);
        //
        // 摘要:
        //     在派生类中重写时,返回一个自定义绑定元素对象。
        //
        // 返回结果:
        //     一个自定义 System.ServiceModel.Channels.BindingElement 对象。
        protected internal abstract BindingElement CreateBindingElement();
        //
        // 摘要:
        //     使用指定绑定元素的内容来初始化此绑定配置节。
        //
        // 参数:
        //   bindingElement:
        //     一个绑定元素。
        protected internal virtual void InitializeFrom(BindingElement bindingElement);
    }
}

public abstract?Type?BindingElementType {?get; } 这个属性中只需要返回自定义类的类型,protected internal abstract?BindingElement?CreateBindingElement()的重载就是在这创建需要的自定义BindingElement,如果自定义的BindingElement有额外的属性或者参数,我们可以还可以通过创建带有ConfigurationPropertyAttribute的属性来指定。这个是.Net配置中的通性。在我们的需求中,需要一个CryptEncodingBindingElement实例,而CryptEncodingBindingElement带有三个参数:InnerMessageEncodingBindingElement,Key,IV。分析到这,我们的自定义BindingElement配置扩展类就成形了,代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Configuration;
using System.Configuration;

namespace RobinLib
{
    public class CryptEncodingBindingElementConfiguration : BindingElementExtensionElement
    {
        public override void ApplyConfiguration(System.ServiceModel.Channels.BindingElement bindingElement)
        {
            CryptEncodingBindingElement bind = bindingElement as CryptEncodingBindingElement;
            if (InnerMessageEncoding.ToLower() == "text")
            {
                bind.InnerMessageEncodingBindingElement = new System.ServiceModel.Channels.TextMessageEncodingBindingElement();
            }
            else if (InnerMessageEncoding.ToLower() == "binary")
            { 
                bind.InnerMessageEncodingBindingElement = new System.ServiceModel.Channels.BinaryMessageEncodingBindingElement();
            }
            else if (InnerMessageEncoding.ToLower() == "mtom")
            {
                bind.InnerMessageEncodingBindingElement = new System.ServiceModel.Channels.MtomMessageEncodingBindingElement();

            }
            bind.Key = Key;
            bind.IV = IV;
            base.ApplyConfiguration(bindingElement);
        }

        [ConfigurationProperty("innerMessageEncoding",DefaultValue="text")]
        public string InnerMessageEncoding
        {
            get
            {
                return base["innerMessageEncoding"] as string;
            }
            set
            {
                base["innerMessageEncoding"] = value;
            }
        }

        [ConfigurationProperty("key", DefaultValue = "")]
        public string Key
        {
            get
            {
                return base["key"] as string;
            }
            set
            {
                base["key"] = value;
            }
        }

        [ConfigurationProperty("iv", DefaultValue = "")]
        public string IV
        {
            get
            {
                return base["iv"] as string;
            }
            set
            {
                base["iv"] = value;
            }
        }

        public override Type BindingElementType
        {
            get { return typeof(CryptEncodingBindingElementConfiguration); }
        }

        protected override System.ServiceModel.Channels.BindingElement CreateBindingElement()
        {
            if (InnerMessageEncoding.ToLower() == "text")
            {
                CryptEncodingBindingElement bindElement = new CryptEncodingBindingElement(new System.ServiceModel.Channels.TextMessageEncodingBindingElement(), Key, IV);
                return bindElement;
            }
            else if (InnerMessageEncoding.ToLower() == "binary")
            {
                CryptEncodingBindingElement bindElement = new CryptEncodingBindingElement(new System.ServiceModel.Channels.BinaryMessageEncodingBindingElement(), Key, IV);
                return bindElement;
            }
            else if (InnerMessageEncoding.ToLower() == "mtom")
            {
                CryptEncodingBindingElement bindElement = new CryptEncodingBindingElement(new System.ServiceModel.Channels.MtomMessageEncodingBindingElement(), Key, IV);
                return bindElement;
            }
            throw new Exception("只支持Text,Binary,MTOM三种内置编码器!");
        }
    }
}

重载实现之后,我们就可以使用它在App.Config或者Web.Config来配置BindingElement,使用的方法为:

1. 在<system.serviceModel><extensions> <bindingElementExtensions>中添加新的配置节点的绑定。

2. 创建新的注册后的节点到自定义绑定中。

来看一下服务的配置文件吧:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <clear/>
      <service name="Robin_Wcf_CustomMessageEncoder_SvcLib.Service1">
        <host>
          <baseAddresses>
            <add baseAddress="http://127.0.0.1:8081/"/>
          </baseAddresses>
        </host>
        <endpoint address="Robin_Wcf_Formatter" name="ep" contract="Robin_Wcf_CustomMessageEncoder_SvcLib.IService1"  binding="customBinding" bindingConfiguration="myBinding"></endpoint>
      </service>
    </services>
    <bindings>
      <customBinding>
        <binding name="myBinding">
          <cryptMessageEncoding key="JggkieIw7JM=" iv="XdTkT85fZ0U=" innerMessageEncoding="Binary"/>
          <httpTransport/>
        </binding>
      </customBinding>
    </bindings>
    <extensions> 
      <bindingElementExtensions>
        <add name="cryptMessageEncoding" type="RobinLib.CryptEncodingBindingElementConfiguration,RobinLib,Version=1.0.0.0, Culture=neutral,PublicKeyToken=null"/>
      </bindingElementExtensions>
    </extensions>
  </system.serviceModel>
</configuration>

托管服务的代码就轻便了好多,如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using Robin_Wcf_CustomMessageEncoder_SvcLib;
using System.ServiceModel.Channels;
using RobinLib;

namespace Robin_Wcf_CustomMessageEncoder_Host
{
    class Program
    {
        static void Main(string[] args)
        { 
            ServiceHost host = new ServiceHost(typeof(Service1)); 
            if (host.Description.Behaviors.Find<System.ServiceModel.Description.ServiceMetadataBehavior>() == null)
            {
                System.ServiceModel.Description.ServiceMetadataBehavior svcMetaBehavior = new System.ServiceModel.Description.ServiceMetadataBehavior();
                svcMetaBehavior.HttpGetEnabled = true;
                svcMetaBehavior.HttpGetUrl = new Uri("http://127.0.0.1:8001/Mex");
                host.Description.Behaviors.Add(svcMetaBehavior);
            }
            host.Opened += new EventHandler(delegate(object obj, EventArgs e)
            {
                Console.WriteLine("服务已经启动!");
            }); 
            host.Open();
            Console.Read();
        }
    }
}

运行项目,能得到和WCF 进阶: 对称加密传输中一致的输出!