WizardWu 編程網

一位台灣的工程師,接觸 .NET 逾十年,近年研究 SQL Server、Performance Tuning、手機應用

博客园 首页 新随笔 联系 订阅 管理

实作最简单的 WCF 懶人验证方式,透过 SoapHeader 或 MessageContract 传送用户名、密码,以调用 WCF 服务。本帖最后,还附一个利用 MessageContract 处理表单提交的示例。

-------------------------------------------------
本帖的示例下载点:
https://files.cnblogs.com/WizardWu/101115.zip
(执行本示例,需要 Visual Studio 2008,不需要数据库)
---------------------------------------------------


我们都知道,WCF 有一套很完整的安全机制,例如可透过 SSL 保障传输层的安全、透过 Federation 加密和数位签章来保护信息本身、使用 Certificate 以跨域或跨平台传送凭证,甚至 WCF 4 还支持新的 WIF 框架以实现 SSO (Single Sign On) 单点登录 [3]


但若我们在企业的 Intranet 或局域网中,没有安全顾虑时 (信任自己公司或部门没有那么无聊的员工),只想用最简单的明文 username、password 验证 (安全后果自负),难道也非得创建:证书、SSL、…及一堆安全性的配置,才能调用 ClientCredentials.UserName.UserName、ClientCredentials.UserName.Password 这些 API 吗?


我以前曾在论坛问过此问题 (听说问过相同问题的人很多):

WCF 用户名、密码验证的设置问题:
http://topic.csdn.net/u/20101024/18/1e491713-46a1-412d-859b-453e49dad672.html?seed=181740069&r=69308273
http://space.cnblogs.com/q/19235/


结论:最简单的做法,是不使用 ClientCredentials,直接把 username、password 添加到 SOAP Header 中,来实现身份验证。有兴趣的人可参考:

WCF进阶:为每个操作附加身份信息
http://www.cnblogs.com/jillzhang/archive/2010/04/11/1709397.html


在传统的 Web Service 也有同样的做法,可参考以下这篇文章。若 Web Service 有安全性顾虑的话,可使用 WSE (Web Services Enhancements) 来加强安全性。

简单的 WebService 安全:
http://www.cnblogs.com/kuyijie/archive/2011/01/08/1930795.html


SOAP Header - 懶人验证法

前述 jillzhang 他用 SOAP Header 传递用户名、密码的做法:

Server-side :

//IService.cs

using System.ServiceModel;

[ServiceContract]
public interface IService
{
    [OperationContract]
    
string getData(int value);
}

 

Service.cs
using System.ServiceModel;

public class Service : IService
{
    
public string getData(int value)
    {
        
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("您输入了: " + value + ", <br> 帐号: " + username + ", 密码: " + pwd); 
    }
}



Client-side :

 

Default.aspx.cs
using System.ServiceModel;
using System.ServiceModel.Channels;

public partial class _Default : System.Web.UI.Page 
{
    
protected void Page_Load(object sender, EventArgs e)
    {
        ServiceReference1.ServiceClient svc 
= new ServiceReference1.ServiceClient();

        
using (OperationContextScope scope = new OperationContextScope(svc.InnerChannel))
        {
            MessageHeader header 
= MessageHeader.CreateHeader("username""http://tempuri.org""WizardWu");
            OperationContext.Current.OutgoingMessageHeaders.Add(header);

            header 
= MessageHeader.CreateHeader("pwd""http://tempuri.org""123456");
            OperationContext.Current.OutgoingMessageHeaders.Add(header);

            
string res = svc.getData(10);

            Response.Write(res);
        }
    }
}

 


图 1 执行画面


MessageContract 实际应用 1 - 懶人验证法


同样的身份验证功能,我们也可透过 MessageContract 类 [5] 来处理,将其包装得更漂亮,亦即可指定 SOAP envelope 的结构,然后就可用 message 作为参数或返回类型,并且在不修改默认 SOAP envelope 本身的情况下,来控制 SOAP body 内容序列化的信息。

MessageContract (消息契约) 对格式化 SOAP 消息,提供了有效的控制。虽然在许多情况下,DataContract (数据契约) 已提供了用户需要的大部分控制,但若需要更灵活地控制,可再加上 MessageContract 来辅助处理,如:允许用户将指定的类型映射到 SOAP 消息,并添加定制的 SOAP Header [7]


以下为 MCTS 70-503 WCF 认证,微软官方教程书籍 [6] 所述:

Data contracts enable you to define the structure of the data that will be sent in the body of your SOAP messages, either in the inbound (request) messages or in the outbound (response) messages. WCF Message contracts take it one step higher, when you use a Message contract as both the operation parameter type and the return type, you gain control over the entire SOAP message and not just over the structure of the data in the body. The following are the primary reasons you might want to use Message contracts :

* To control how the SOAP message body is structured and, ultimately, how it is serialized.
* To supply and access custom headers.


用 MessageContract 并透过其可「包装」的特性,我们可包装欲传递的用户名、密码,以及包装从服务器返回的信息,如下代码的 ContactInfoRequestMessage、ContactInfoResponseMessage 自定义类 (将这些类、接口都写在同一个 .cs 文件里,是不好的设计模式,这里只是为了演示方便):


Server-side :


IService.cs
using System.Runtime.Serialization;
using System.ServiceModel;

[ServiceContract]
public interface IService
{
    [OperationContract()]
    [FaultContract(
typeof(string))]
    ContactInfoResponseMessage GetProviderContactInfo(ContactInfoRequestMessage reqMsg);
}

[DataContract]
public class ContactInfo
{
    [DataMember()]
    
public string PhoneNumber;

    [DataMember()]
    
public string EmailAddress;
}

[MessageContract(IsWrapped 
= false)]
public class ContactInfoRequestMessage
{
    [MessageHeader()]
    
public string id;

    [MessageHeader()]
    
public string pwd;
}

[MessageContract(IsWrapped 
= false)]
public class ContactInfoResponseMessage
{
    [MessageBodyMember()]
    
public ContactInfo ProviderContractInfo;
}

 

Service.cs
using System.ServiceModel;

public class Service : IService
{
    
private const string username = "WizardWu";
    
private const string pwd = "123456";

    
public ContactInfoResponseMessage GetProviderContactInfo(ContactInfoRequestMessage reqMsg)
    {
        
if ((reqMsg.id != username) || (reqMsg.pwd != pwd))
        {
            
const string msg = "用户名、密码输入错误~~";
            
throw new FaultException<string>(msg);
        }

        
//返回信息。
        
//透过 MessageContract 包装 ContactInfoResponseMessage 自定义类及其成员,
        
//并包装真正存储数据的 DataContract 自定义类 ContactInfo

        ContactInfoResponseMessage respMsg 
= new ContactInfoResponseMessage();
        respMsg.ProviderContractInfo 
= new ContactInfo();
        respMsg.ProviderContractInfo.EmailAddress 
= "test@mail.com";
        respMsg.ProviderContractInfo.PhoneNumber 
= "0512-67891234";

        
return respMsg;
    }
}


Client-side :


Default.aspx.cs
using System.ServiceModel;

public partial class _Default : System.Web.UI.Page 
{
    
protected void Page_Load(object sender, EventArgs e)
    {
        ServiceReference1.ServiceClient svc 
= new ServiceReference1.ServiceClient();

        
try
        {
            
//透过 MessageContract 包装 ContactInfoRequestMessage 自定义类及其成员,让使用者输入用户名、密码
            ServiceReference1.ContactInfo ci = svc.GetProviderContactInfo("WizardWu""123456");

            Response.Write(
"电子邮件:" + ci.EmailAddress + "<br> 电话号码:" + ci.PhoneNumber);
        }
        
catch (FaultException<string> ex)
        {
            Response.Write(ex.Detail);  
//印出「用户名、密码输入错误~~」
        }
        
catch (Exception ex)
        {
            Response.Write(ex.ToString());
        }
    }
}

 


图 2 执行画面,输入了正确的用户名、密码,便可从 Server-side 取得想要的信息 (邮件、电话号码)



图 3 执行画面,输入了错误的用户名、密码


NOTE: Message contracts must be used all or none
There isn't any partial usage of Message contracts. After you introduce a Message contract into an operation's signature, you must use a Message contract as the "only" parameter type "and" as the return type of the operation. This is in contrast with the more typical scenario in which you have a parameter list or return value composed of Data contracts or serializable types.


Supplying Custom Headers
You might sometimes need to send along private elements in your SOAP messages, and defining Message contracts supports this. Two common reasons for doing this are that :
* You have your own security mechanism in place, so you need to pass along your own authentication token in a private SOAP header.
* Consumers of your service might have to include some sort of license key (例如上方示例的用户名和密码) to access the service at run time. In such cases, a SOAP header is a reasonable place for such a field.


MessageContract 实际应用 2 - 订单提交

接下来我们看 MessageContract 在实务上的另一种应用 - 网上书店的订单处理 [7]。本例中,定义了包含订书信息的 MessageContract,其会在 Server-side 和 Client-side 之间传递。如下 IService.cs 的代码,这个 BookOrder 的 message,成员包含一个消息头,和五个消息体。

头部包含了书的 ISBN 号,体包含了订单提交者的信息。订单提交后,会将此消息传递至 WCF Service,经过处理后,会把订单编号 (00012345678) 传回到客户端浏览器中显示。


示例的核心,在于 Service.cs 里的 WCF 服务,若系统将来需要修改 Server-side 逻辑,直接修改即可,不需要重新编译。若有宿主 (Host) 应用程序,也不用修改该个宿主应用程序。


Server-side :

 

IService.cs
using System;
using System.Runtime.Serialization;
using System.ServiceModel;

[ServiceContract]
public interface IService
{
    [OperationContract]
    
string InitiateOrder();

    [OperationContract()]
    BookOrder PlaceOrder(BookOrder request);

    [OperationContract]
    
string FinalizeOrder();
}

[MessageContract(IsWrapped 
= false)]
public class BookOrder
{
    
private string isbn;
    
private int quantity;
    
private string firstname;
    
private string lastname;
    
private string address;
    
private string ordernumber;

    
public BookOrder() { }

    
public BookOrder(BookOrder message)
    {
        
this.isbn = message.isbn;
        
this.quantity = message.quantity;
        
this.firstname = message.firstname;
        
this.lastname = message.lastname;
        
this.address = message.address;
        
//this.ordernumber = message.ordernumber;
    }

    [MessageHeader]
    
public string ISBN
    {
        
get { return isbn; }
        
set { isbn = value; }
    }

    [MessageBodyMember(Order 
= 1)]
    
public int Quantity
    {
        
get { return quantity; }
        
set { quantity = value; }
    }

    [MessageBodyMember(Order 
= 2)]
    
public string FirstName
    {
        
get { return firstname; }
        
set { firstname = value; }
    }

    [MessageBodyMember(Order 
= 3)]
    
public string LastName
    {
        
get { return lastname; }
        
set { lastname = value; }
    }

    [MessageBodyMember(Order 
= 4)]
    
public string Address
    {
        
get { return address; }
        
set { address = value; }
    }

    [MessageBodyMember(Order 
= 5)]
    
public string OrderNumber
    {
        
get { return ordernumber; }
        
set { ordernumber = value; }
    }
}

 

Service.cs
using System;
using System.ServiceModel;

public class Service : IService
{
    
string IService.InitiateOrder()
    {
        
return "开始处理订单...";   //进度显示「开始处理订单...」
    }

    
public BookOrder PlaceOrder(BookOrder request)
    {
        
//here you could do something with the information passed in to place the order.
        BookOrder response = new BookOrder(request);
        response.OrderNumber 
= "00012345678";  //系统给的订单编号
        return response;
    }

    
string IService.FinalizeOrder()
    {
        
return "您的订单已订购成功!";    //进度显示「您的订单已订购成功!」
    }
}


Client-side :


Default.aspx
<html>
<body>
    
<form id="form1" runat="server">
    
<div>    
        ISBN: 
<asp:TextBox ID="TextBox_isbn" runat="server"></asp:TextBox><br />
        数量: 
<asp:TextBox ID="TextBox_quantity" runat="server"></asp:TextBox><br />
        名字: 
<asp:TextBox ID="TextBox_firstname" runat="server"></asp:TextBox><br />
        姓氏: 
<asp:TextBox ID="TextBox_lastname" runat="server"></asp:TextBox><br />
        地址: 
<asp:TextBox ID="TextBox_address" runat="server"></asp:TextBox><br />
        订单编号(由系统自动带入): 
<asp:TextBox ID="TextBox_ordernumber" runat="server" ReadOnly="true" BackColor="Silver"></asp:TextBox><br />
        
<asp:Button ID="Button1" runat="server" Text="提交订单" onclick="Button1_Click" />
        
<p></p>
        
<asp:Label ID="Label_Progress" runat="server" ForeColor="Red"></asp:Label>
    
</div>
    
</form>
</body>
</html>

 

Default.aspx.cs
using System.ServiceModel;

public partial class _Default : System.Web.UI.Page 
{
    
protected void Page_Load(object sender, EventArgs e)
    {        
    }

    
protected void Button1_Click(object sender, EventArgs e)
    {
        ServiceReference1.ServiceClient svc 
= new ServiceReference1.ServiceClient();
        ServiceReference1.BookOrder bo 
= new ServiceReference1.BookOrder();
        
        bo.ISBN 
= TextBox_isbn.Text;
        bo.Quantity 
= Convert.ToInt32(TextBox_quantity.Text);
        bo.FirstName 
= TextBox_firstname.Text;
        bo.LastName 
= TextBox_lastname.Text;
        bo.Address 
= TextBox_address.Text;

        
try
        {
            Label_Progress.Text 
= svc.InitiateOrder();  //进度显示「开始处理订单...」    

            ServiceReference1.BookOrder reply 
= ((ServiceReference1.IService)svc).PlaceOrder(bo);
            TextBox_ordernumber.Text 
= reply.OrderNumber;   //Server-side 系统给的订单编号

            Label_Progress.Text 
= svc.FinalizeOrder();  //进度显示「您的订单已订购成功!」
        }
        
catch (Exception ex)
        {
            Label_Progress.Text 
= "订购过程发生错误!<br>" + ex.Message;
        }
    }
}

 


图 4 执行画面,订单提交成功的画面


此示例中,用户在表单里填写的信息,会借由「消息头 (MessageHeader)」和「消息体 (MessageBodyMember)」传送。提交后,系统会返回一个订单编号。若提交成功,最后会有反馈信息,可让用户知道已正确提交了订单。

此例还可加入其他应用,提交后除了返回一个订单编号,还可再扩展来完成更多工作,如:验证书籍库存数量是否足够、查找书名信息 ...等。



相关文章

[1] WCF分布式安全开发实践(10):消息安全模式之自定义用户名密码:Message_UserNamePassword_WSHttpBinding
http://www.cnblogs.com/frank_xl/archive/2009/08/12/1543867.html

[2] WCF问题(10):如何配置不启用安全的WCF服务?
http://social.microsoft.com/Forums/zh-CN/wcfzhchs/thread/e5d6e169-bb86-43df-8b13-9d4bead33cba
http://social.microsoft.com/Forums/zh-CN/wcfzhchs/thread/2f95f41c-430d-495a-b55b-245922fc63fe

[3] WCF 4 安全性和 WIF 简介
http://www.cnblogs.com/WizardWu/archive/2010/10/04/1841793.html

[4] Security - SSL (繁体中文)
http://sites.google.com/site/stevenattw/dot-net/wcf/security-SSL

[5] MessageContractAttribute 类
http://msdn.microsoft.com/zh-cn/library/system.servicemodel.messagecontractattribute.aspx
http://msdn.microsoft.com/zh-cn/library/ms732038.aspx

 

参考书籍

[6] MCTS 70-503 - Microsoft .NET 3.5 Windows Communication Foundation, Chapter 1
http://www.silverlightchina.net/html/download/books/2009/1110/235.html

[7] Professional WCF Programming, Chapter 6
http://www.wrox.com/WileyCDA/WroxTitle/Professional-WCF-Programming-NET-Development-with-the-Windows-Communication-Foundation.productCd-0470089849.html

 


posted on 2011-01-15 05:46  WizardWu  阅读(4195)  评论(7编辑  收藏  举报