65.9K
CodeProject 正在变化。 阅读更多。
Home

在 IIS 中使用自定义用户名和密码验证器的 WCF 服务(HTTPS)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2010年2月19日

CPOL

5分钟阅读

viewsIcon

122564

如何在 IIS 中托管一个具有自定义用户名验证器的 WCF HTTPS 服务。

引言

当前任务可以表述如下:创建一个托管在 IIS7 上的 WCF 服务,通过 HTTPS 连接,并带有自定义的用户名和密码验证器。使用传输模式,不进行消息加密。听起来很简单,对吧?

背景

设计一个由 HTTPS 保护的服务,并能够使用自定义验证器(例如,与 SQL 或其他自定义身份验证方案进行比对),这是一项常见任务。我们想要的是一个简单的安全服务,代码量很少。

网上有大量文章描述了使用传输模式或消息安全模式绑定的自定义用户名验证(在 CodePlex 上有大量集合,或者在 Google 上搜索“WCF username transport”),但当我处理生产环境时,遇到了很多问题,因此我认为在本篇文章中提及这些问题会很有用。

我假设您已经有 WCF 方面的经验;这里不会提供完整的示例,而是提供一些关于如何正确设置一切的技巧。

Service

构建一个由 HTTPS 保护的服务很简单。在接下来的文档和示例中,我们将为服务声明契约、实现和配置部分。

对于契约,我们按如下方式定义 IDC.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace SafeService
{
 [ServiceContract]
  public interface IDC
  {
    [OperationContract]
    bool DoSomethingSecurely(string sParameter);
  }
}

为简单起见,我们省略了数据契约。

服务契约的实现位于 DCService.svc 中。

using System;
using System.Security;
using System.IdentityModel.Selectors;

namespace SafeService
{  
  public class DCService : IDC
  {
    public bool DoSomethingSecurely(string sParam)
    {
      return sParam == "isthissafeservice?";
    }
  }

  public class UNValidator : UserNamePasswordValidator
  {
    public UNValidator()
      : base()
    {
    }
    public override void Validate(string userName, string password)
    {
      if (userName == "test" && password == "the best")
        return;
      throw new System.IdentityModel.Tokens.SecurityTokenException(
                "Unknown Username or Password");
    }
  }
}

请注意,我们还定义了一个名为 UNValidator 的类,它将在稍后用作自定义用户名和密码验证器。

现在我们必须定义服务的配置。我们先讨论各个部分;请注意,此处仅显示了配置文件中的 <system.serviceModel><diagnostics> 部分!

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

    <!-- here are your server, appllication and other settings-->
     <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="SafeServiceConf" 
                        maxReceivedMessageSize="65536">
                    <security mode="TransportWithMessageCredential">
                        <message clientCredentialType="UserName"/>
                    </security>
                    <readerQuotas   maxArrayLength="65536" 
                                    maxBytesPerRead="65536 
                                    maxStringContentLength="65536"/>
                </binding>
            </wsHttpBinding>
        </bindings>
        <services>
          <service behaviorConfiguration="SafeSerice.ServiceBehavior" 
                   name="SafeService.DCService">
              <endpoint address="/safe" 
                        binding="wsHttpBinding" 
                        contract="SafeService.IDC" 
                        bindingConfiguration="SafeServiceConf">
                  <identity>
                  <dns value="localhost"/>
                  </identity>                  
              </endpoint>
              <endpoint address="mex" binding="mexHttpsBinding" 
                                      contract="SafeService.IDC" />
           </service>
          </services>
        <behaviors>
          <serviceBehaviors>
            <behavior name="SafeSerice.ServiceBehavior">
                      <serviceMetadata httpGetEnabled="true" />
                      <serviceDebug includeExceptionDetailInFaults="true" />
                      <serviceCredentials>
                        <userNameAuthentication 
                             userNamePasswordValidationMode="Custom"  
                             customUserNamePasswordValidatorType=
                                        "SafeService.UNValidator,SafeService"/>
                        </serviceCredentials>                    
                     </behavior>
          </serviceBehaviors>
        </behaviors>
 
        <!-- comment this if you don't want to make diag trace -->
        <diagnostics>
            <messageLogging maxMessagesToLog="30000"
                    logEntireMessage="true"
                    logMessagesAtServiceLevel="false"
                    logMalformedMessages="true"
                    logMessagesAtTransportLevel="true">
                <filters>
                    <clear/>
                </filters>
            </messageLogging>
        </diagnostics>
    </system.serviceModel>

    <!-- comment this if you don't want to make diag trace -->
    <system.diagnostics>
        <sources>
            <source name="System.ServiceModel" 
                     switchValue="Warning, ActivityTracing" 
                     propagateActivity="true" >
                <listeners>
                    <add name="xml" />
                </listeners>
            </source>
            <source name="System.ServiceModel.MessageLogging" 
                      switchValue="Warning">
                <listeners>
                    <add name="xml" />
                </listeners>
            </source>
        </sources>
        <sharedListeners>
            <add name="xml" 
              type="System.Diagnostics.XmlWriterTraceListener" 
              initializeData="C:\Temp\Server2.svclog" />
        </sharedListeners>
        <trace autoflush="true" indentsize="4"/>
    </system.diagnostics>
</configuration>

正如预期的那样,最棘手的部分隐藏在服务的配置中……

数据绑定。

我们定义了 WsHttpBinding,其安全模式为 TransportWithMessageCredentials。您可能会问,为什么不是 Transport。答案是,如果我们想在 IIS 下使用简单的自定义用户名验证器,我们必须使用 TransportWithMessageCredentials,即使它有很大的开销。在 IIS 下,自定义验证器在 Transport 安全模式下将无法工作。至少,我无法使其工作,并且 此处 也提到了这一点。

maxReceiveMessageSize, readerQuotas

请注意要将这些值设置得足够大,否则您将遇到奇怪的错误或**甚至超时**;服务不会告诉您消息大小太小的问题。

安全

即使我们使用 HTTPS(也称为传输模式),安全也是通过 SOAP 消息身份验证机制实现的,所以我们必须设置 <security> 元素,并指定 <message> 子元素。我们将 clientCredentialType 设置为 custom。通过这样做,我们启用了自定义用户名和密码验证器的使用。

serviceMetadata

在开发过程中不要忘记启用此项,否则在构建客户端时可能无法获取服务元数据。如果启用,您还可以从浏览器浏览和测试服务的 mex 端点。在生产环境中禁用此项。

serviceCerdentials, userNameAuthentiocation

这些元素允许您指定自定义用户名和密码验证器将由我们之前定义的类实现。customUserNamePasswordValidatorType 定义了实现验证器的类的类型。语法与其他情况一样:“类名(带命名空间),程序集名称”。

诊断

当您无法使其工作且不知道该怎么做时,这些部分会很有用。激活诊断功能,发送客户端请求,然后打开跟踪文件 - Microsoft Trace viewer 会打开这些文件。

所以,这就是我们安全的服务的设置。我们将其部署到 IIS,99% 的情况下它不会工作。我们也必须仔细设置 IIS。

IIS

您需要注意以下几点才能完成,或检查给定环境中的这些内容:

  • 创建网站
  • 创建单独的应用程序池(生产环境)
  • 在服务器设置中启用 WCF 激活(不是 IIS,而是在 2008 Server 的“启用/关闭系统功能”中,或者如果您正在开发,则是在 W7 中)
  • 启用 HTTPS 协议
  • 添加 HTTPS 绑定
  • 在生产环境中,删除 HTTP 绑定
  • 启用匿名身份验证,禁用所有其他身份验证
  • 创建服务器证书,并为站点添加一个(测试时,请遵循 CodePlex 上的示例)

这应该就够了。之后,在浏览器中测试您的服务。也许有人会指出您可能遇到的其他陷阱。

客户端

这很简单。在客户端,您添加服务引用;您可以使用 disco,或者像我一样输入地址(在本例中为 https:///DocumentCenterWCF/SafeService.svc)。

但请注意,您必须暂时在服务配置中启用 httpGetEnabledhttpsGetEnabled,才能获取元数据。

一旦添加了服务引用,您可能想开始使用客户端。使用标准的客户端构建方法,设置凭据、绑定和终结点,并重复调用方法。您可能会遇到奇怪的**超时错误**(**通道超时**或类似错误)。跟踪会显示一些错误,但我敢打赌您找不到更多信息。唯一指向解决方案的是关于对象激活的内容。激活需要停用……

问题在于您必须在客户端的 ChannelFactory 上显式调用 Close 方法。更智能、更健壮的解决方案是使用 using,因为客户端实现了 IDisposable

因此,在这种情况下,我使用类似下面的内容,这在创建 WCF 客户端的几个案例中很有用:

/// <summary>
/// Helper to get instance of the disposable client
/// </summary>
/// <returns></returns>
protected DCService.DCClient getWCF()
{
  //endpoint -do not forget to add endpoint address after svc
  EndpointAddress ep = new EndpointAddress(new Uri(
     "https:///SafeService/safeService.svc/safe"));
  //transport encrypting with SOAP message authentication
  WSHttpBinding bind = new WSHttpBinding(
           SecurityMode.TransportWithMessageCredential);
  //10 sec timeouts
  bind.ReceiveTimeout = new TimeSpan(0, 0, 10); 
  bind.SendTimeout = new TimeSpan(0, 0, 10); 
  bind.BypassProxyOnLocal = false; // to be able bedugging eg. with fiddler
  bind.UseDefaultWebProxy = true; //use default system proxy
  //this we need for our custom credentials
  bind.Security.Message.ClientCredentialType = 
                        MessageCredentialType.UserName;
  //construct client
  DCService.DCClient cli = new DCService.DCClient(bind, ep);
  //pass custom credentials
  cli.ClientCredentials.UserName.UserName = WS_user;
  cli.ClientCredentials.UserName.Password = WS_pwd;
  return cli;
}

/// <summary>
/// Call the service
/// </summary>
/// <param name="something"></param>
/// <returns></returns>
public bool DoSomething(string something)
{
  using (var cli = getWCF())
  {
    //call to ensure object disposing and thus channel closing
    return cli.DoSomethingSecurely(something);
  }
}

请注意,这只是一个示例;代理和终结点地址应可配置,但我喜欢在代码中使用我自己的设置来配置客户端事物,并且这个示例应该说明我将一些东西硬编码为 URL。

最后一点 - 如果您在开发过程中没有安装受信任的证书,您会遇到证书错误。在开发过程中,我使用一个简单的证书解决方法,但请注意在生产环境中进行适当的证书检查以确保服务安全!

//my WCF service connector class constructor
public DCConnector()
{
  //SET certificate validation callback
  ServicePointManager.ServerCertificateValidationCallback = new 
    System.Net.Security.RemoteCertificateValidationCallback(
    ValidateServerCertificate);
}

//my certificate validation
public static bool ValidateServerCertificate(
      object sender,
      X509Certificate certificate,
      X509Chain chain,
      SslPolicyErrors sslPolicyErrors)
{
  //only in development! skip certificatefaults
  return true;
}

WCF 很好、可靠且高效。但您需要时刻思考并努力理解整个框架。在 WCF 的情况下,复制/粘贴并不总是最好的选择。

© . All rights reserved.