当绑定安全性为TransportCredentialOnly

时间:2018-07-10 12:07:19

标签: c# wcf wcf-security

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

自定义AuthorizationPolicypublic 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

有没有办法可以将密码信息转发到该星座中的服务?我的首选解决方案是自定义app.use(app.loopback.json());,但我愿意接受其他建议。

1 个答案:

答案 0 :(得分: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
}

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

  

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

,然后创建一个实现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来忽略验证时的密码)。