WCF加密操作(包括证书和证书+帐号密码)

  WCF作为.net三大组件之一,伟大之处不用多说,但是其加密配置对于我这样的萌新来说还是颇有难度,因此将几天来的研究成果共享出来,与各位共勉~

  首先声明我的开发环境,Win10创意者更新 + Visual Studio 2015 update3 + .Net 4.5 + iis10

  一、创建X.509证书

    1、创建证书

    可通过PowerShell或者makecert工具两种方式,个人建议使用参考资料更多后者,但最新的Windows和VS都不带makecert,所以需要的话可以到文章结尾处下载

    使用CMD运行: 

makecert -sr CurrentUser -ss My -n CN=HelloServiceClient -sky exchange -pe -r

    提示Succeded即创建完成。

    此时将在当前用户下的个人项目中看到这个证书,图中MMC管理单元的使用可以参考这里

    

    2、设置为信任

    由于创建的证书在个人域,且不在信任链中,wcf和iis目前不能使用这个证书,一次需要将其设置为信任。

    首先先将其导出到磁盘:证书上右键--所有任务--导出--选择导出私钥--设置私钥密码,完成后将得到一个pfx文件。

    然后进入上图的本地计算机,在个人域导入刚才那个pfx文件,完成后双击证书,在“证书路径”标签中提示“由于CA 根证书不在“受信任的根证书颁发机构”存储区中,所以它不受信任。”,此时证书仍然不能被使用,我的做法是在本地计算机的“受信任的根证书颁发机构”重复导入一次。此时两个证书都变成可信,即使将第二次导入的删除也没关系。

    以上做完没问题的话,双击证书后的状态应该是这样的:

    

  二、通过证书加密的项目

    1、创建wcf服务

      VS中新建“WCF服务应用程序”的项目,命名为WCF_HelloService,此时不用任何修改,已经是可运行的wcf服务,然后将其部署到iis,在浏览器中可使用http访问到服务信息:

      并重写Service1.scv.cs中的GetData()方法:

        public string GetData(int value)
        {
            if (ServiceSecurityContext.Current != null)
            {
                if (!ServiceSecurityContext.Current.IsAnonymous)
                {
                    return "Hello:" + ServiceSecurityContext.Current.PrimaryIdentity.Name + ";type=" + ServiceSecurityContext.Current.PrimaryIdentity.AuthenticationType;
                }
                return "Hello,你输入的是:" + value;
            }
            return "Hello ||未检测到证书:" + value;
        }
View Code

      下面是重点,编辑服务的Web.config文件,使其访问证书,这里尤其注意要注意用于各项配置互调的名称设置,如behaviorConfiguration和bindingConfiguration等:

<?xml version="1.0" encoding="utf-8"?>
<configuration>

  <appSettings>
    <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true"/>
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.5.2"/>
    <httpRuntime targetFramework="4.5.2"/>
    <httpModules>
      <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web"/>
    </httpModules>
  </system.web>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <remove name="ApplicationInsightsWebTracking"/>
      <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web"
        preCondition="managedHandler"/>
    </modules>
    <!--
        若要在调试过程中浏览 Web 应用程序根目录,请将下面的值设置为 True。
        在部署之前将该值设置为 False 可避免泄露 Web 应用程序文件夹信息。
      -->
    <directoryBrowse enabled="true"/>
    <validation validateIntegratedModeConfiguration="false"/>
  </system.webServer>
  
  <system.serviceModel>
    <services>
      <service name="WCF_HelloService.HelloService" behaviorConfiguration="CustomBehavior">

        <endpoint
    binding="mexHttpBinding"
    contract="IMetadataExchange"
    address="mex" />
        <endpoint address="" binding="wsHttpBinding" contract="WCF_HelloService.IHelloService" bindingConfiguration="CustomBinding"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="CustomBehavior">
          <!-- 为避免泄漏元数据信息,请在部署前将以下值设置为 false 并删除上面的元数据终结点 -->
          <serviceMetadata httpGetEnabled="true"/>
          <!-- 要接收故障异常详细信息以进行调试,请将以下值设置为 true。在部署前设置为 false 以避免泄漏异常信息 -->
          <serviceDebug includeExceptionDetailInFaults="false"/>
          
          <!--add by Lbh-->
          <serviceCredentials>
            <!-- 服务端采用证书详细配置    findValue :创建证书名称   storeName:证书储存详细位于哪    storeLocation :证书储存位于当前本机用户  X509FindType : x509查找证书主题名-->
            <serviceCertificate findValue="HelloServiceClient" storeName="My" storeLocation="LocalMachine" x509FindType="FindBySubjectName"/>
            <!--客户端验证方式-->
            <clientCertificate>
              <authentication certificateValidationMode="None"/>
            </clientCertificate>
          </serviceCredentials>

        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
    
    <!--add by Lbh-->
    <bindings>
      <wsHttpBinding>
        <binding name="CustomBinding">
          <!--验证方式-->
          <security mode="Message">
            <message clientCredentialType="Certificate"/>
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>

  </system.serviceModel>
</configuration>
View Code

      添加add by 注释是添加的内容,注意serviceCertificate节点,这里定义了目的证书的信息,请务必使其指向我们刚才配置好的证书,其他诸如命名空间、接口、类名等也应与项目对应。

      配置完成后如无问题,刷新刚才的web页面,我们仍然能看到服务启动成功的页面。

      2、配置客户端

      随便添加个winform程序,首先引用上面的服务,然后修改其app.config,同样需要注意behaviorConfiguration设置:

      

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
    </startup>
    <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="WSHttpBinding_IHelloService">
                    <security mode="Message">
                        <transport clientCredentialType="Windows" />
                        <message clientCredentialType="Certificate" />
                    </security>
                </binding>
            </wsHttpBinding>
        </bindings>
      <!--add by Lau-->
      <behaviors>
        <endpointBehaviors>
          <behavior name="CustomBehavior">
            <clientCredentials>
              <clientCertificate findValue="HelloServiceClient" storeName="My" storeLocation="LocalMachine" x509FindType="FindBySubjectName"/>
              <serviceCertificate>
                <authentication certificateValidationMode="None"/>
              </serviceCertificate>
            </clientCredentials>
          </behavior>
        </endpointBehaviors>
      </behaviors>
      
      <client>
        <endpoint address="http://localhost:8096/HelloService.svc" behaviorConfiguration="CustomBehavior"
          binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IHelloService"
          contract="HelloService.IHelloService" name="WSHttpBinding_IHelloService">
          <identity>
            <certificate encodedValue="AwAAAAEAAAAUAAAAmIXXyLpHnm+H6oDaCP03aIn03SsgAAAAAQAAABUCAAAwggIRMIIBeqADAgECAhC1V8uCAl/avEkX078G+PlRMA0GCSqGSIb3DQEBBAUAMB0xGzAZBgNVBAMTEkhlbGxvU2VydmljZUNsaWVudDAeFw0xNzA1MDgwNzE1NDBaFw0zOTEyMzEyMzU5NTlaMB0xGzAZBgNVBAMTEkhlbGxvU2VydmljZUNsaWVudDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1+nEnhCxXtfAFxOGgFgzBjcPeO2WmxQI5SC14e6S4yEz+ymJtfKBcEnRSCX7onQDRE5H9dPl9CqoNjI/nkU5OKZ789f5Jh7ISfDK0jfHPa2EYwKK3FwOwGFmx5YY2/7Eb/nmyq6gbroronBIioFU6mcZjkFmTQTDa2WnZJMIsikCAwEAAaNSMFAwTgYDVR0BBEcwRYAQhYkF0TiSQwHAV/0wgMmvE6EfMB0xGzAZBgNVBAMTEkhlbGxvU2VydmljZUNsaWVudIIQtVfLggJf2rxJF9O/Bvj5UTANBgkqhkiG9w0BAQQFAAOBgQA0LvNliWDaWtU4YkqXI8JU9/2mIHO2PK4EVUmUYJu0oxFNEeRcX8ZpAAAA26gRYN+J4IjC1F33NjRG/tzkGJeaTBdOl2SkJo8LqD2D7YfOcMaXfrAsAOcEP5e4z2Z4aZlZp1tOjf0X5SZ6QL4FbPiiJog+1UbF/z5J097peDU7Bw==" />
          </identity>
        </endpoint>
      </client>
    </system.serviceModel>
</configuration>
View Code

      与服务端类似地,clientCertificate节点定义了客户端证书,本例中使用了服务端相同的证书,也可以创建另一个专供客户端使用。certificate节点的内容来自服务端,引用WCF服务操作完成后会自动生成,如果没有,请检查WCF的web.config中是否定义为baseHttpBinding而不是wsHttpBinding(正确的是后者)。

      最后在winform加上基本的button和txtResult,并在button按钮事件写入代码:

      

        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                HelloService.HelloServiceClient client = new HelloService.HelloServiceClient();
                string result = client.GetData(DateTime.Now.Second);
                txtResult.Text = result;
            }
            catch (Exception ex)
            {
                this.txtResult.Text = ex.ToString();
            }
        }
View Code

      运行程序,得到正常结果如图:

      

      并且通过http拦截到的都是密文:

      

      至此,第一个证书项目完成,demo请到文章结尾处下载

  三、通过证书+帐号密码加密的项目

    1、创建WCF服务

      按上面步骤创建好服务,首先添加IdentityModel库的引用:

      

      然后创建用于校验的CustomUserPassword类,代码如下:

using System.IdentityModel.Selectors;
using System.ServiceModel;

namespace TestUserPassService
{
    public class CustomUserPassword : UserNamePasswordValidator
    {
        public override void Validate(string userName, string password)
        {
            if (userName != "admin" || password != "admin")
            {
                //throw new SecurityNegotiationException("验证用户名和密码时,未通过检测");// 此异常可能无法被客户端捕获
                throw new FaultException("用户名或者密码错误!");
            }
        }
    }
}
View Code

       最后修改web.config文件,可以看到增加了userNameAuthentication节点,定义的正是自定义的校验类:

<?xml version="1.0" encoding="utf-8"?>
<configuration>

  <appSettings>
    <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true"/>
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.5.2"/>
    <httpRuntime targetFramework="4.5.2"/>
    <httpModules>
      <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web"/>
    </httpModules>
  </system.web>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <remove name="ApplicationInsightsWebTracking"/>
      <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web"
        preCondition="managedHandler"/>
    </modules>
    <!--
        若要在调试过程中浏览 Web 应用程序根目录,请将下面的值设置为 True。
        在部署之前将该值设置为 False 可避免泄露 Web 应用程序文件夹信息。
      -->
    <directoryBrowse enabled="true"/>
    <validation validateIntegratedModeConfiguration="false"/>
  </system.webServer>
  <system.serviceModel>
    <services>
      <service name="TestUserPassService.Service1" behaviorConfiguration="CustomBehavior">

        <endpoint
    binding="mexHttpBinding"
    contract="IMetadataExchange"
    address="mex" />
        <endpoint address="" binding="wsHttpBinding" contract="TestUserPassService.IService1" bindingConfiguration="CustomBinding"/>
      </service>
    </services>
    
    <!--add by Lbh-->
    <behaviors>
      <serviceBehaviors>
        <behavior name="CustomBehavior">
          <!-- 为避免泄漏元数据信息,请在部署前将以下值设置为 false -->
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
          <!-- 要接收故障异常详细信息以进行调试,请将以下值设置为 true。在部署前设置为 false 以避免泄漏异常信息 -->
          <serviceDebug includeExceptionDetailInFaults="false"/>
          <serviceCredentials>
            <!-- 服务端采用证书详细配置    findValue :创建证书名称   storeName:证书储存详细位于哪    storeLocation :证书储存位于当前本机用户  X509FindType : x509查找证书主题名-->
            <serviceCertificate findValue="HelloServiceClient" storeName="My" storeLocation="LocalMachine" x509FindType="FindBySubjectName"/>
            <!--客户端验证方式-->
            <clientCertificate>
              <authentication certificateValidationMode="None"/>
            </clientCertificate>
            <userNameAuthentication  customUserNamePasswordValidatorType="TestUserPassService.CustomUserPassword,TestUserPassService" userNamePasswordValidationMode="Custom"/>
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <protocolMapping>
        <add binding="basicHttpsBinding" scheme="https"/>
    </protocolMapping>    
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/>
  
    <!--add by Lbh-->
    <bindings>
      <wsHttpBinding>
        <binding name="CustomBinding">
          <security mode="Message">
            <transport clientCredentialType="Windows"/>
            <message clientCredentialType="UserName"/>
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>

</configuration>
View Code

      注意clientCredentialType节点,这里采用映射到Windows账户的方式,这是颇为常用和可靠的方式。

      部署到iis,没问题的话,我们仍然可以使用浏览器通过http访问到服务。

    2、创建测试客户端

      新建winform客户端,首先添加引用,修改后的app.config如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
    </startup>
    <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="WSHttpBinding_IService1">
                  
                  <!--add by Lbh-->
                    <security mode="Message">
                        <transport clientCredentialType="Windows" />
                        <message clientCredentialType="UserName" />
                    </security>
                </binding>
            </wsHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:8095/Service1.svc" binding="wsHttpBinding"
                bindingConfiguration="WSHttpBinding_IService1" contract="Service1.IService1"
                name="WSHttpBinding_IService1">
                <identity>
                    <certificate encodedValue="AwAAAAEAAAAUAAAAmIXXyLpHnm+H6oDaCP03aIn03SsgAAAAAQAAABUCAAAwggIRMIIBeqADAgECAhC1V8uCAl/avEkX078G+PlRMA0GCSqGSIb3DQEBBAUAMB0xGzAZBgNVBAMTEkhlbGxvU2VydmljZUNsaWVudDAeFw0xNzA1MDgwNzE1NDBaFw0zOTEyMzEyMzU5NTlaMB0xGzAZBgNVBAMTEkhlbGxvU2VydmljZUNsaWVudDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1+nEnhCxXtfAFxOGgFgzBjcPeO2WmxQI5SC14e6S4yEz+ymJtfKBcEnRSCX7onQDRE5H9dPl9CqoNjI/nkU5OKZ789f5Jh7ISfDK0jfHPa2EYwKK3FwOwGFmx5YY2/7Eb/nmyq6gbroronBIioFU6mcZjkFmTQTDa2WnZJMIsikCAwEAAaNSMFAwTgYDVR0BBEcwRYAQhYkF0TiSQwHAV/0wgMmvE6EfMB0xGzAZBgNVBAMTEkhlbGxvU2VydmljZUNsaWVudIIQtVfLggJf2rxJF9O/Bvj5UTANBgkqhkiG9w0BAQQFAAOBgQA0LvNliWDaWtU4YkqXI8JU9/2mIHO2PK4EVUmUYJu0oxFNEeRcX8ZpAAAA26gRYN+J4IjC1F33NjRG/tzkGJeaTBdOl2SkJo8LqD2D7YfOcMaXfrAsAOcEP5e4z2Z4aZlZp1tOjf0X5SZ6QL4FbPiiJog+1UbF/z5J097peDU7Bw==" />
                </identity>
            </endpoint>
        </client>
    </system.serviceModel>
</configuration>
View Code

      可以看到,配置相比上一个项目简单许多,因为这里的客户端无需调用证书,只需定义加密类型。

      添加两个textbox一个button和一个textResult,定义按钮事件代码:

using System;
using System.Windows.Forms;

namespace TestUserPassService_Client
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void textBox1_TextChanged(object sender, EventArgs e)
        {

        }

        private void textBox2_TextChanged(object sender, EventArgs e)
        {

        }

        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                Service1.Service1Client client = new Service1.Service1Client();
                // 传入帐号密码
                client.ClientCredentials.UserName.UserName = this.textBox1.Text;
                client.ClientCredentials.UserName.Password = this.textBox2.Text;
                string result = client.GetData(DateTime.Now.Second);
                txtResult.Text = result;
            }
            catch (Exception ex)
            {
                this.txtResult.Text = ex.ToString();
            }
        }
    }
}
View Code

      运行客户端,正确的结果如图:

      

      假若修改传入的帐号密码,结果如下:

      

      查看http传输内容,同样是密文:

      

      至此,本项目完成,demo可在文章结尾处下载

  四、总结

    其实wcf加密操作没有太高深的内容(或者说暂且不用理会里面高深的内容),繁琐的部分在于web.config和app.config的配置,尤其bindingConfiguration这类名称命名上,由于网上教程众多,东拉一块西扯一块拼起来是用不了的。比如我这样的萌新调通两个项目就花了2天时间,因此这篇文章也尽可能将容易踩到的雷点暴露出来,供后来者们借鉴。当然篇幅和能力有限不能面面俱到,也请各位谅解,有问题可以在下面回复或者请教谷歌。

  五、demo下载

  

  证书demo

  

    证书+帐号密码demo

--------------------------------------------------------------------------------更新01------------------------------------------------------------------------------------------------------

  如果web访问配置好的服务提示“密钥集不存在”的问题,请按一下方法处理:

  进入路径:C:ProgramDataMicrosoftCryptoRSAMachineKeys(vista之后可用)

  找到刚才创建的证书文件,如果你不确定,可以参考这里

  然后右键-属性-安全,保证IIS_IUSRS用户有读取该文件的权限(本机测试时IIS是由这个用户运行的,其他电脑可能会有不同。)即可。

原文地址:https://www.cnblogs.com/lbhqq/p/6830875.html