当绑定安全性为 TransportCredentialOnly 时,在 WCF REST 中创建自定义 IIdentity
Create a custom IIdentity in WCF REST when binding security is TransportCredentialOnly
我需要实现一个使用 HTTP 基本身份验证的 REST 服务。由于它是建立在现有基础结构之上的,因此我需要将其实现为 WCF 服务。出于向后兼容和集成到现有生态系统的原因,我需要将用户名和密码都传递给服务(此时请不要介意可能的安全隐患)。由于默认情况下身份验证信息被 WCF 运行时从 header 中删除,我的解决方案是创建一个包含密码信息的自定义 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;
}
}
我试过通过以下方式转发密码,都没有成功:
- 实施自定义
UserNamePasswordValidator
,它可以访问密码,但只能处理身份验证。无法创建或修改 IIdentity
.
- 正在创建自定义
ServiceCredentials
as described in this article,当绑定安全性设置为 Transport
时工作正常。然而,这需要与服务的 HTTPS 连接,这对我来说是不可行的,因为传输级别的安全性由上游的负载平衡器处理。服务本身必须是 HTTP。因此安全设置为 TransportCredentialOnly
。其效果是自定义 ServiceCredentials
class 永远不会被 WCF 运行时初始化(与安全设置为 Transport
不同)。
- 直接在
app.config
中配置自定义 AuthorizationPoliciy
。在这种情况下,自定义授权策略被初始化,但它在密码信息已经不可用的地方被调用(当它用 ServiceCredentials
初始化时这不是问题,因为它确实收到密码在初始化期间)。
自定义ServiceCredentials
和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;
}
}
}
我用的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
,但我愿意接受其他建议。
是否也可以通过 cookie 发送信息,您可以尝试以下操作,
服务端
创建一个实现 IDispatchMessageInspector
的 class
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
}
请注意,根据 linked MSDN link、
,第 OperationContext.IncomingMessageProperties Property 行可用于获取消息的传入消息属性
Use this property to inspect or modify the message properties for a request message in a service operation or a reply message in a client proxy
,然后创建一个实现 IServiceBehvaior 的 class,例如
public class 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());
}
}
}
,然后继续将其添加到您的网站。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 并修改
public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request,
System.ServiceModel.IClientChannel channel)
将凭据添加到客户端代码的方法。
接下来,将其添加到实现 IEndpointBehavior、
的 class
internal class InterceptorBehaviorExtension : BehaviorExtensionElement, IEndpointBehavior
并修改
public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(new CookieMessageInspector());
}
方法,然后将以上代码添加到您的 WCF 客户端代码中的端点行为列表中,
虽然我想您可以使用 HttpClient 或 WebClient 添加代码并在连接到服务时使用它来提供凭据。
更新:
解决方案的关键是从这一行的原始 HTTP 消息中获取 headers:
var messageProperty = (HttpRequestMessageProperty)OperationContext.Current
.IncomingMessageProperties[HttpRequestMessageProperty.Name];
这允许您像这样访问授权 header:
string authorization = message.Headers.Get("Authorization");
由于 OperationContext
可从服务本身读取,因此可以直接从服务读取和解析授权数据。在基本身份验证的情况下,这包括用户名和密码。不需要消息检查器(尽管您需要一个额外的 UserNamePasswordValidator
来忽略验证密码)。
我需要实现一个使用 HTTP 基本身份验证的 REST 服务。由于它是建立在现有基础结构之上的,因此我需要将其实现为 WCF 服务。出于向后兼容和集成到现有生态系统的原因,我需要将用户名和密码都传递给服务(此时请不要介意可能的安全隐患)。由于默认情况下身份验证信息被 WCF 运行时从 header 中删除,我的解决方案是创建一个包含密码信息的自定义 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;
}
}
我试过通过以下方式转发密码,都没有成功:
- 实施自定义
UserNamePasswordValidator
,它可以访问密码,但只能处理身份验证。无法创建或修改IIdentity
. - 正在创建自定义
ServiceCredentials
as described in this article,当绑定安全性设置为Transport
时工作正常。然而,这需要与服务的 HTTPS 连接,这对我来说是不可行的,因为传输级别的安全性由上游的负载平衡器处理。服务本身必须是 HTTP。因此安全设置为TransportCredentialOnly
。其效果是自定义ServiceCredentials
class 永远不会被 WCF 运行时初始化(与安全设置为Transport
不同)。 - 直接在
app.config
中配置自定义AuthorizationPoliciy
。在这种情况下,自定义授权策略被初始化,但它在密码信息已经不可用的地方被调用(当它用ServiceCredentials
初始化时这不是问题,因为它确实收到密码在初始化期间)。
自定义ServiceCredentials
和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;
}
}
}
我用的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
,但我愿意接受其他建议。
是否也可以通过 cookie 发送信息,您可以尝试以下操作,
服务端
创建一个实现 IDispatchMessageInspector
的 classpublic 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
}
请注意,根据 linked MSDN link、
,第 OperationContext.IncomingMessageProperties Property 行可用于获取消息的传入消息属性Use this property to inspect or modify the message properties for a request message in a service operation or a reply message in a client proxy
,然后创建一个实现 IServiceBehvaior 的 class,例如
public class 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());
}
}
}
,然后继续将其添加到您的网站。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 并修改
public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
将凭据添加到客户端代码的方法。
接下来,将其添加到实现 IEndpointBehavior、
的 classinternal class InterceptorBehaviorExtension : BehaviorExtensionElement, IEndpointBehavior
并修改
public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(new CookieMessageInspector());
}
方法,然后将以上代码添加到您的 WCF 客户端代码中的端点行为列表中, 虽然我想您可以使用 HttpClient 或 WebClient 添加代码并在连接到服务时使用它来提供凭据。
更新:
解决方案的关键是从这一行的原始 HTTP 消息中获取 headers:
var messageProperty = (HttpRequestMessageProperty)OperationContext.Current
.IncomingMessageProperties[HttpRequestMessageProperty.Name];
这允许您像这样访问授权 header:
string authorization = message.Headers.Get("Authorization");
由于 OperationContext
可从服务本身读取,因此可以直接从服务读取和解析授权数据。在基本身份验证的情况下,这包括用户名和密码。不需要消息检查器(尽管您需要一个额外的 UserNamePasswordValidator
来忽略验证密码)。