zoukankan      html  css  js  c++  java
  • 关于WEB Service&WCF&WebApi实现身份验证之WCF篇(1)

    WCF身份验证一般常见的方式有:自定义用户名及密码验证、X509证书验证、ASP.NET成员资格(membership)验证、SOAP Header验证、Windows集成验证、WCF身份验证服务(AuthenticationService),这些验证方式其实网上都有相关的介绍文章,我这里算是一个总结吧,顺便对于一些注意细节进行说明,以便大家能更好的掌握这些知识。

    第一种:自定义用户名及密码验证(需要借助X509证书)

    由于该验证需要借助于X509证书,所以我们需要先创建一个证书,可以利用MS自带的makecert.exe程序来制作测试用证书,使用步骤:请依次打开开始->Microsoft Visual Studio 2010(VS菜单,版本不同,名称有所不同)->Visual Studio Tools->Visual Studio 命令提示,然后执行以下命令:

    makecert -r -pe -n "CN=ZwjCert" -ss TrustedPeople -sr LocalMachine -sky exchange

    上述命令中除了我标粗的部份可改成你实际的请求外(为证书名称),其余的均可以保持不变,命令的意思是:创建一个名为ZwjCert的证书将将其加入到本地计算机的受信任人区域中。

    如果需要查看该证书,那么可以通过MMC控制台查询证书,具体操作步骤如下:

    运行->MMC,第一次打开Windows没有给我们准备好直接的管理证书的入口,需要自行添加,添加方法如下:

    1. 在控制台菜单,文件→添加/删除管理单元→添加按钮→选”证书”→添加→选”我的用户账户”→关闭→确定
    2. 在控制台菜单,文件→添加/删除管理单元→添加按钮→选”证书”→添加→选”计算机账户”→关闭→确定

    这样MMC中左边就有菜单了,然后依次展开:证书(本地计算机)->受信任人->证书,最后就可以在右边的证书列表中看到自己的证书了,如下图示:

    证书创建好,我们就可以开始编码了,本文主要讲的就是WCF,所以我们首先定义一个WCF服务契约及服务实现类(后面的各种验证均采用该WCF服务),我这里直接采用默认的代码,如下:

    namespace WcfAuthentications
    {
        [ServiceContract]
        public interface IService1
        {
            [OperationContract]
            string GetData(int value);
    
            [OperationContract]
            CompositeType GetDataUsingDataContract(CompositeType composite);
    
        }
    
        [DataContract]
        public class CompositeType
        {
            bool boolValue = true;
            string stringValue = "Hello ";
    
            [DataMember]
            public bool BoolValue
            {
                get { return boolValue; }
                set { boolValue = value; }
            }
    
            [DataMember]
            public string StringValue
            {
                get { return stringValue; }
                set { stringValue = value; }
            }
        }
    }
    
    
    namespace WcfAuthentications
    {
        public class Service1 : IService1
        {
            public string GetData(int value)
            {
                return string.Format("You entered: {0}", value);
            }
    
            public CompositeType GetDataUsingDataContract(CompositeType composite)
            {
                if (composite == null)
                {
                    throw new ArgumentNullException("composite");
                }
                if (composite.BoolValue)
                {
                    composite.StringValue += "Suffix";
                }
                return composite;
            }
        }
    }

    要实现用户名及密码验证,就需要定义一个继承自UserNamePasswordValidator的用户名及密码验证器类CustomUserNameValidator,代码如下:

    namespace WcfAuthentications
    {
        public class CustomUserNameValidator : UserNamePasswordValidator
        {
    
            public override void Validate(string userName, string password)
            {
                if (null == userName || null == password)
                {
                    throw new ArgumentNullException();
                }
                if (userName != "admin" && password != "wcf.admin") //这里可依实际情况下实现用户名及密码判断
                {
                    throw new System.IdentityModel.Tokens.SecurityTokenException("Unknown Username or Password");
                }
    
            }
        }
    }
    

    代码很简单,只是重写其Validate方法,下面就是将创建WCF宿主,我这里采用控制台程序

    代码部份:

    namespace WcfHost
    {
        class Program
        {
            static void Main(string[] args)
            {
                using (var host = new ServiceHost(typeof(Service1)))
                {
                    host.Opened += delegate
                    {
                        Console.WriteLine("Service1 Host已开启!");
                    };
                    host.Open();
                    Console.ReadKey();
                }
            }
        }
    }
    

    APP.CONFIG部份(这是重点,可以使用WCF配置工具来进行可视化操作配置,参见:http://www.cnblogs.com/Moosdau/archive/2011/04/17/2019002.html):

      <system.serviceModel>
        <bindings>
          <wsHttpBinding>
            <binding name="Service1Binding">
              <security mode="Message">
                <message clientCredentialType="UserName" />
              </security>
            </binding>
          </wsHttpBinding>
        </bindings>
        <services>
          <service behaviorConfiguration="Service1Behavior" name="WcfAuthentications.Service1">
            <endpoint address="" binding="wsHttpBinding" bindingConfiguration="Service1Binding"
              contract="WcfAuthentications.IService1">
              <identity>
                <dns value="ZwjCert" />
              </identity>
            </endpoint>
            <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
            <host>
              <baseAddresses>
                <add baseAddress="http://localhost:8732/WcfAuthentications/Service1/" />
              </baseAddresses>
            </host>
          </service>
        </services>
        <behaviors>
          <serviceBehaviors>
            <behavior name="Service1Behavior">
              <serviceMetadata httpGetEnabled="true" />
              <serviceDebug includeExceptionDetailInFaults="false" />
              <serviceCredentials>
                <serviceCertificate findValue="ZwjCert" x509FindType="FindBySubjectName" storeLocation="LocalMachine" storeName="TrustedPeople" />
                <userNameAuthentication userNamePasswordValidationMode="Custom"
                  customUserNamePasswordValidatorType="WcfAuthentications.CustomUserNameValidator,WcfAuthentications" />
              </serviceCredentials>
            </behavior>
          </serviceBehaviors>
        </behaviors>
      </system.serviceModel>
    

    这里面有几个需要注意的点:

    1.<dns value="ZwjCert" />与<serviceCertificate findValue="ZwjCert" ..>中的value必需都为证书的名称,即:ZwjCert;

    2.Binding节点中需配置security节点,message子节点中的clientCredentialType必需设为:UserName;

    3.serviceBehavior节点中,需配置serviceCredentials子节点,其中serviceCertificate 中各属性均需与证书相匹配,userNameAuthentication的userNamePasswordValidationMode必需为Custom,customUserNamePasswordValidatorType为上面自定义的用户名及密码验证器类的类型及其程序集

    最后就是在客户端使用了,先引用服务,然后看下App.Config,并进行适当的修改,如下:

        <system.serviceModel>
            <bindings>
                <wsHttpBinding>
                    <binding name="WSHttpBinding_IService1" >
                        <security mode="Message">
                            <transport clientCredentialType="Windows" proxyCredentialType="None"
                                realm="" />
                            <message clientCredentialType="UserName" negotiateServiceCredential="true"
                                algorithmSuite="Default" />
                        </security>
                    </binding>
                </wsHttpBinding>
            </bindings>
            <client>
                <endpoint address="http://localhost:8732/WcfAuthentications/Service1/"
                    binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1"
                    contract="ServiceReference1.IService1" name="WSHttpBinding_IService1">
                    <identity>
                        <dns value="ZwjCert" />
                    </identity>
                </endpoint>
            </client>
        </system.serviceModel>
    

    为了突出重点,我这里对Binding节点进行了精简,去掉了许多的属性配置,仅保留重要的部份,如:security节点,修改其endpoint下面的identity中<dns value="ZwjCert" />,这里的value与服务中所说的相同节点相同,就是证书名称,如果不相同,那么就会报错,具体的错误消息大家可以自行试下,我这里限于篇幅内容就不贴出来了。

    客户端使用服务代码如下:

    namespace WCFClient
    {
        class Program
        {
            static void Main(string[] args)
            {
                using (var proxy = new ServiceReference1.Service1Client())
                {
                    proxy.ClientCredentials.UserName.UserName = "admin";
                    proxy.ClientCredentials.UserName.Password = "wcf.admin";
                    string result = proxy.GetData(1);
                    Console.WriteLine(result);
                    var compositeObj = proxy.GetDataUsingDataContract(new CompositeType() { BoolValue = true, StringValue = "test" });
                    Console.WriteLine(SerializerToJson(compositeObj));
                }
                Console.ReadKey();
            }
    
            /// <summary>
            /// 序列化成JSON字符串
            /// </summary>
            static string SerializerToJson<T>(T obj) where T:class
            {
                var serializer = new DataContractJsonSerializer(typeof(T));
                var stream = new MemoryStream();
                serializer.WriteObject(stream,obj);
    
                byte[] dataBytes = new byte[stream.Length];
    
                stream.Position = 0;
                stream.Read(dataBytes, 0, (int)stream.Length);
                string dataString = Encoding.UTF8.GetString(dataBytes);
                return dataString;
            }
        }
    }
    

    运行结果如下图示:

      

    如果不传入用户名及密码或传入不正确的用户名及密码,均会报错:

    第二种:X509证书验证


    首先创建一个证书,我这里就用上面创建的一个证书:ZwjCert;由于服务器端及客户端均需要用到该证书,所以需要导出证书,在客户端的电脑上导入该证书,以便WCF可进行验证。

    WCF服务契约及服务实现类与第一种方法相同,不再重贴代码。

    WCF服务器配置如下:

      <system.serviceModel>
        <bindings>
          <wsHttpBinding>
            <binding name="Service1Binding">
              <security mode="Message">
                <message clientCredentialType="Certificate" />
              </security>
            </binding>
          </wsHttpBinding>
        </bindings>
        <services>
          <service behaviorConfiguration="Service1Behavior" name="WcfAuthentications.Service1">
            <endpoint address="" binding="wsHttpBinding" bindingConfiguration="Service1Binding"
              contract="WcfAuthentications.IService1">
            </endpoint>
            <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
            <host>
              <baseAddresses>
                <add baseAddress="http://127.0.0.1:8732/WcfAuthentications/Service1/" />
              </baseAddresses>
            </host>
          </service>
        </services>
        <behaviors>
          <serviceBehaviors>
            <behavior name="Service1Behavior">
              <serviceMetadata httpGetEnabled="true" />
              <serviceDebug includeExceptionDetailInFaults="false" />
              <serviceCredentials>
                <serviceCertificate findValue="ZwjCert" x509FindType="FindBySubjectName" storeLocation="LocalMachine" storeName="TrustedPeople" />
                <clientCertificate>
                  <authentication certificateValidationMode="None"/>
                </clientCertificate>
              </serviceCredentials>
            </behavior>
          </serviceBehaviors>
        </behaviors>
      </system.serviceModel>
    

    这里需注意如下几点:

    1.<message clientCredentialType="Certificate" />clientCredentialType设为:Certificate;

    2.需配置serviceCredentials节点,其中serviceCertificate 中各属性均需与证书相匹配,clientCertificate里面我将authentication.certificateValidationMode="None",不设置采用默认值其实也可以;

    客户端引用服务,自动生成如下配置信息:

        <system.serviceModel>
            <bindings>
                <wsHttpBinding>
                    <binding name="WSHttpBinding_IService1">
                        <security mode="Message">
                            <transport clientCredentialType="Windows" proxyCredentialType="None"
                                realm="" />
                            <message clientCredentialType="Certificate" negotiateServiceCredential="true"
                                algorithmSuite="Default" />
                        </security>
                    </binding>
                </wsHttpBinding>
            </bindings>
            <client>
                <endpoint address="http://127.0.0.1:8732/WcfAuthentications/Service1/"
                    binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1"
                    contract="ServiceReference1.IService1" name="WSHttpBinding_IService1" behaviorConfiguration="Service1Nehavior">
                    <identity>
                        <certificate encodedValue="AwAAAAEAAAAUAAAAkk2avjNCItzUlS2+Xj66ZA2HBZYgAAAAAQAAAOwBAAAwggHoMIIBVaADAgECAhAIAOzFvLxLuUhHJRwHUUh9MAkGBSsOAwIdBQAwEjEQMA4GA1UEAxMHWndqQ2VydDAeFw0xNTEyMDUwMjUyMTRaFw0zOTEyMzEyMzU5NTlaMBIxEDAOBgNVBAMTB1p3akNlcnQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALfGfsiYpIVKu3gPJl790L13+CZWt6doePZHmcjMl+xPQKIR2fDvsCq9ZxzapDgiG4T3mgcVKUv55DBiuHcpXDvXt28m49AjdKwp924bOGKPM56eweKDCzYfLxy5SxaZfA9qjUhnPq3kGu1lfWjXbsp1rKI1UhKJg5b2j0V7AOC3AgMBAAGjRzBFMEMGA1UdAQQ8MDqAEH/MEXV8FHNLtxvllQ5SMbihFDASMRAwDgYDVQQDEwdad2pDZXJ0ghAIAOzFvLxLuUhHJRwHUUh9MAkGBSsOAwIdBQADgYEAdBtBNTK/Aj3woH2ts6FIU3nh7FB2tKQ9L3k6QVL+kCR9mHuqWtYFJTBKxzESN2t0If6muiktcO+C8iNwYpJpPzLAOMFMrTQhkO82gcdr9brQzMWPTraK1IS+GGH8QBIOTLx9zfV/iCIXxRub+Sq9dmRSQjKDeLeHWoE5I6FkQJg=" />
                    </identity>
                </endpoint>
            </client>
          <behaviors>
            <endpointBehaviors>
              <behavior name="Service1Nehavior">
                <clientCredentials>
                  <clientCertificate findValue="ZwjCert" x509FindType="FindBySubjectName" storeLocation="LocalMachine" storeName="TrustedPeople" />
                </clientCredentials>
              </behavior>
            </endpointBehaviors>
          </behaviors>
        </system.serviceModel>
    

    可以看出endpoint节点下的identity.certificate的encodedValue包含了加密的数据,另外需要手动增加clientCertificate配置信息,该信息表示证书在本地电脑存放的位置,当然也可以通过代码来动态指定,如:proxy.ClientCredentials.ClientCertificate.SetCertificate("ZwjCert", StoreLocation.LocalMachine, StoreName.My);

    客户端使用服务代码如下:

            static void Main(string[] args)
            {
                using (var proxy = new ServiceReference1.Service1Client())
                {
    
                    //proxy.ClientCredentials.ClientCertificate.SetCertificate("ZwjCert", StoreLocation.LocalMachine, StoreName.My); //直接动态指定证书存储位置
                    string result = proxy.GetData(1);
                    Console.WriteLine(result);
                    var compositeObj = proxy.GetDataUsingDataContract(new CompositeType() { BoolValue = true, StringValue = "test" });
                    Console.WriteLine(SerializerToJson(compositeObj));
                }
                Console.ReadKey();
            }
    

    网上还有另类的针对X509证书验证,主要是采用了自定义的证书验证器类,有兴趣的可以参见这篇文章:http://www.cnblogs.com/ejiyuan/archive/2010/05/31/1748363.html

    第三种:ASP.NET成员资格(membership)验证

     由于该验证需要借助于X509证书,所以仍然需要创建一个证书(方法如第一种中创建证书方法相同):ZwjCert;

    由于该种验证方法是基于ASP.NET的membership,所以需要创建相应的数据库及创建账号,创建数据库,请通过运行aspnet_regsql.exe向导来创建数据库及其相关的表,通过打开ASP.NET 网站管理工具(是一个自带的管理网站),并在上面创建角色及用户,用于后续的验证;

    这里特别说明一下,若采用VS2013,VS上是没有自带的GUI按钮来启动该管理工具网站,需要通过如下命令来动态编译该网站:

    cd C:Program FilesIIS Express
    iisexpress.exe /path:C:WindowsMicrosoft.NETFrameworkv4.0.30319ASP.NETWebAdminFiles /vpath:/WebAdmin /port:12345 /clr:4.0 /ntlm
    

    编译时若出现报错:“System.Configuration.StringUtil”不可访问,因为它受保护级别限制,请将WebAdminPage.cs中代码作如下修改:

    //取消部份:
    string appId = StringUtil.GetNonRandomizedHashCode(String.Concat(appPath, appPhysPath)).ToString("x", CultureInfo.InvariantCulture);
    
    
    //新增加部份:
    Assembly sysConfig = Assembly.LoadFile(@"C:WindowsMicrosoft.NETFrameworkv4.0.30319System.Configuration.dll");
    Type sysConfigType = sysConfig.GetType("System.Configuration.StringUtil");
    string appId = ((int)sysConfigType.GetMethod("GetNonRandomizedHashCode").Invoke(null, new object[] { String.Concat(appPath, appPhysPath), true })).ToString("x", CultureInfo.InvariantCulture);
    

    这样就可以按照命令生生成的网址进行访问就可以了。如果像我一样,操作系统为:WINDOWS 10,那么不好意思,生成的网站虽然能够打开,但仍会报错:

    遇到错误。请返回上一页并重试。

    目前没有找到解决方案,网上有说ASP.NET网站管理工具在WIN10下不被支持,到底为何暂时无解,若大家有知道的还请分享一下(CSDN有别人的求问贴:http://bbs.csdn.net/topics/391819719),非常感谢,我这里就只好换台电脑来运行ASP.NET管理工具网站了。

     WCF服务端配置如下:

      <connectionStrings>
        <add name="SqlConn" connectionString="Server=.;Database=aspnetdb;Uid=sa;Pwd=www.zuowenjun.cn;"/>
      </connectionStrings>
      <system.web>
        <compilation debug="true" targetFramework="4.5" />
        <httpRuntime targetFramework="4.5"/>
        <membership defaultProvider="SqlMembershipProvider">
          <providers>
            <clear/>
            <add name="SqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="SqlConn" applicationName="/" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" requiresUniqueEmail="true" passwordFormat="Hashed"/>
          </providers>
        </membership>
      </system.web>
      <system.serviceModel>
        <behaviors>
          <serviceBehaviors>
            <behavior name="Service1Behavior">
              <serviceCredentials>
                <serviceCertificate findValue="ZwjCert" storeLocation="LocalMachine"
                  storeName="TrustedPeople" x509FindType="FindBySubjectName" />
                <userNameAuthentication userNamePasswordValidationMode="MembershipProvider"
                  membershipProviderName="SqlMembershipProvider" />
              </serviceCredentials>
               <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
              <serviceDebug includeExceptionDetailInFaults="false" />
            </behavior>
          </serviceBehaviors>
        </behaviors>
        <bindings>
          <wsHttpBinding>
            <binding name="Service1Binding">
              <security mode="Message">
                <message clientCredentialType="UserName"/>
              </security>
            </binding>
          </wsHttpBinding>
        </bindings>
        <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
      <services>
        <service name="WcfService1.Service1" behaviorConfiguration="Service1Behavior">
          <endpoint address="" binding="wsHttpBinding" contract="WcfService1.IService1" bindingConfiguration="Service1Binding">
          </endpoint>
        </service>
      </services>
      </system.serviceModel>
    

    这里需注意几点:

    1.配置connectionString,连接到membership所需的数据库; 

    2.配置membership,增加SqlMembershipProvider属性配置;

    3.配置serviceCredential,与第一种基本相同,不同的是userNameAuthentication的配置:userNamePasswordValidationMode="MembershipProvider",membershipProviderName="SqlMembershipProvider";

    4.配置Binding节点<message clientCredentialType="UserName"/>,这与第一种相同; 

    客户端引用WCF服务,查看生成的配置文件内容,需确保Binding节点有以下配置信息:

    <security mode="Message">
            <message clientCredentialType="UserName" />
     </security>
    

    最后使用WCF服务,使用代码与第一种相同,唯一需要注意的是,传入的UserName和Password均为ASP.NET网站管理工具中创建的用户信息。

    另外我们也可以采用membership+Form验证,利用ASP.NET的身份验证机制,要实现这种模式,是需要采用svc文件,并寄宿在IIS上,具体实现方法,参见:http://www.cnblogs.com/danielWise/archive/2011/01/30/1947912.html

    由于WCF的验证方法很多,本文无法一次性全部写完,敬请期待续篇!

  • 相关阅读:
    HDU 1229 还是A+B(A+B陶冶情操)
    WINDOWS API ——CREATETOOLHELP32SNAPSHOT——查找进程
    WinAPI: GetCurrentThread、GetCurrentThreadId、GetCurrentProcess、GetCurrentProcessId
    创建线程后马上CloseHandle(threadhandle)起什么作用
    CloseHandle(),TerminateThread(),ExitThread()的区别
    WinAPI: OpenProcess、GetExitCodeProcess、TerminateProcess (测试强制关闭 OICQ)
    GetVersion和GetVersionEx
    WinAPI: GetModuleFileName、GetModuleHandle
    C# 获取窗口句柄并且关闭应用程序
    IsWindow,findwindow
  • 原文地址:https://www.cnblogs.com/zuowj/p/5021270.html
Copyright © 2011-2022 走看看