使用wsHttpBinding的WCF服务 - 操作HTTP请求头

6
我一直在跟随这个教程(链接)来实现我的WCF服务中的用户名认证和传输安全。但是该教程使用的是不可接受的basicHttpBinding,我需要使用wsHttpBinding
我的想法是在WCF服务中创建一个自定义的BasicAuthenticationModule,它将从HTTP请求中读取“Authorization”头并根据其内容执行身份验证过程。问题是,“Authorization”头丢失了!
我通过自定义行为实现了IClientMessageInspector以操作输出消息并添加自定义SOAP头。我在BeforeSendRequest函数中添加了以下代码:
    HttpRequestMessageProperty httpRequest = request.Properties.Where(x => x.Key == "httpRequest").Single().Value;
    httpRequest.Headers.Add("CustomHeader", "CustomValue");

这应该可以行得通,根据许多网络资源的说法,它适用于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();
        }
    }
此帖底部的文章显示,使用wsHttpBinding不可能实现。我不想接受这个答复。
顺便说一下,如果我使用IIS中内置的Basic身份验证模块而不是自定义模块,则在尝试Roles.IsInRole("RoleName")或`[PrincipalPermission(SecurityAction.Demand, Role = "RoleName")]`时会收到错误消息“参数'username'不能包含逗号。”,这可能是因为我的PrimaryIdentity.Name属性包含证书主题名称,因为我正在使用基于证书的消息安全性的TransportWithMessageCredential安全性。
我愿意听取建议以及解决该问题的替代方法。谢谢。 更新 似乎在WCF服务代码后面正确读取了HTTP头。 (HttpRequestMessageProperty)OperationContext.Current.IncomingMessageProperties["httpRequest"]包含我的自定义标头。但是,这已经是消息级别...如何将标头传递给传输认证例程? 更新2
经过一番研究,我得出结论,当Web浏览器收到HTTP状态码401时,它会向我呈现登录对话框,我可以在其中指定我的凭据。但是,WCF客户端仅引发异常,不想发送凭据。当在Internet Explorer中访问https://myserver/myservice/service.svc时,我能够验证此行为。尝试使用链接中的信息进行修复,但无济于事。这是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>

服务返回的异常是:

HTTP 请求未经授权,客户端身份验证方案为“匿名”。从服务器接收的身份验证标头为“Basic Realm,Negotiate,NTLM”。(远程服务器返回错误:(401) 未授权。)


与此问题相关:http://social.msdn.microsoft.com/Forums/en-US/wcf/thread/ec572181-a91a-4b22-9671-070a766f3def - Dejan Janjušević
2个回答

3
有趣的是,在我写完上面的评论后,我停了一会儿。我的评论中包含“……如果HTTP头部不包含“Authorization”头部,我将状态设置为401,这会导致异常。”我将状态设置为401。懂了吗?解决方案一直在那里。
即使我明确添加它,初始数据包也不包含授权标头。但是,每个后续数据包都含有它,因为我在禁用授权模块时进行了测试。所以我想,为什么不尝试区分这个初始数据包和其他数据包呢?因此,如果我看到它是初始数据包,则将HTTP状态代码设置为200(OK),如果不是,则检查身份验证标头。那很容易,因为初始数据包在SOAP信封中发送安全令牌请求(包含<t:RequestSecurityToken>标记)。
好的,现在让我们来看看我实现的内容,以防其他人需要它。
这是实现IHTTPModule的BasicAuthenticationModule实现:
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();
    }
}

重要提示:为了使我们能够读取http请求流,不能启用ASP.NET兼容性。

如果要让IIS加载此模块,必须将其添加到web.config的<system.webServer>部分中,如下所示:

<system.webServer>
  <modules runAllManagedModulesForAllRequests="true">
    <remove name="BasicAuthenticationModule" />
    <add name="BasicAuthenticationModule" type="UserAuthenticator" />
  </modules>

但在此之前,您必须确保BasicAuthenticationModule部分未被锁定,通常情况下它应该是被锁定的。如果它被锁定了,您将无法替换它。
为了解锁模块(注:我使用的是IIS 7.5):
  1. 打开IIS管理器
  2. 在左侧面板中,单击您的主机名
  3. 在中间面板中,在“管理”部分下,打开“配置编辑器”
  4. 单击上面板区域中“Section”标签旁边的组合框,展开“system.webServer”,然后导航到“modules”
  5. 在“(Collection)”键下,单击“(Count=nn)”值,以使一个带有“...”按钮的小窗口出现。单击它。
  6. 在“Items”列表中,找到“BasicAuthenticationModule”,并在右面板中单击“Unlock Item”(如果有!)
  7. 如果您更改了此设置,请关闭配置编辑器,并保存更改。
在客户端上,您需要能够向传出消息添加自定义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;
    }    

好的,已经完成翻译。这个问题解决起来花了点时间,但是还是值得的,因为我在网络上发现了很多类似的未被回答的问题。


2
这是WCF的行为非常糟糕,当你无法配置它从第一条消息开始发送authorization头时。你就像一只猴子一样,不得不重新发明轮子。 - Johnny_D

0

您正在尝试实现HTTP身份验证,因此请查看MSDN文章以确保您已正确配置服务。正如您所发现的那样,您参考的教程适用于basicHttpBinding,但wsHttpBinding需要特殊配置才能支持HTTP身份验证。


实际上,那是我为了达到这个目的阅读的第一篇文章之一。我已经将相关的web.config中的system.servicemodel部分添加到问题中。 - Dejan Janjušević
根据错误信息,看起来你的客户端 system.serviceModel 元素与服务配置不匹配。如果你正在使用 Visual Studio 服务引用,请记得在对服务 system.serviceModel 配置进行任何更改后更新服务引用。否则,请确保服务元素在服务端和客户端之间保持一致。 - Sixto Saez
也不是这个问题。我非常确定服务元素在服务端和客户端之间是一致的。那个错误是因为我实现了自定义的HTTP模块来验证连接的用户。如果HTTP头部不包含“Authorization”头部,我会将状态设置为401,从而导致异常。 - Dejan Janjušević

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接