Dej*_*vić 5 c# wcf basic-authentication wshttpbinding http-headers
我一直在关注本教程,以便在我的 WCF 服务中使用传输安全进行用户名身份验证。然而,本教程提到使用basicHttpBinding哪个是不可接受的 - 我需要wsHttpBinding.
这个想法是BasicAuthenticationModule对 WCF 服务进行自定义,该服务将从 HTTP 请求中读取“授权”标头并根据“授权”标头内容执行身份验证过程。问题是缺少“授权”标题!
我已IClientMessageInspector通过自定义行为实现,以便操作传出消息并添加自定义 SOAP 标头。我在BeforeSendRequest函数中添加了以下代码:
HttpRequestMessageProperty httpRequest = request.Properties.Where(x => x.Key == "httpRequest").Single().Value;
httpRequest.Headers.Add("CustomHeader", "CustomValue");
Run Code Online (Sandbox Code Playgroud)
这应该有效,并且根据许多网络资源,它适用于basicHttpBinding但不适用于wsHttpBinding. 当我说“有效”时,我的意思是 WCF 服务成功接收到标头。
这是在 WCF 服务端检查接收到的 HTTP 消息的简化函数:
public void OnAuthenticateRequest(object source, EventArgs eventArgs)
{
HttpApplication app = (HttpApplication)source;
//the Authorization header is checked if present
string authHeader = app.Request.Headers["Authorization"];
if (string.IsNullOrEmpty(authHeader))
{
app.Response.StatusCode = 401;
app.Response.End();
}
}
Run Code Online (Sandbox Code Playgroud)
2011 年 9 月该线程的底部帖子说,这是不可能的wsHttpBinding。我不想接受那个回应。
作为旁注,如果我使用 IIS 中内置的基本身份验证模块而不是自定义模块,我会得到
参数 'username' 不能包含逗号。** 尝试时的错误消息
Roles.IsInRole("RoleName")或 `[PrincipalPermission(SecurityAction.Demand, Role = "RoleName")]
可能是因为我的PrimaryIdentity.Name属性包含证书主题名称,因为我将TransportWithMessageCredential安全性与基于证书的消息安全性结合使用。
我愿意接受建议以及解决问题的替代方法。谢谢。
更新
看起来,HTTP 标头稍后会在整个 WCF 服务代码中被正确读取。
(HttpRequestMessageProperty)OperationContext.Current.IncomingMessageProperties["httpRequest"]包含我的自定义标题。但是,这已经是消息级别的了。如何将标头传递给传输身份验证例程?
更新 2
经过一番研究,我得出一个结论,当 Web 浏览器收到 HTTP 状态代码 401 时,它会向我显示登录对话框,我可以在其中指定我的凭据。但是,WCF 客户端只是抛出异常并且不想发送凭据。https://myserver/myservice/service.svc在 Internet Explorer 中访问时,我能够验证此行为。尝试使用此链接中的信息进行修复,但无济于事。这是 WCF 中的错误还是我遗漏了什么?
编辑
以下是我的system.servicemodel(来自web.config)的相关部分- 我很确定我的配置是正确的。
<serviceBehaviors>
<behavior name="ServiceBehavior">
<serviceMetadata httpsGetEnabled="true" httpGetEnabled="false" />
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceCredentials>
<clientCertificate>
<authentication certificateValidationMode="ChainTrust" revocationMode="NoCheck" />
</clientCertificate>
<serviceCertificate findValue="server.uprava.djurkovic-co.me" x509FindType="FindBySubjectName" storeLocation="LocalMachine" storeName="My" />
</serviceCredentials>
<serviceAuthorization principalPermissionMode="UseAspNetRoles" roleProviderName="AspNetSqlRoleProvider" />
</behavior>
</serviceBehaviors>
................
<wsHttpBinding>
<binding name="EndPointWSHTTP" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="20480000" maxReceivedMessageSize="20480000" messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
<readerQuotas maxDepth="20480000" maxStringContentLength="20480000" maxArrayLength="20480000" maxBytesPerRead="20480000" maxNameTableCharCount="20480000" />
<reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false" />
<security mode="TransportWithMessageCredential">
<transport clientCredentialType="Basic" />
<message clientCredentialType="Certificate" negotiateServiceCredential="true" algorithmSuite="Default" />
</security>
</binding>
</wsHttpBinding>
............
<service behaviorConfiguration="ServiceBehavior" name="DjurkovicService.Djurkovic">
<endpoint address="" binding="wsHttpBinding" bindingConfiguration="EndPointWSHTTP" name="EndPointWSHTTP" contract="DjurkovicService.IDjurkovic" />
</service>
Run Code Online (Sandbox Code Playgroud)
服务返回的异常是:
HTTP 请求未经授权,客户端身份验证方案为“匿名”。从服务器收到的身份验证标头是“基本领域,协商,NTLM”。(远程服务器返回错误:(401) Unauthorized。)
有趣的是,当我写关于上述答案的最后评论时,我停了下来。我的评论包含“...如果 HTTP 标头不包含“授权”标头,我将状态设置为 401,这会导致异常。” 我将状态设置为 401。明白了吗?解决方案一直就在那里。
即使我显式添加授权标头,初始数据包也不包含授权标头。然而,正如我在授权模块处于非活动状态时所测试的那样,每个后续数据包确实包含它。所以我想,为什么我不尝试将这个初始数据包与其他数据包区分开来呢?因此,如果我看到它是初始数据包,请将 HTTP 状态代码设置为 200(正常),如果不是,请检查身份验证标头。<t:RequestSecurityToken>这很容易,因为初始数据包在 SOAP 信封(包含标签)中发送对安全令牌的请求。
好吧,让我们看一下我的实现,以防其他人需要它。
这是 BasicAuthenticationModule 实现,它实现了 IHTTPModule:
public class UserAuthenticator : IHttpModule
{
public void Dispose()
{
}
public void Init(HttpApplication application)
{
application.AuthenticateRequest += new EventHandler(this.OnAuthenticateRequest);
application.EndRequest += new EventHandler(this.OnEndRequest);
}
public void OnAuthenticateRequest(object source, EventArgs eventArgs)
{
HttpApplication app = (HttpApplication)source;
// Get the request stream
Stream httpStream = app.Request.InputStream;
// I converted the stream to string so I can search for a known substring
byte[] byteStream = new byte[httpStream.Length];
httpStream.Read(byteStream, 0, (int)httpStream.Length);
string strRequest = Encoding.ASCII.GetString(byteStream);
// This is the end of the initial SOAP envelope
// Not sure if the fastest way to do this but works fine
int idx = strRequest.IndexOf("</t:RequestSecurityToken></s:Body></s:Envelope>", 0);
httpStream.Seek(0, SeekOrigin.Begin);
if (idx != -1)
{
// Initial packet found, do nothing (HTTP status code is set to 200)
return;
}
//the Authorization header is checked if present
string authHeader = app.Request.Headers["Authorization"];
if (!string.IsNullOrEmpty(authHeader))
{
if (authHeader == null || authHeader.Length == 0)
{
// No credentials; anonymous request
return;
}
authHeader = authHeader.Trim();
if (authHeader.IndexOf("Basic", 0) != 0)
{
// the header doesn't contain basic authorization token
// we will pass it along and
// assume someone else will handle it
return;
}
string encodedCredentials = authHeader.Substring(6);
byte[] decodedBytes = Convert.FromBase64String(encodedCredentials);
string s = new ASCIIEncoding().GetString(decodedBytes);
string[] userPass = s.Split(new char[] { ':' });
string username = userPass[0];
string password = userPass[1];
// the user is validated against the SqlMemberShipProvider
// If it is validated then the roles are retrieved from
// the role provider and a generic principal is created
// the generic principal is assigned to the user context
// of the application
if (Membership.ValidateUser(username, password))
{
string[] roles = Roles.GetRolesForUser(username);
app.Context.User = new GenericPrincipal(new
GenericIdentity(username, "Membership Provider"), roles);
}
else
{
DenyAccess(app);
return;
}
}
else
{
app.Response.StatusCode = 401;
app.Response.End();
}
}
public void OnEndRequest(object source, EventArgs eventArgs)
{
// The authorization header is not present.
// The status of response is set to 401 Access Denied.
// We will now add the expected authorization method
// to the response header, so the client knows
// it needs to send credentials to authenticate
if (HttpContext.Current.Response.StatusCode == 401)
{
HttpContext context = HttpContext.Current;
context.Response.AddHeader("WWW-Authenticate", "Basic Realm");
}
}
private void DenyAccess(HttpApplication app)
{
app.Response.StatusCode = 403;
app.Response.StatusDescription = "Forbidden";
// Write to response stream as well, to give the user
// visual indication of error
app.Response.Write("403 Forbidden");
app.CompleteRequest();
}
}
Run Code Online (Sandbox Code Playgroud)
重要:为了让我们能够读取http请求流,ASP.NET兼容性一定不能启用ASP.NET兼容性。
要使 IIS 加载此模块,您必须将其添加到<system.webServer>web.config 部分,如下所示:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<remove name="BasicAuthenticationModule" />
<add name="BasicAuthenticationModule" type="UserAuthenticator" />
</modules>
Run Code Online (Sandbox Code Playgroud)
但在此之前,您必须确保BasicAuthenticationModule该部分没有被锁定,并且默认情况下应该是锁定的。如果它被锁定,您将无法更换它。
解锁模块:(注意:我使用的是 IIS 7.5)
在客户端,您需要能够将自定义 HTTP 标头添加到传出消息中。执行此操作的最佳方法是实现 IClientMessageInspector 并使用该BeforeSendRequest函数添加标头。我不会解释如何实现 IClientMessageInspector,网上有大量关于该主题的资源。
要将“Authorization”HTTP 标头添加到消息中,请执行以下操作:
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
// Making sure we have a HttpRequestMessageProperty
HttpRequestMessageProperty httpRequestMessageProperty;
if (request.Properties.ContainsKey(HttpRequestMessageProperty.Name))
{
httpRequestMessageProperty = request.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
if (httpRequestMessageProperty == null)
{
httpRequestMessageProperty = new HttpRequestMessageProperty();
request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessageProperty);
}
}
else
{
httpRequestMessageProperty = new HttpRequestMessageProperty();
request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessageProperty);
}
// Add the authorization header to the WCF request
httpRequestMessageProperty.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes(Service.Proxy.ClientCredentials.UserName.UserName + ":" + Service.Proxy.ClientCredentials.UserName.Password)));
return null;
}
Run Code Online (Sandbox Code Playgroud)
就这样,花了一段时间才解决,但这是值得的,因为我在网上发现了许多类似的未解答的问题。