WcfService:单服务多契约接口以及用户名密码认证
很久没写过博客了,以往写博客都是直接在网页上写的,效率比较低。今天刚好研究了下Windows Live Writer,写博客要方便了很多。所以打算将这一年来研究的部分成果发一发。
这一段时间主要是研究了服务端开发的框架,包括Web Service、Web API以及WCF,通过VS实现Web Service是最容易的,适合轻量级的Web服务,Web API因为之前用了很久的Asp.net MVC,所以学起来很快,WCF难度最大,框架也较为复杂。在学习使用WCF的过程中趟过了很多坑,通过今天这个单服务多契约接口及用户名密码认证的实例来给自己做个背书吧。
项目结构
如下图所示,我构建了一个名为“ywt.WcfService”的解决方案,并在其下建立了四个项目:
各项目的名称及功能描述如下表:
名称 | 类型 | 功能描述 |
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,做为演示,代码非常简单。代码分别如下所示:
ICalculator.csusing 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接口项目。在该项目中需要实现接口项目中定义的所有接口,我们可以通过一个服务来实现所有的接口。一个服务实现所有的接口时,可能会出现服务代码过于臃肿,不便于查看维护,我们可以将我们的服务实现类拆分成多个部分类,并为各个部分类的文件名(注意是文件名而不是类名)取一个对应的名称。
我做了2个类,文件名分别为CalculatorService.cs和LogService.cs,2个文件中分别用于实现不同的接口。2个类文件的代码如下:
CalculatorService.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 ywt.WcfService.Interfaces; namespace ywt.WcfService.Services { public partial class MyService : ICalculator { public double Add(double param1, double param2) { return param1+param2; } } }
LogService.cs
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证书,该证书在服务端安装操作系统时自动创建。
根据以上情况,在本项目中需要做三件事情:
- 在Program.cs中实现服务寄宿的代码(另外我在这个环节中通过代码设置了证书,其实也可以通过配置来实现)。
- 创建自定义的UserNamePasswordValidator,用于验证用户名和密码,实际应用中需要读取数据库,我这里直接进行的静态比较。
- 设置配置文件
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程序启动。随后我们可以为当前的客户端添加服务引用:
在地址中录入正确的服务地址,然后点击转到,在服务列表框中我们可以看到我们的MyService,其下包含了2个我们定义的接口。在命名空间中录入自定义的命名空间文本,假如在此处录入了WcfServices,那么实际最后完整的命名空间完整路径是:ywt.WcfService.WinFormClient.WcfServices。也就是说此处的命名空间不需要写成完全的,VS会自动补全,将当前客户端项目的命名空间加在前面。
客户端仅添加了一个窗体,窗体上放置了2个文本框、2个Lable以及一个按钮控件。界面如下所示:
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>
运行效果
提出问题
通过以上客户端调用Wcf服务时可以看到,每当调用一个契约的本地代理时,都得传递用户名和密码,这实在是坑爹的办法,因为实际的项目中,一个服务中可能会需要实现无数个契约接口,如果每用一次契约就得传一次用户名密码,真是让人无法忍受。所以现在就有了一个新的问题,如何只需传递一次用户名和密码?网上有将用户名和密码写入SOAP消息头的做法,但是这种方法并不推荐。推荐的方式是做一个契约专门完成登录退出,在该契约的Login方法中我们为完成正确登录的用户分发一个令牌(由服务端生成的具有一定时效的字符串),然后将该令牌写入SOAP消息头,随后客户端和服务端的通信认证都由这个令牌来识别。这种模式我在Web Service、Web API里都搞过,但是WCF还没试过实现。所以也不提供代码了。