令牌验证器

此示例演示如何实现自定义令牌验证器。 Windows Communication Foundation(WCF)中的令牌验证器用于验证与消息一起使用的令牌、验证它是否自一致以及对与令牌关联的标识进行身份验证。

自定义令牌验证器在各种情况下非常有用,例如:

  • 当您希望重写与令牌相关联的默认身份验证机制时。

  • 当您生成自定义令牌时。

此示例演示了以下内容:

  • 客户端如何使用用户名/密码对进行身份验证。

  • 服务器如何使用自定义令牌验证器验证客户端凭据。

  • WCF 服务代码如何与自定义令牌验证器关联。

  • 如何使用服务器的 X.509 证书对服务器进行身份验证。

该示例说明在自定义令牌身份验证过程完成后,如何从WCF访问调用方的身份信息。

该服务公开一个终结点,用于与服务通信,该终结点使用 App.config 配置文件定义。 终结点由地址、绑定和协定组成。 绑定配置为标准 wsHttpBinding,安全模式设置为消息——这是 wsHttpBinding 的默认模式。 此示例设置使用客户端用户名身份验证的标准 wsHttpBinding 。 该服务还使用 serviceCredentials 行为配置服务证书。 此 securityCredentials 行为允许指定服务证书。 客户端使用服务证书对服务进行身份验证并提供消息保护。 以下配置引用在示例安装过程中安装的 localhost 证书,如以下安装说明中所述。

<system.serviceModel>
    <services>
      <service
          name="Microsoft.ServiceModel.Samples.CalculatorService"
          behaviorConfiguration="CalculatorServiceBehavior">
        <host>
          <baseAddresses>
            <!-- configure base address provided by host -->
            <add baseAddress ="http://localhost:8000/servicemodelsamples/service" />
          </baseAddresses>
        </host>
        <!-- use base address provided by host -->
        <endpoint address=""
                  binding="wsHttpBinding"
                  bindingConfiguration="Binding1"
                  contract="Microsoft.ServiceModel.Samples.ICalculator" />
      </service>
    </services>

    <bindings>
      <wsHttpBinding>
        <binding name="Binding1">
          <security mode="Message">
            <message clientCredentialType="UserName" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>

    <behaviors>
      <serviceBehaviors>
        <behavior name="CalculatorServiceBehavior">
          <serviceDebug includeExceptionDetailInFaults="False" />
          <!--
          The serviceCredentials behavior allows one to define a service certificate.
          A service certificate is used by a client to authenticate the service and provide message protection.
          This configuration references the "localhost" certificate installed during the setup instructions.
....        -->
          <serviceCredentials>
            <serviceCertificate findValue="localhost" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName" />
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>

  </system.serviceModel>

客户端终结点配置包括配置名称、服务终结点的绝对地址、绑定和协定。 该客户端绑定是使用相应的 ModeclientCredentialType 配置的。

<system.serviceModel>
    <client>
      <endpoint name=""
                address="http://localhost:8000/servicemodelsamples/service"
                binding="wsHttpBinding"
                bindingConfiguration="Binding1"
                contract="Microsoft.ServiceModel.Samples.ICalculator">
      </endpoint>
    </client>

    <bindings>
      <wsHttpBinding>
        <binding name="Binding1">
          <security mode="Message">
            <message clientCredentialType="UserName" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>

客户端实现设置要使用的用户名和密码。

static void Main()
{
     ...
     client.ClientCredentials.UserNamePassword.UserName = username;
     client.ClientCredentials.UserNamePassword.Password = password;
     ...
}

自定义令牌验证器

使用以下步骤创建自定义令牌验证器:

  1. 编写自定义令牌验证器。

    此示例实现一个自定义令牌验证器,用于验证用户名是否具有有效的电子邮件格式。 它派生 UserNameSecurityTokenAuthenticator。 在这个类中,最重要的方法是ValidateUserNamePasswordCore(String, String)。 在此方法中,验证器验证用户名的格式,以及主机名是否不是来自恶意域。 如果满足这两个条件,则会返回实例的 IAuthorizationPolicy 只读集合,该集合随后用于提供声明,这些声明表示存储在用户名令牌中的信息。

    protected override ReadOnlyCollection<IAuthorizationPolicy> ValidateUserNamePasswordCore(string userName, string password)
    {
        if (!ValidateUserNameFormat(userName))
            throw new SecurityTokenValidationException("Incorrect UserName format");
    
        ClaimSet claimSet = new DefaultClaimSet(ClaimSet.System, new Claim(ClaimTypes.Name, userName, Rights.PossessProperty));
        List<IIdentity> identities = new List<IIdentity>(1);
        identities.Add(new GenericIdentity(userName));
        List<IAuthorizationPolicy> policies = new List<IAuthorizationPolicy>(1);
        policies.Add(new UnconditionalPolicy(ClaimSet.System, claimSet, DateTime.MaxValue.ToUniversalTime(), identities));
        return policies.AsReadOnly();
    }
    
  2. 提供由自定义令牌验证器返回的授权策略。

    此示例提供名为 IAuthorizationPolicyUnconditionalPolicy 的自己的实现,在该示例的构造函数中,此策略返回一组传入该示例的声明和标识。

    class UnconditionalPolicy : IAuthorizationPolicy
    {
        String id = Guid.NewGuid().ToString();
        ClaimSet issuer;
        ClaimSet issuance;
        DateTime expirationTime;
        IList<IIdentity> identities;
    
        public UnconditionalPolicy(ClaimSet issuer, ClaimSet issuance, DateTime expirationTime, IList<IIdentity> identities)
        {
            if (issuer == null)
                throw new ArgumentNullException("issuer");
            if (issuance == null)
                throw new ArgumentNullException("issuance");
    
            this.issuer = issuer;
            this.issuance = issuance;
            this.identities = identities;
            this.expirationTime = expirationTime;
        }
    
        public string Id
        {
            get { return this.id; }
        }
    
        public ClaimSet Issuer
        {
            get { return this.issuer; }
        }
    
        public DateTime ExpirationTime
        {
            get { return this.expirationTime; }
        }
    
        public bool Evaluate(EvaluationContext evaluationContext, ref object state)
        {
            evaluationContext.AddToTarget(this, this.issuance);
    
            if (this.identities != null)
            {
                object value;
                IList<IIdentity> contextIdentities;
                if (!evaluationContext.Properties.TryGetValue("Identities", out value))
                {
                    contextIdentities = new List<IIdentity>(this.identities.Count);
                    evaluationContext.Properties.Add("Identities", contextIdentities);
                }
                else
                {
                    contextIdentities = value as IList<IIdentity>;
                }
                foreach (IIdentity identity in this.identities)
                {
                    contextIdentities.Add(identity);
                }
            }
    
            evaluationContext.RecordExpirationTime(this.expirationTime);
            return true;
        }
    }
    
  3. 编写自定义安全令牌管理器。

    SecurityTokenManager用于为通过SecurityTokenAuthenticator方法传递给它的特定SecurityTokenRequirement对象创建一个CreateSecurityTokenAuthenticator。 安全令牌管理器还用于创建令牌提供程序和令牌序列化程序,但此示例未涵盖这些提供程序和令牌序列化程序。 在此示例中,自定义安全令牌管理器通过继承ServiceCredentialsSecurityTokenManager类并重新定义CreateSecurityTokenAuthenticator方法,实现了在传递的令牌要求表明需要用户名验证器时返回自定义用户名令牌验证器的功能。

    public class MySecurityTokenManager : ServiceCredentialsSecurityTokenManager
    {
        MyUserNameCredential myUserNameCredential;
    
        public MySecurityTokenManager(MyUserNameCredential myUserNameCredential)
            : base(myUserNameCredential)
        {
            this.myUserNameCredential = myUserNameCredential;
        }
    
        public override SecurityTokenAuthenticator CreateSecurityTokenAuthenticator(SecurityTokenRequirement tokenRequirement, out SecurityTokenResolver outOfBandTokenResolver)
        {
            if (tokenRequirement.TokenType ==  SecurityTokenTypes.UserName)
            {
                outOfBandTokenResolver = null;
                return new MyTokenAuthenticator();
            }
            else
            {
                return base.CreateSecurityTokenAuthenticator(tokenRequirement, out outOfBandTokenResolver);
            }
        }
    }
    
  4. 编写自定义服务凭据。

    服务凭据类用于表示为服务配置的凭据,并创建一个安全令牌管理器,用于获取令牌验证器、令牌提供程序和令牌序列化程序。

    public class MyUserNameCredential : ServiceCredentials
    {
    
        public MyUserNameCredential()
            : base()
        {
        }
    
        protected override ServiceCredentials CloneCore()
        {
            return new MyUserNameCredential();
        }
    
        public override SecurityTokenManager CreateSecurityTokenManager()
        {
            return new MySecurityTokenManager(this);
        }
    
    }
    
  5. 将服务配置为使用自定义服务凭据。

    为了使服务使用自定义服务凭据,我们在捕获已在默认服务凭据中预配置的服务证书后删除默认服务凭据类,并将新的服务凭据实例配置为使用预配置的服务证书并将此新的服务凭据实例添加到服务行为。

    ServiceCredentials sc = serviceHost.Credentials;
    X509Certificate2 cert = sc.ServiceCertificate.Certificate;
    MyUserNameCredential serviceCredential = new MyUserNameCredential();
    serviceCredential.ServiceCertificate.Certificate = cert;
    serviceHost.Description.Behaviors.Remove((typeof(ServiceCredentials)));
    serviceHost.Description.Behaviors.Add(serviceCredential);
    

若要显示调用方的信息,可以使用 PrimaryIdentity 以下代码所示。 Current 包含有关当前调用方的声明信息。

static void DisplayIdentityInformation()
{
    Console.WriteLine("\t\tSecurity context identity  :  {0}",
            ServiceSecurityContext.Current.PrimaryIdentity.Name);
     return;
}

运行示例时,操作请求和响应将显示在客户端控制台窗口中。 在客户端窗口中按 Enter 关闭客户端。

设置批处理文件

此示例中包含的 Setup.bat 批处理文件允许你配置具有相关证书的服务器,以运行需要基于服务器证书的安全的自承载应用程序。 必须修改此批处理文件,以便跨计算机或在非承载情况下工作。

下面简要概述了批处理文件的不同部分,以便可以对其进行修改以在适当配置中运行。

  • 创建服务器证书。

    Setup.bat 批处理文件中的以下行创建要使用的服务器证书。 %SERVER_NAME%变量指定服务器名称。 更改此变量可以指定您自己的服务器名称。 此批处理文件中的默认值为 localhost。

    echo ************
    echo Server cert setup starting
    echo %SERVER_NAME%
    echo ************
    echo making server cert
    echo ************
    makecert.exe -sr LocalMachine -ss MY -a sha1 -n CN=%SERVER_NAME% -sky exchange -pe
    
  • 将服务器证书安装到客户端的受信任证书存储中。

    Setup.bat 批处理文件中的以下行将服务器证书复制到客户端的受信任的人的存储区中。 此步骤是必需的,因为 Makecert.exe 生成的证书不受客户端系统隐式信任。 如果已有一个证书,该证书已植根于客户端受信任的根证书(例如Microsoft颁发的证书),则不需要使用服务器证书填充客户端证书存储区。

    certmgr.exe -add -r LocalMachine -s My -c -n %SERVER_NAME% -r CurrentUser -s TrustedPeople
    

    注释

    安装程序批处理文件旨在从 Windows SDK 命令提示符运行。 这要求 MSSDK 环境变量指向 SDK 的安装目录。 此环境变量在 Windows SDK 命令提示符中自动设置。

设置和生成示例

  1. 确保已为 Windows Communication Foundation 示例 执行One-Time 安装过程。

  2. 要生成解决方案,请按照生成 Windows Communication Foundation 示例中的说明进行操作。

在同一计算机上运行示例

  1. 使用管理员权限从 Visual Studio 命令提示符内的示例安装文件夹运行 Setup.bat。 这会安装运行示例所需的所有证书。

    注释

    Setup.bat 批处理文件设计为通过 Visual Studio 命令提示符运行。 在 Visual Studio 命令提示中设置的 PATH 环境变量指向包含 Setup.bat 脚本所需的可执行文件的目录。

  2. 从 service\bin 启动 service.exe。

  3. 从 \client\bin 启动 client.exe。 客户端活动显示在客户端控制台应用程序中。

  4. 如果客户端和服务无法通信,请参阅 WCF 示例 故障排除提示。

跨计算机运行示例

  1. 在服务计算机上为服务二进制文件创建目录。

  2. 将服务程序文件复制到服务计算机上的服务目录。 此外,将 Setup.bat 和 Cleanup.bat 文件复制到服务计算机。

  3. 必须具有一个其主题名称中包含计算机的完全限定域名的服务器证书。 必须更新服务 App.config 文件以反映此新证书名称。 如果您将 %SERVER_NAME% 变量设置为将在其上运行服务的计算机的完全限定主机名,则可以使用 Setup.bat 来创建一个这样的证书。 请注意,必须使用管理员权限从 Visual Studio 的开发人员命令提示符下运行 setup.bat 文件。

  4. 将服务器证书复制到客户端 CurrentUser-TrustedPeople 存储中。 除非服务器证书由客户端受信任的颁发者颁发,否则不需要执行此作。

  5. 在服务计算机的 App.config 文件中,更改基址的值以指定一个完全限定的计算机名称,而不是 localhost。

  6. 在服务计算机上,从命令提示符运行 service.exe。

  7. 将 \client\bin\ 文件夹中的客户端程序文件(在特定于语言的文件夹下)复制到客户端计算机。

  8. 在客户端计算机上的 Client.exe.config 文件中,更改终结点的地址值以匹配服务的新地址。

  9. 在客户端计算机上,在命令提示符下启动 Client.exe。

  10. 如果客户端和服务无法通信,请参阅 WCF 示例 故障排除提示。

运行示例后进行清理

  1. 运行完示例后,在示例文件夹中运行 Cleanup.bat。