實(shí)作最簡(jiǎn)單的 WCF 懶人驗(yàn)證方式,透過(guò) SoapHeader 或 MessageContract 傳送用戶名、密碼,以調(diào)用 WCF 服務(wù)。本帖最后,還附一個(gè)利用 MessageContract 處理表單提交的示例。
-------------------------------------------------
本帖的示例下載點(diǎn):
https://files.cnblogs.com/WizardWu/101115.zip
(執(zhí)行本示例,需要 Visual Studio 2008,不需要數(shù)據(jù)庫(kù))
---------------------------------------------------
我們都知道,WCF 有一套很完整的安全機(jī)制,例如可透過(guò) SSL 保障傳輸層的安全、透過(guò) Federation 加密和數(shù)位簽章來(lái)保護(hù)信息本身、使用 Certificate 以跨域或跨平臺(tái)傳送憑證,甚至 WCF 4 還支持新的 WIF 框架以實(shí)現(xiàn) SSO (Single Sign On) 單點(diǎn)登錄 [3]。
但若我們?cè)谄髽I(yè)的 Intranet 或局域網(wǎng)中,沒(méi)有安全顧慮時(shí) (信任自己公司或部門(mén)沒(méi)有那么無(wú)聊的員工),只想用最簡(jiǎn)單的明文 username、password 驗(yàn)證 (安全后果自負(fù)),難道也非得創(chuàng)建:證書(shū)、SSL、…及一堆安全性的配置,才能調(diào)用 ClientCredentials.UserName.UserName、ClientCredentials.UserName.Password 這些 API 嗎?
我以前曾在論壇問(wèn)過(guò)此問(wèn)題 (聽(tīng)說(shuō)問(wèn)過(guò)相同問(wèn)題的人很多):
WCF 用戶名、密碼驗(yàn)證的設(shè)置問(wèn)題:
http://topic.csdn.net/u/20101024/18/1e491713-46a1-412d-859b-453e49dad672.html?seed=181740069&r=69308273
http://space.cnblogs.com/q/19235/
結(jié)論:最簡(jiǎn)單的做法,是不使用 ClientCredentials,直接把 username、password 添加到 SOAP Header 中,來(lái)實(shí)現(xiàn)身份驗(yàn)證。有興趣的人可參考:
WCF進(jìn)階:為每個(gè)操作附加身份信息
http://www.rzrgm.cn/jillzhang/archive/2010/04/11/1709397.html
在傳統(tǒng)的 Web Service 也有同樣的做法,可參考以下這篇文章。若 Web Service 有安全性顧慮的話,可使用 WSE (Web Services Enhancements) 來(lái)加強(qiáng)安全性。
簡(jiǎn)單的 WebService 安全:
http://www.rzrgm.cn/kuyijie/archive/2011/01/08/1930795.html
| SOAP Header - 懶人驗(yàn)證法 |
前述 jillzhang 他用 SOAP Header 傳遞用戶名、密碼的做法:
Server-side :
using System.ServiceModel;
[ServiceContract]
public interface IService
{
[OperationContract]
string getData(int value);
}
Service.cs
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> 帳號(hào): " + username + ", 密碼: " + pwd);
}
}
Client-side :
Default.aspx.cs
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 執(zhí)行畫(huà)面
| MessageContract 實(shí)際應(yīng)用 1 - 懶人驗(yàn)證法 |
同樣的身份驗(yàn)證功能,我們也可透過(guò) MessageContract 類 [5] 來(lái)處理,將其包裝得更漂亮,亦即可指定 SOAP envelope 的結(jié)構(gòu),然后就可用 message 作為參數(shù)或返回類型,并且在不修改默認(rèn) SOAP envelope 本身的情況下,來(lái)控制 SOAP body 內(nèi)容序列化的信息。
MessageContract (消息契約) 對(duì)格式化 SOAP 消息,提供了有效的控制。雖然在許多情況下,DataContract (數(shù)據(jù)契約) 已提供了用戶需要的大部分控制,但若需要更靈活地控制,可再加上 MessageContract 來(lái)輔助處理,如:允許用戶將指定的類型映射到 SOAP 消息,并添加定制的 SOAP Header [7]。
以下為 MCTS 70-503 WCF 認(rèn)證,微軟官方教程書(shū)籍 [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 并透過(guò)其可「包裝」的特性,我們可包裝欲傳遞的用戶名、密碼,以及包裝從服務(wù)器返回的信息,如下代碼的 ContactInfoRequestMessage、ContactInfoResponseMessage 自定義類 (將這些類、接口都寫(xiě)在同一個(gè) .cs 文件里,是不好的設(shè)計(jì)模式,這里只是為了演示方便):
Server-side :
IService.cs
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
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 = "用戶名、密碼輸入錯(cuò)誤~~";
throw new FaultException<string>(msg);
}
//返回信息。
//透過(guò) MessageContract 包裝 ContactInfoResponseMessage 自定義類及其成員,
//并包裝真正存儲(chǔ)數(shù)據(jù)的 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
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
ServiceReference1.ServiceClient svc = new ServiceReference1.ServiceClient();
try
{
//透過(guò) MessageContract 包裝 ContactInfoRequestMessage 自定義類及其成員,讓使用者輸入用戶名、密碼
ServiceReference1.ContactInfo ci = svc.GetProviderContactInfo("WizardWu", "123456");
Response.Write("電子郵件:" + ci.EmailAddress + "<br> 電話號(hào)碼:" + ci.PhoneNumber);
}
catch (FaultException<string> ex)
{
Response.Write(ex.Detail); //印出「用戶名、密碼輸入錯(cuò)誤~~」
}
catch (Exception ex)
{
Response.Write(ex.ToString());
}
}
}

圖 2 執(zhí)行畫(huà)面,輸入了正確的用戶名、密碼,便可從 Server-side 取得想要的信息 (郵件、電話號(hào)碼)

圖 3 執(zhí)行畫(huà)面,輸入了錯(cuò)誤的用戶名、密碼
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 實(shí)際應(yīng)用 2 - 訂單提交 |
接下來(lái)我們看 MessageContract 在實(shí)務(wù)上的另一種應(yīng)用 - 網(wǎng)上書(shū)店的訂單處理 [7]。本例中,定義了包含訂書(shū)信息的 MessageContract,其會(huì)在 Server-side 和 Client-side 之間傳遞。如下 IService.cs 的代碼,這個(gè) BookOrder 的 message,成員包含一個(gè)消息頭,和五個(gè)消息體。
頭部包含了書(shū)的 ISBN 號(hào),體包含了訂單提交者的信息。訂單提交后,會(huì)將此消息傳遞至 WCF Service,經(jīng)過(guò)處理后,會(huì)把訂單編號(hào) (00012345678) 傳回到客戶端瀏覽器中顯示。
示例的核心,在于 Service.cs 里的 WCF 服務(wù),若系統(tǒng)將來(lái)需要修改 Server-side 邏輯,直接修改即可,不需要重新編譯。若有宿主 (Host) 應(yīng)用程序,也不用修改該個(gè)宿主應(yīng)用程序。
Server-side :
IService.cs
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.ServiceModel;
public class Service : IService
{
string IService.InitiateOrder()
{
return "開(kāi)始處理訂單..."; //進(jìn)度顯示「開(kāi)始處理訂單...」
}
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"; //系統(tǒng)給的訂單編號(hào)
return response;
}
string IService.FinalizeOrder()
{
return "您的訂單已訂購(gòu)成功!"; //進(jìn)度顯示「您的訂單已訂購(gòu)成功!」
}
}
Client-side :
Default.aspx
<body>
<form id="form1" runat="server">
<div>
ISBN: <asp:TextBox ID="TextBox_isbn" runat="server"></asp:TextBox><br />
數(shù)量: <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 />
訂單編號(hào)(由系統(tǒng)自動(dòng)帶入): <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
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(); //進(jìn)度顯示「開(kāi)始處理訂單...」
ServiceReference1.BookOrder reply = ((ServiceReference1.IService)svc).PlaceOrder(bo);
TextBox_ordernumber.Text = reply.OrderNumber; //Server-side 系統(tǒng)給的訂單編號(hào)
Label_Progress.Text = svc.FinalizeOrder(); //進(jìn)度顯示「您的訂單已訂購(gòu)成功!」
}
catch (Exception ex)
{
Label_Progress.Text = "訂購(gòu)過(guò)程發(fā)生錯(cuò)誤!<br>" + ex.Message;
}
}
}

圖 4 執(zhí)行畫(huà)面,訂單提交成功的畫(huà)面
此示例中,用戶在表單里填寫(xiě)的信息,會(huì)借由「消息頭 (MessageHeader)」和「消息體 (MessageBodyMember)」傳送。提交后,系統(tǒng)會(huì)返回一個(gè)訂單編號(hào)。若提交成功,最后會(huì)有反饋信息,可讓用戶知道已正確提交了訂單。
此例還可加入其他應(yīng)用,提交后除了返回一個(gè)訂單編號(hào),還可再擴(kuò)展來(lái)完成更多工作,如:驗(yàn)證書(shū)籍庫(kù)存數(shù)量是否足夠、查找書(shū)名信息 ...等。
| 相關(guān)文章 |
[1] WCF分布式安全開(kāi)發(fā)實(shí)踐(10):消息安全模式之自定義用戶名密碼:Message_UserNamePassword_WSHttpBinding
http://www.rzrgm.cn/frank_xl/archive/2009/08/12/1543867.html
[2] WCF問(wèn)題(10):如何配置不啟用安全的WCF服務(wù)?
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 簡(jiǎn)介
http://www.rzrgm.cn/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
| 參考書(shū)籍 |
[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

浙公網(wǎng)安備 33010602011771號(hào)