Spiga

WCF中使用扩展行为来验证连接的用户

2010-03-21 11:20 by ruinet, 1470 visits, 收藏, 编辑

        在WCF实现安全控制的方法很多,如使用证书、windows身份认证等等。本文要介绍的是使用简单的用户名密码方式来验证,客户端在与服务端交互时附带传递用户名和密码。使用该方法的好处就是配置简单,不受环境的制约。

通过WCF中的扩展行为来将用户名和密码附加到消息头MessageHeader中,自然这样就可以在服务端通过读取IncomingMessageHeaders得到用户名和密码。

       自定义行为,自定义行为要实现:IEndpointBehavior BehaviorExtensionElement

IEndpointBehavior members:

代码
public void AddBindingParameters(ServiceEndpoint endpoint, 
            System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
        {
            
return;
        }

        
public void ApplyClientBehavior(ServiceEndpoint endpoint, 
            System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(
new IdentityHeaderInspector());
        }

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

        }

        
public void Validate(ServiceEndpoint endpoint)
        {
            
return;
        }

 

BehaviorExtensionElement members

 

代码
  public override Type BehaviorType
        {
            
get
            {
                
return typeof(AttachContextBehavior);
            }
        }

        
protected override object CreateBehavior()
        {
            
return new AttachContextBehavior();
        }

 


自定义消息检查器IClientMessageInspector,在消息检查器中添加MessageHeader附加用户名和密码:

代码
  private class IdentityHeaderInspector : IClientMessageInspector
        {
            
#region IClientMessageInspector Members

            
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
<string> header = new MessageHeader<string>(AppContext.UserName);
                request.Headers.Add(header.GetUntypedHeader(
"UserName""http://ruinet.cnblogs.com/username"));

                header 
= new MessageHeader<string>(AppContext.Password);
                request.Headers.Add(header.GetUntypedHeader(
"Password""http://ruinet.cnblogs.com/password"));
                
return null;
            }

            
#endregion

        }

 

 

然后在实现的IEndpointBehavior接口的ApplyClientBehavior行为中添加IdentityHeaderInspector

代码
public void ApplyClientBehavior(ServiceEndpoint endpoint, 
            System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(
new IdentityHeaderInspector());
        }

完整扩展行为代码:
代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Dispatcher;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Configuration;
using WCFSecurity.Security;

namespace WCFSecurity.Security
{
    
public class AttachContextBehavior : BehaviorExtensionElement, IEndpointBehavior
    {
        
public override Type BehaviorType
        {
            
get
            {
                
return typeof(AttachContextBehavior);
            }
        }

        
protected override object CreateBehavior()
        {
            
return new AttachContextBehavior();
        }

        
#region IEndpointBehavior Members

        
public void AddBindingParameters(ServiceEndpoint endpoint, 
            System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
        {
            
return;
        }

        
public void ApplyClientBehavior(ServiceEndpoint endpoint, 
            System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(
new IdentityHeaderInspector());
        }

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

        }

        
public void Validate(ServiceEndpoint endpoint)
        {
            
return;
        }

        
#endregion

        
private class IdentityHeaderInspector : IClientMessageInspector
        {
            
#region IClientMessageInspector Members

            
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
<string> header = new MessageHeader<string>(AppContext.UserName);
                request.Headers.Add(header.GetUntypedHeader(
"UserName""http://ruinet.cnblogs.com/username"));

                header 
= new MessageHeader<string>(AppContext.Password);
                request.Headers.Add(header.GetUntypedHeader(
"Password""http://ruinet.cnblogs.com/password"));
                
return null;
            }

            
#endregion

        }
    }
}

 在客户端的配置文件中加入扩展行为AttachContextBehavior

 

代码
<behaviors>
            
<endpointBehaviors>
                
<behavior name="headersMapping">
                    
<attachContextHeader/>
                
</behavior>
            
</endpointBehaviors>
        
</behaviors>
        
<extensions>
            
<behaviorExtensions>
                
<add name="attachContextHeader" type="WCFSecurity.Security.AttachContextBehavior, WCFSecurity.Security, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
            
</behaviorExtensions>
        
</extensions>

 

behaviorExtensions节点中加入自定义对象,然后再behavior节点指定:<attachContextHeader/>,最后在endpoint的behaviorConfiguration指定行为即可:

 

代码
<endpoint name="CalculateService"
    contract 
="WCFSecurity.Contracts.ICalculateService"
    address
="http://localhost:8001/CalculateService"
    binding 
="wsHttpBinding"
    bindingConfiguration 
="wsHttpBindingConfig"
    behaviorConfiguration 
="headersMapping" />

实例客户端运行时传递用名ruinet和密码88888:

 

代码
 static void Main(string[] args)
        {
            AppContext.UserName 
= "ruinet";
            AppContext.Password 
= "888888";

            Console.WriteLine(
"connecting host..");
            CalculateServiceProxy proxy 
= new CalculateServiceProxy();
            Console.WriteLine ( proxy.Add(
1222));

            proxy.Close();

            Console.WriteLine(
"close connect");
            Console.ReadLine(); 
        }

 

在服务器端接收用户名和密码:

 

代码
namespace WCFSecurity.Services
{
    
public class CalculateService : WCFSecurity.Contracts.ICalculateService
    {
        
public double Add(double num1, double num2)
        {
            Console.WriteLine(
"Client  Requirement is coming");

            
int index= OperationContext.Current.IncomingMessageHeaders.FindHeader("UserName"
                
"http://ruinet.cnblogs.com/username");
            
if (index != -1)
            {
                
string userName = OperationContext.Current.IncomingMessageHeaders.GetHeader<string>("UserName"
                    
"http://ruinet.cnblogs.com/username");
                
string password = OperationContext.Current.IncomingMessageHeaders.GetHeader<string>("Password"
                    
"http://ruinet.cnblogs.com/password");

                Console.WriteLine(
"UserName:" + userName);
                Console.WriteLine(
"Password:" + password);
            }
          

            
return num1 + num2;
        }
    }
}

 

运行结果:

代码下载

Add your comment

23 条回复

  1. #1楼 xwy      2010-03-21 16:02
    学习
     回复 引用 查看   
  2. #2楼 Artech      2010-03-21 16:33
    将Password放入Context是决定不允许的!
     回复 引用 查看   
  3. #3楼[楼主] ruinet      2010-03-21 17:47
    引用Artech:将Password放入Context是决定不允许的!

    why?实际环境可能采用明码不太合适,但是放在上下文中传递没什么不妥。在网络中要认证就得要传递账户。
     回复 引用 查看   
  4. #4楼 liulun      2010-03-22 08:34
    @Artech
    也没什么绝对不允许吧
    传递过程中不要传递明文就可以了
    另外也增加了传递数据包的大小
     回复 引用 查看   
  5. #5楼 liulun      2010-03-22 09:10
    @ruinet
    实际应用中,也没有什么需求要这样解决的吧?
    把用户名密码传递到服务端之后要去数据库验证是否正确
    这样的话,每次调用方法都要访问一次数据库。
    ....
    不如做session来的方便
     回复 引用 查看   
  6. #6楼[楼主] ruinet      2010-03-22 09:29
    @liulun
    第一次连接哪里来的session
     回复 引用 查看   
  7. #7楼 Artech      2010-03-22 09:42
    引用liulun:
    @Artech
    也没什么绝对不允许吧
    传递过程中不要传递明文就可以了
    另外也增加了传递数据包的大小

    我不是在说密码传递的问题,这可以通过加密来解决。我是说这句代码:AppContext.Password。如果AppContext可以理解为一个全局上下文的话,意味着你将密码进行了缓存,这是不符合规范的。密码不能被缓存起来,能被缓存的只能是Security Token!
    当然,如果AppContext不是我想的那样,另当别论!
     回复 引用 查看   
  8. #8楼 不若相忘于江湖      2010-03-22 10:08
    引用ruinet:
    @liulun
    第一次连接哪里来的session


    第一次从数据库中查, 以后的都采用SEESION.

     回复 引用 查看   
  9. #9楼 不若相忘于江湖      2010-03-22 10:33

    楼主. 提个疑问:

    1 客户端与服务器端共用引用一个程序集?

    2 客户端:
    AppContext.UserName = "ruinet";
    AppContext.Password = "888888";
    AppContext并不是数据契约,也不是消息契约, 它如何能从客户端
    传到服务器端.


     回复 引用 查看   
  10. #10楼[楼主] ruinet      2010-03-22 11:24
    引用不若相忘于江湖:
    引用ruinet:
    @liulun
    第一次连接哪里来的session


    第一次从数据库中查, 以后的都采用SEESION.


    不是所有binding支持session
     回复 引用 查看   
  11. #11楼[楼主] ruinet      2010-03-22 11:31
    引用不若相忘于江湖:
    楼主. 提个疑问:

    1 客户端与服务器端共用引用一个程序集?

    2 客户端:
    AppContext.UserName = "ruinet";
    AppContext.Password = "888888";
    AppContext并不是数据契约,也不是消息契约, 它如何能从客户端
    传到服务器端.



    NO!
    服务器端并不依赖AppContext
    主要要看的是下面这段代码,是通过MessageHeader传递的。

    OperationContext
      AppContext.UserName = "ruinet";
      AppContext.Password = "888888";
     
     public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
                {
                    MessageHeader<string> header = new MessageHeader<string>(AppContext.UserName);
                    request.Headers.Add(header.GetUntypedHeader("UserName", "http://ruinet.cnblogs.com/username"));
    
                    header = new MessageHeader<string>(AppContext.Password);
                    request.Headers.Add(header.GetUntypedHeader("Password", "http://ruinet.cnblogs.com/password"));
                    return null;
                }
    
     回复 引用 查看   
  12. #12楼 不若相忘于江湖      2010-03-22 12:00
    @ruinet

    谢谢.. 没注意到.. 哈哈. 原来它是放在客户端的.

    想问一个问题: 如果我需要这样的一个功能,该怎么办.

    当调用服务器端的任何方法时,一开始都会执行一个方法,

    就如同ASP.NET中的HTTPMOUDLE一样的. 该如何做呢.


     回复 引用 查看   
  13. #13楼[楼主] ruinet      2010-03-22 12:49
    引用不若相忘于江湖:
    @ruinet

    谢谢.. 没注意到.. 哈哈. 原来它是放在客户端的.

    想问一个问题: 如果我需要这样的一个功能,该怎么办.

    当调用服务器端的任何方法时,一开始都会执行一个方法,

    就如同ASP.NET中的HTTPMOUDLE一样的. 该如何做呢.



    和这个实现差不多,也是实现IEndpointBehavior接口,和客户端不同的是在public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
    {
    // 附加行为endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new AttachContextInspector());
    }


    private class AttachContextInspector : IDispatchMessageInspector
        {
          #region IDispatchMessageInspector Members
          //在已接收入站消息后将消息调度到应发送到的操作之前调用。
          public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
          {
           
            return null;
          }
          //在操作已返回后发送回复消息之前调用。
          public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
          {
            
          }
    
          #endregion
        }
    
     回复 引用 查看   
  14. #14楼 不若相忘于江湖      2010-03-22 13:45
    @ruinet

    谢谢...

     回复 引用 查看   
  15. #15楼 不若相忘于江湖      2010-03-22 16:15
    1 AfterReceiveRequest

    不会触发到这个事件,

    2 必面实现 IServiceBehavior接口 才可以使用

    <behaviorExtensions><add name="attachContextHeader">
    这里没有全部写上</behaviorExtensions>
     回复 引用 查看   
  16. #16楼[楼主] ruinet      2010-03-22 16:55
    引用不若相忘于江湖:
    1 AfterReceiveRequest

    不会触发到这个事件,

    2 必面实现 IServiceBehavior接口 才可以使用

    <behaviorExtensions><add name="attachContextHeader">
    这里没有全部写上</behaviorExtensions>


    不明白你的问题,如果你是在客户端附加行为,AfterReceiveRequest不会被触发,只会触发BeforeSendReply
    如果是在服务器端附加行为会先触发AfterReceiveRequest,然后再触发BeforeSendReply事件
     回复 引用 查看   
  17. #17楼 不若相忘于江湖      2010-03-22 17:36
    楼主.

    是这样的.

      void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
            {
    
                foreach (ChannelDispatcher ch1 in serviceHostBase.ChannelDispatchers)
                {
                    foreach (EndpointDispatcher end in ch1.Endpoints)
                    {
                        end.DispatchRuntime.MessageInspectors.Add(new AttachContextInspector());
                    }
                }
    
            }
    
     回复 引用 查看   
  18. #18楼 不若相忘于江湖      2010-03-22 17:47
    哦. 忘了说明.

    是在服务端
     回复 引用 查看   
  19. #19楼[楼主] ruinet      2010-03-22 21:09
    引用不若相忘于江湖:
    楼主.

    是这样的.

      void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
            {
    
                foreach (ChannelDispatcher ch1 in serviceHostBase.ChannelDispatchers)
                {
                    foreach (EndpointDispatcher end in ch1.Endpoints)
                    {
                        end.DispatchRuntime.MessageInspectors.Add(new AttachContextInspector());
                    }
                }
    
            }
    


    你实现的是什么接口,你搞错了吧,要实现IEndpointBehavior
     回复 引用 查看   
  20. #20楼 不若相忘于江湖      2010-03-23 09:52
    引用ruinet:
    引用不若相忘于江湖:
    楼主.

    是这样的.

    [code=csharp]
    void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {

    foreach (ChannelDispatcher ch1 in serviceHostBase.ChannelDispatchers)
    {
    foreach...


    实现IServiceBehavior 接口. 你在服务器端只实现IEndpointBehavior不

    会触发新的事件.


     回复 引用 查看   
  21. #21楼[楼主] ruinet      2010-03-23 11:56
    引用不若相忘于江湖:
    引用ruinet:
    引用不若相忘于江湖:
    楼主.

    是这样的.

    [code=csharp]
    void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {

    foreach (ChannelDispatcher ch1 in serviceHostBase.ChannelDispatchers)
    {
    foreach...


    实现IServiceBehavior 接口. 你在服务器端只实现IEndpointBehavior不

    会触发新的事件.




    谁说实现IEndpointBehavior不会触发事件。
    这个demo的完整实例,还需在服务端附加行为,来封装当前访问用户,而不是每次都通过string userName = OperationContext.Current.IncomingMessageHeaders.GetHeader<string>("UserName",
    "http://ruinet.cnblogs.com/username");这样的方法来获取。肯定可以触发
     回复 引用 查看   
  22. #22楼 年青人      2010-10-15 00:10
    GOOD,gooD,tks.
     回复 引用 查看   
  23. #23楼 暗香疏影[未注册用户]2010-12-24 13:29
    楼主,问下AppContext是在哪里定义的啊??????
     回复 引用   
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1690848 mXD90JGuQEM=

free web counter