代码之家  ›  专栏  ›  技术社区  ›  Sefe

当绑定安全性为TransportCredentially时,在WCF REST中创建自定义IIdentity

  •  4
  • Sefe  · 技术社区  · 7 年前

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

    习惯 服务凭据 AuthorizationPolicy 实现方法如下:

    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;
            }
        }
    }
    

    应用程序配置 我使用的是:

    <?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>
    

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

    1 回复  |  直到 7 年前
        1
  •  1
  •   Sefe    7 年前

    通过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 ,例如

    公共类拦截器行为张力:行为张力,IServicebehavior,

    您将需要实现接口,并修改

    应用DispatchBehavior

    方法如下

    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 并修改

    public对象beforesendRequest(ref system.serviceModel.channels.message请求, system.serviceModel.iclientchannel频道)

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

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

    内部类拦截器行为拉伸:行为拉伸元素,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 在验证时忽略密码)。