【问题标题】:Create a custom IIdentity in WCF REST when binding security is TransportCredentialOnly当绑定安全性为 TransportCredentialOnly 时,在 WCF REST 中创建自定义 IIdentity
【发布时间】:2018-07-10 12:07:18
【问题描述】:

我需要实现一个使用 HTTP 基本身份验证的 REST 服务。由于它是在现有基础架构上构建的,因此我需要将其实现为 WCF 服务。出于向后兼容和集成到现有生态系统的原因,我需要将用户名和密码都传递给服务(此时请不要考虑可能的安全隐患)。由于默认情况下,WCF 运行时会从标头中剥离身份验证信息,因此我的解决方案是创建一个自定义 IIdentity,其中包含我可以在服务级别访问的密码信息:

public class UserIdentity : GenericIdentity
{
    private readonly bool m_isAuthenticated;

    public string Password {
        get;
    }

    public override bool IsAuthenticated {
        get {
            return base.IsAuthenticated && m_isAuthenticated;
        }
    }
    public UserIdentity(IIdentity existingIdentity, string password)
        : base(existingIdentity.Name)
    {
        m_isAuthenticated = existingIdentity.IsAuthenticated;
        Password = password;
    }
}

我曾尝试通过以下方式转发密码,但都没有成功:

  1. 实现一个自定义UserNamePasswordValidator,它可以访问密码,但只能处理身份验证。无法创建或修改IIdentity
  2. 创建自定义ServiceCredentialsas described in this article,当绑定安全设置为Transport 时可以正常工作。但是,这需要到服务的 HTTPS 连接,这对我来说是不可行的,因为传输级别的安全性由上游的负载平衡器处理。服务本身必须是 HTTP。因此安全设置为TransportCredentialOnly。这样做的效果是自定义 ServiceCredentials 类永远不会被 WCF 运行时初始化(与安全设置为 Transport 不同)。
  3. 直接在app.config中配置自定义AuthorizationPoliciy。在这种情况下,自定义授权策略被初始化,但在密码信息已经不可用的时候调用它(当它使用ServiceCredentials 初始化时,这不是问题,因为它在初始化期间确实收到了密码)。

自定义的ServiceCredentialsAuthorizationPolicy 实现如下:

public class UserServiceCredentials : ServiceCredentials
{
    public UserServiceCredentials()
    {
    }

    protected UserServiceCredentials(ServiceCredentials other) : base(other)
    {
    }

    protected override ServiceCredentials CloneCore()
    {
        return new UserServiceCredentials(this);
    }

    public override SecurityTokenManager CreateSecurityTokenManager()
    {
        if (UserNameAuthentication.UserNamePasswordValidationMode == UserNamePasswordValidationMode.Custom)
        {
            return new UserSecurityTokenManager(this);
        }
        return base.CreateSecurityTokenManager();
    }
}

internal class UserSecurityTokenManager : ServiceCredentialsSecurityTokenManager
{
    public UserSecurityTokenManager(UserServiceCredentials credentials) : base(credentials)
    {
    }

    public override SecurityTokenAuthenticator CreateSecurityTokenAuthenticator(SecurityTokenRequirement tokenRequirement,
        out SecurityTokenResolver outOfBandTokenResolver)
    {
        outOfBandTokenResolver = null;
        UserNamePasswordValidator validator = ServiceCredentials.UserNameAuthentication.CustomUserNamePasswordValidator;
        return new UserSecurityTokenAuthenticator(validator ?? new Validator());
    }
}

internal class UserSecurityTokenAuthenticator : CustomUserNameSecurityTokenAuthenticator
{
    public UserSecurityTokenAuthenticator(UserNamePasswordValidator validator) : base(validator)
    {
    }

    protected override ReadOnlyCollection<IAuthorizationPolicy> ValidateUserNamePasswordCore(string userName,
        string password)
    {
        ReadOnlyCollection<IAuthorizationPolicy> currentPolicies =
            base.ValidateUserNamePasswordCore(userName, password);
        List<IAuthorizationPolicy> policies = new List<IAuthorizationPolicy>(currentPolicies);
        policies.Add(new UserAuthorizationPolicy(userName, password));
        return policies.AsReadOnly();
    }
}

public class UserAuthorizationPolicy : IAuthorizationPolicy
{
    private string m_userName;
    private string m_password;

    //Called when used with service credentials
    public UserAuthorizationPolicy(string userName, string password)
    {
        m_userName = userName;
        m_password = password;
    }

    //Called when directly configured in the config file
    public UserAuthorizationPolicy()
    {
    }

    public ClaimSet Issuer {
        get;
    } = ClaimSet.System;

    public string Id {
        get;
    } = Guid.NewGuid().ToString();

    public bool Evaluate(EvaluationContext evaluationContext, ref object state)
    {
        bool hasIdentities = evaluationContext.Properties.TryGetValue("Identities", out object rawIdentities);
        if (rawIdentities is IList<IIdentity> identities)
        {
            var identityQry =
                from id in identities
                where String.Equals(id.Name, m_userName, StringComparison.OrdinalIgnoreCase)
                select id;
            IIdentity identity = identityQry.FirstOrDefault();
            if (identity == null)
            {
                return false;
            }
            UserIdentity userIdentity = new UserIdentity(identity, m_password);
            identities.Remove(identity);
            identities.Add(userIdentity);

            evaluationContext.Properties["PrimaryIdentity"] = userIdentity;
            evaluationContext.Properties["Principal"] = new GenericPrincipal(userIdentity, null);

            return true;
        }
        else
        {
            return false;
        }
    }
}

我正在使用的app.config是这个:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
    <system.serviceModel>
        <bindings>
            <webHttpBinding>
                <binding name="TestBinding">
                    <security mode="TransportCredentialOnly">
                        <transport clientCredentialType="Basic">
                        </transport>
                    </security>
                </binding>
            </webHttpBinding>
        </bindings>
        <behaviors>
            <serviceBehaviors>
                <behavior name="TestServiceBehavior">
                    <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
                    <serviceDebug includeExceptionDetailInFaults="true"/>
                    <!-- Custom service credentials: Works when binding security is Transport. Is not invoked when security TransportCredentialOnly-->
                    <serviceCredentials type="WcfTestServices.UserServiceCredentials, WcfTestServices">
                        <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="WcfTestServices.Validator, WcfTestServices"/>
                    </serviceCredentials>
                    <serviceAuthorization principalPermissionMode="Custom">
                        <!-- Authorization policy works when binding security is TransportCredentialOnly, but has no password -->
                        <authorizationPolicies>
                            <add policyType="WcfTestServices.UserAuthorizationPolicy, WcfTestServices"/>
                        </authorizationPolicies>
                    </serviceAuthorization>
                </behavior>
            </serviceBehaviors>
            <endpointBehaviors>
                <behavior name="TestEndpointBehavior">
                    <webHttp/>
                </behavior>
            </endpointBehaviors>
        </behaviors>
        <services>
            <service name="WcfTestServices.TestService" behaviorConfiguration="TestServiceBehavior">
                <endpoint address="" binding="webHttpBinding"
                                    bindingConfiguration="TestBinding"
                                    behaviorConfiguration="TestEndpointBehavior"
                                    contract="WcfTestServices.ITestService"/>
                <host>
                    <baseAddresses>
                        <add baseAddress="http://localhost:12700/"/>
                    </baseAddresses>
                </host>
            </service>
        </services>
    </system.serviceModel>
</configuration>

有没有办法可以将密码信息转发到这个星座中的服务?我首选的解决方案是自定义 IIdentity,但我愿意接受其他建议。

【问题讨论】:

  • 这并没有真正回答问题,但您能不能只信任负载均衡器上的自签名证书?
  • @ste-fu:我不知道负载平衡器是否可以这样配置,但无论如何,我希望 IT 的管理工作尽可能低。
  • 我在一个很难让操作人员做事的环境中工作,但这对他们来说应该是一个很好理解的任务。他们可以创建一个自签名证书(也许使用 openssl)。然后他们只需登录您的服务正在运行的盒子,在 iis 中安装证书。然后登录负载均衡器并将证书安装在受信任的证书存储中

标签: c# wcf wcf-security


【解决方案1】:

通过cookie发送信息也可能是一种选择,你可以尝试以下,

服务端

创建一个实现IDispatchMessageInspector的类

public class IdentityMessageInspector : IDispatchMessageInspector
{
    public object AfterReceiveRequest(ref Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
        {
            var messageProperty = (HttpRequestMessageProperty)
                OperationContext.Current.IncomingMessageProperties[HttpRequestMessageProperty.Name];
            string cookie = messageProperty.Headers.Get("Set-Cookie");
            if (cookie == null) // Check for another Message Header - SL applications
            {
                cookie = messageProperty.Headers.Get("Cookie");
            }
            if (cookie == null)
                cookie = string.Empty;
            //You can get the credentials from here, do something to them, on the service side
}

注意OperationContext.IncomingMessageProperties Property这行,可以用来获取消息的传入消息属性,根据链接的MSDN链接,

使用此属性检查或修改服务操作中的请求消息或客户端代理中的回复消息的消息属性

,然后创建一个实现IServiceBehvaior的类,例如

公共类 InterceptorBehaviorExtension : BehaviorExtensionElement, IServiceBehavior,

你需要实现接口,并修改

ApplyDispatchBehavior

方法如下

public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
    {
        foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
        {
            foreach (var endpoint in dispatcher.Endpoints)
            {
                endpoint.DispatchRuntime.MessageInspectors.Add(new IdentityMessageInspector());
            }
        }
    }

,然后继续将其添加到您的 web.config/app.config 文件中

<extensions>
  <behaviorExtensions>
    <add name="interceptorBehaviorExtension" type="test.InterceptorBehaviorExtension, test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
  </behaviorExtensions>
</extensions>

,然后包含该行

<interceptorBehaviorExtension />

在你的行为元素标签中。

客户

在客户端,您需要使用IClientMessageInspector 修改httpmessage 并修改

公共对象 BeforeSendRequest(参考 System.ServiceModel.Channels.Message 请求, System.ServiceModel.IClientChannel 通道)

将凭据添加到客户端代码的方法。

接下来,将此添加到实现IEndpointBehavior的类中,

内部类 InterceptorBehaviorExtension : BehaviorExtensionElement, IEndpointBehavior

并修改

public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(new CookieMessageInspector());
        }

方法,然后将上述代码添加到 WCF 客户端代码中的端点行为列表中, 虽然我想您可以使用 HttpClient 或 WebClient 添加代码,并在连接到服务时使用它来提供凭据。


更新:

解决方案的关键是从该行的原始 HTTP 消息中获取标头:

var messageProperty = (HttpRequestMessageProperty)OperationContext.Current
    .IncomingMessageProperties[HttpRequestMessageProperty.Name];

这允许您像这样访问授权标头:

string authorization = message.Headers.Get("Authorization");

由于OperationContext 可以从服务本身读取,因此可以直接从服务读取和解析授权数据。在基本身份验证的情况下,这包括用户名和密码。不需要消息检查器(尽管您需要一个额外的 UserNamePasswordValidator 来忽略验证时的密码)。

【讨论】:

  • 这个答案让我走上了正轨。您可以使用OperationContext.Current.IncomingMessageProperties[HttpRequestMessageProperty.Name] 简单地获取服务级别的授权标头,然后从那里访问授权标头。不需要消息检查器或任何东西。您只需要一个允许请求通过的自定义验证器。我给你+1和赏金;如果您在答案中添加简单的解决方案,我也会接受。
  • 谢谢@Sefe,我已经更新了帖子以包含信息
  • 介意我编辑您的帖子以包含我为解决方案收集的信息吗?
  • 是的,谢谢,这对我来说将是一次学习体验,也是关于如何表达我的问题或答案的经验。
  • 我已经编辑了你的答案并添加了我的关键要点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-05-23
  • 1970-01-01
  • 1970-01-01
  • 2016-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多