WcfService:單服務多契約接口以及用戶名密碼認證

      好久沒寫過博客了,以往寫博客都是直接在網頁上寫的,效率比較低。今天恰好研究了下Windows Live Writer,寫博客要方便了不少。因此打算將這一年來研究的部分紅果發一發。數據庫

      這一段時間主要是研究了服務端開發的框架,包括Web Service、Web API以及WCF,經過VS實現Web Service是最容易的,適合輕量級的Web服務,Web API由於以前用了好久的Asp.net MVC,因此學起來很快,WCF難度最大,框架也較爲複雜。在學習使用WCF的過程當中趟過了不少坑,經過今天這個單服務多契約接口及用戶名密碼認證的實例來給本身作個背書吧。安全

項目結構

      以下圖所示,我構建了一個名爲「ywt.WcfService」的解決方案,並在其下創建了四個項目:框架

1

      各項目的名稱及功能描述以下表:async

名稱 類型 功能描述
ywt.WcfService.Interfaces 類庫項目 包含全部的WcfService契約接,爲簡單期間,我僅作了2個契約接口
ywt.WcfService.SelfHost 控制檯應用程序項目 對WcfService服務進行自寄宿,相關的服務配置都在該項目的App.Config文件中
ywt.WcfService.Service 類庫項目 服務實現代碼
ywt.WcfService.WinFormClient WinForm應用程序項目 Winform客戶端程序

ywt.WcfService.Interfaces接口項目

      該項須要引用System.ServiceModel和System.Runtime.Serialization。包含2個接口的定義:ICalculator.cs、ILog.cs,作爲演示,代碼很是簡單。代碼分別以下所示:ide

ICalculator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Text;
using System.Threading.Tasks;

namespace ywt.WcfService.Interfaces
{
    [ServiceContract]
    public interface ICalculator
    {
        [OperationContract]
        double Add(double param1, double param2);
    }
}

ILog.cs學習

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Text;
using System.Threading.Tasks;

namespace ywt.WcfService.Interfaces
{
    [ServiceContract]
    public interface ILog
    {
        [OperationContract]
        string Log(string text);
    }
}

ywt.WcfService.Service服務實現項目

      該項須要引用System.ServiceModel和System.Runtime.Serialization,另外還須要引用ywt.WcfService.Interfaces接口項目。在該項目中須要實現接口項目中定義的全部接口,咱們能夠經過一個服務來實現全部的接口。一個服務實現全部的接口時,可能會出現服務代碼過於臃腫,不便於查看維護,咱們能夠將咱們的服務實現類拆分紅多個部分類,併爲各個部分類的文件名(注意是文件名而不是類名)取一個對應的名稱。spa

      我作了2個類,文件名分別爲CalculatorService.cs和LogService.cs,2個文件中分別用於實現不一樣的接口。2個類文件的代碼以下:操作系統

CalculatorService.cs.net

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization;
using System.ServiceModel;
using ywt.WcfService.Interfaces;

namespace ywt.WcfService.Services
{
    public partial class MyService : ICalculator
    {
        public double Add(double param1, double param2)
        {
            return param1+param2;
        }
    }
}

LogService.cs3d

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ywt.WcfService.Interfaces;

namespace ywt.WcfService.Services
{
    public partial class MyService : ILog
    {
        public string Log(string text)
        {
            return $"ServiceLog: {text}";
        }
    }
}

ywt.WcfService.SelfHost服務寄宿控制檯項目

      該項須要引用ywt.WcfService.Interfaces、ywt.WcfService.Service兩個項目,以及System.ServiceModel、System.Runtime.Serialization、System.IdentityModel和System.IdentityModel.Selectors。我採用是消息安全模式,客戶端認證方式是用戶名密碼,此時服務端必須設置證書,爲了省去建立證書這一步,我直接使用服務端的本機localhost證書,該證書在服務端安裝操做系統時自動建立。

      根據以上狀況,在本項目中須要作三件事情:

  1. 在Program.cs中實現服務寄宿的代碼(另外我在這個環節中經過代碼設置了證書,其實也能夠經過配置來實現)。
  2. 建立自定義的UserNamePasswordValidator,用於驗證用戶名和密碼,實際應用中須要讀取數據庫,我這裏直接進行的靜態比較。
  3. 設置配置文件

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Security.Cryptography.X509Certificates;
using ywt.WcfService.Services;

namespace ywt.WcfService.SeftHost
{
    class Program
    {
        private static ServiceHost host;

        static void Main(string[] args)
        {
            Console.WriteLine("Wcf服務開始啓動");
            try
            {
                host = new ServiceHost(typeof(MyService));
                ServiceCredentials scs = host.Description.Behaviors.Find<ServiceCredentials>();
                if (scs == null)
                {
                    scs = new ServiceCredentials();
                    host.Description.Behaviors.Add(scs);
                }
                scs.ServiceCertificate.SetCertificate("CN=localhost",StoreLocation.LocalMachine,StoreName.My);
                host.Open();
                Console.WriteLine("Wcf服務啓動成功");
                Console.ReadKey();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Wcf服務啓動失敗: {ex.Message}");
                Console.ReadKey();
            }
            finally
            {
                host.Close();
            }            
        }
    }
}

MyUserNamePasswordValidator.cs

using System;
using System.Collections.Generic;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ywt.WcfService.SeftHost
{
    public class MyUserNamePasswordValidator : UserNamePasswordValidator
    {
        public override void Validate(string userName, string password)
        {
            if (userName != "admin" || password != "admin")
            {
                throw new SecurityTokenValidationException("用戶未得到受權");
            }
        }
    }
}

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
    <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="msgUserNameHttp">
                    <security>
                        <message clientCredentialType="UserName" negotiateServiceCredential="true"/>
                    </security>
                </binding>
            </wsHttpBinding>
        </bindings>
        <services>
            <service behaviorConfiguration="behavior1" name="ywt.WcfService.Services.MyService">
                <endpoint name="Calculator" address="" binding="wsHttpBinding" bindingConfiguration="msgUserNameHttp"
                    contract="ywt.WcfService.Interfaces.ICalculator" />
                <endpoint name="Log" address="" binding="wsHttpBinding" bindingConfiguration="msgUserNameHttp" contract="ywt.WcfService.Interfaces.ILog" />
                <host>
                    <baseAddresses>
                        <add baseAddress="http://127.0.0.1:9876/MyService" />
                    </baseAddresses>
                </host>
            </service>
        </services>
        <behaviors>
            <serviceBehaviors>
                <behavior name="behavior1">
                    <serviceMetadata httpGetEnabled="true" httpGetUrl="http://127.0.0.1:9876/MyService/MEX" />
                    <serviceCredentials>
                        <userNameAuthentication userNamePasswordValidationMode="Custom"
                                                customUserNamePasswordValidatorType="ywt.WcfService.SeftHost.MyUserNamePasswordValidator,ywt.WcfService.SeftHost"/>
                    </serviceCredentials>
                </behavior>
            </serviceBehaviors>
        </behaviors>
    </system.serviceModel>
</configuration>

ywt.WcfService.WinFormClient客戶端項目

      該項須要引用System.ServiceModel、System.Runtime.Serialization,調用Wcf服務咱們採用引用代理。首先得將ywt.WcfService.SelfHost運行起來,注意不能直接在VS中直接調試運行,須要編譯該項目後,找到生成的exe程序啓動。隨後咱們能夠爲當前的客戶端添加服務引用:

2

      在地址中錄入正確的服務地址,而後點擊轉到,在服務列表框中咱們能夠看到咱們的MyService,其下包含了2個咱們定義的接口。在命名空間中錄入自定義的命名空間文本,假如在此處錄入了WcfServices,那麼實際最後完整的命名空間完整路徑是:ywt.WcfService.WinFormClient.WcfServices。也就是說此處的命名空間不須要寫成徹底的,VS會自動補全,將當前客戶端項目的命名空間加在前面。

      客戶端僅添加了一個窗體,窗體上放置了2個文本框、2個Lable以及一個按鈕控件。界面以下所示:

3

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.ServiceModel.Security;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using ywt.WcfService.WinFormClient.WcfServices;

namespace ywt.WcfService.WinFormClient
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {            
            CalculatorClient calculator = new CalculatorClient();
            UserNamePasswordClientCredential credential = calculator.ClientCredentials.UserName;
            credential.UserName = "admin";
            credential.Password = "admin";
            LogClient log = new LogClient();
            double p1, p2;
            double.TryParse(textBox1.Text, out p1);
            double.TryParse(textBox2.Text, out p2);
            p1=await calculator.AddAsync(p1, p2);
            label1.Text= p1.ToString();

            credential = log.ClientCredentials.UserName;
            credential.UserName = "admin";
            credential.Password = "admin";
            label2.Text= await log.LogAsync(label1.Text);
        }
    }
}

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
    <system.serviceModel>
        <behaviors>
            <endpointBehaviors>
                <behavior name="NewBehavior0">
                    <clientCredentials>
                        <serviceCertificate>
                            <authentication certificateValidationMode="None" revocationMode="NoCheck" />
                        </serviceCertificate>
                    </clientCredentials>
                </behavior>
            </endpointBehaviors>
        </behaviors>
        <bindings>
            <wsHttpBinding>
                <binding name="Calculator">
                    <security>
                        <message clientCredentialType="UserName" />
                    </security>
                </binding>
                <binding name="Log">
                    <security>
                        <message clientCredentialType="UserName" />
                    </security>
                </binding>
            </wsHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://127.0.0.1:9876/MyService" binding="wsHttpBinding"
                bindingConfiguration="Calculator" contract="WcfServices.ICalculator" behaviorConfiguration="NewBehavior0"
                name="Calculator">
                <identity>
                    <certificate encodedValue="AwAAAAEAAAAUAAAAemLPWHcq5CeL/jln/1OjQSeKL/QgAAAAAQAAAPACAAAwggLsMIIB1KADAgECAhAdf5gB+4wxqEgNTFZJZ+SUMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xODA0MTkxMzIzMTZaFw0yMzA0MTkwMDAwMDBaMBQxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1T6iuo0M8fsCuazkDkD/sPPICeJrabdoq1B/0/xIKSNV+IFPnncPdefefh6ZOdyaPXN9yF+/v6GIveLEseQ8w0oXC5l+Eyl7kXTc3xzysLaKL/rFtjUH91+6qKjE9un5C+bVp884zQnOKhqDXxiqn6Aoem2kjAWbo0244weA2VE5kQZHAEsd2PrZpcy8gLptmtPc5Kqp1UuyVRmdTkmm2HZD3GQmgmASf5LUtgTAtcxLEjAQ4dtzyoBPnAL8meR6mgbj/JKOXutyY/QRxxfYun+sBDIJArL3tBnKQTBHJxCLuU8j0dSGYCfCyvaMNgXQWL1G4SjG9LAKQkj3c+LkcCAwEAAaM6MDgwCwYDVR0PBAQDAgSwMBMGA1UdJQQMMAoGCCsGAQUFBwMBMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAVPCHXEqNMnsZ5uCQ0y7iNvR9aQgZTGmWdn/c2GH39VqJ1bJpToTZm5SQeJYCLUW2f0bDq1JbLWSaRG8c9PREvYsIlOtrJdyhxplBOkdcs+zyx2BQC7tlCWaoDjGS1SEVAu48NrspktB6rh3KOjuoxcr5vWO1G76zYaSAQ2At/5+VIINxkg8/tk6JF3wEq63qdrRgVUCru0Yi0cVU0UViVPVWl61LrrERenRHT1YhldwwpPDQC38qLnE6YREQzEzEHEzoeBWU1dj65/X5b53v6B7jqm5cXhuAvZZMt8Kvo1HzWVwHDmOD3VMoEPR3aXCjXZ5WK9AHXsOrH3SKjPsXIQ==" />
                </identity>
            </endpoint>
            <endpoint address="http://127.0.0.1:9876/MyService" binding="wsHttpBinding"
                bindingConfiguration="Log" contract="WcfServices.ILog" behaviorConfiguration="NewBehavior0" name="Log">
                <identity>
                    <certificate encodedValue="AwAAAAEAAAAUAAAAemLPWHcq5CeL/jln/1OjQSeKL/QgAAAAAQAAAPACAAAwggLsMIIB1KADAgECAhAdf5gB+4wxqEgNTFZJZ+SUMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xODA0MTkxMzIzMTZaFw0yMzA0MTkwMDAwMDBaMBQxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1T6iuo0M8fsCuazkDkD/sPPICeJrabdoq1B/0/xIKSNV+IFPnncPdefefh6ZOdyaPXN9yF+/v6GIveLEseQ8w0oXC5l+Eyl7kXTc3xzysLaKL/rFtjUH91+6qKjE9un5C+bVp884zQnOKhqDXxiqn6Aoem2kjAWbo0244weA2VE5kQZHAEsd2PrZpcy8gLptmtPc5Kqp1UuyVRmdTkmm2HZD3GQmgmASf5LUtgTAtcxLEjAQ4dtzyoBPnAL8meR6mgbj/JKOXutyY/QRxxfYun+sBDIJArL3tBnKQTBHJxCLuU8j0dSGYCfCyvaMNgXQWL1G4SjG9LAKQkj3c+LkcCAwEAAaM6MDgwCwYDVR0PBAQDAgSwMBMGA1UdJQQMMAoGCCsGAQUFBwMBMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAVPCHXEqNMnsZ5uCQ0y7iNvR9aQgZTGmWdn/c2GH39VqJ1bJpToTZm5SQeJYCLUW2f0bDq1JbLWSaRG8c9PREvYsIlOtrJdyhxplBOkdcs+zyx2BQC7tlCWaoDjGS1SEVAu48NrspktB6rh3KOjuoxcr5vWO1G76zYaSAQ2At/5+VIINxkg8/tk6JF3wEq63qdrRgVUCru0Yi0cVU0UViVPVWl61LrrERenRHT1YhldwwpPDQC38qLnE6YREQzEzEHEzoeBWU1dj65/X5b53v6B7jqm5cXhuAvZZMt8Kvo1HzWVwHDmOD3VMoEPR3aXCjXZ5WK9AHXsOrH3SKjPsXIQ==" />
                </identity>
            </endpoint>
        </client>
    </system.serviceModel>
</configuration>

運行效果

4     5

提出問題

      經過以上客戶端調用Wcf服務時能夠看到,每當調用一個契約的本地代理時,都得傳遞用戶名和密碼,這實在是坑爹的辦法,由於實際的項目中,一個服務中可能會須要實現無數個契約接口,若是每用一次契約就得傳一次用戶名密碼,真是讓人沒法忍受。因此如今就有了一個新的問題,如何只需傳遞一次用戶名和密碼?網上有將用戶名和密碼寫入SOAP消息頭的作法,可是這種方法並不推薦。推薦的方式是作一個契約專門完成登陸退出,在該契約的Login方法中咱們爲完成正確登陸的用戶分發一個令牌(由服務端生成的具備必定時效的字符串),而後將該令牌寫入SOAP消息頭,隨後客戶端和服務端的通訊認證都由這個令牌來識別。這種模式我在Web Service、Web API裏都搞過,可是WCF還沒試過實現。因此也不提供代碼了。

相關文章
相關標籤/搜索