【问题标题】:WCF Service with wsHttpBinding - Manipulating HTTP request headers带有 wsHttpBinding 的 WCF 服务 - 操作 HTTP 请求标头
【发布时间】:2012-02-25 01:12:29
【问题描述】:

我一直在关注this 教程,以便在我的 WCF 服务中使用传输安全性获得用户名身份验证。然而,本教程提到使用 basicHttpBinding,这是不可接受的 - 我需要 wsHttpBinding

这个想法是在 WCF 服务上有一个自定义的BasicAuthenticationModule,它将从 HTTP 请求中读取“授权”标头,并根据“授权”标头内容执行身份验证过程。问题是缺少“授权”标头!

我通过自定义行为实现了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();
        }
    }

this 线程日期为 2011 年 9 月的底部帖子说,wsHttpBinding 无法做到这一点。我不想接受那个回复。

附带说明,如果我使用 IIS 中内置的基本身份验证模块而不是自定义模块,我会得到 ​​p>

参数“用户名”不能包含逗号。** 尝试Roles.IsInRole("RoleName") 或 `[PrincipalPermission(SecurityAction.Demand, Role = "RoleName")] 时出现错误消息

可能是因为我的 PrimaryIdentity.Name 属性包含证书主题名称,因为我正在使用 TransportWithMessageCredential 安全性和基于证书的消息安全性。

我愿意接受建议以及解决问题的替代方法。谢谢。

更新

看起来,HTTP 标头稍后会在整个 WCF 服务代码中正确读取。 (HttpRequestMessageProperty)OperationContext.Current.IncomingMessageProperties["httpRequest"] 包含我的自定义标题。但是,这已经是消息级别的了。如何将标头传递给传输身份验证例程?

更新 2
经过一番研究,我得出一个结论,当 Web 浏览器收到 HTTP 状态代码 401 时,它会向我显示登录对话框,我可以在其中指定我的凭据。然而,WCF 客户端只是抛出一个异常并且不想发送凭据。在 Internet Explorer 中访问 https://myserver/myservice/service.svc 时,我能够验证此行为。尝试使用来自this 链接的信息进行修复,但无济于事。这是 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) Unauthorized。)

【问题讨论】:

标签: c# wcf http-headers basic-authentication wshttpbinding


【解决方案1】:

有趣的是,当我写关于上述答案的最后一条评论时,我停了一会儿。我的评论包含“...如果 HTTP 标头不包含“授权”标头,我将状态设置为 401,这会导致异常。” 我将状态设置为 401。 明白了吗?解决方案一直存在。

即使我明确添加,初始数据包也不包含授权标头。然而,每个后续数据包都包含它,因为我在授权模块处于非活动状态时进行了测试。所以我为什么不尝试将这个初始数据包与其他数据包区分开来呢?因此,如果我看到它是初始数据包,请将 HTTP 状态代码设置为 200(OK),如果不是 - 检查身份验证标头。这很简单,因为初始数据包在 SOAP 信封(包含 &lt;t:RequestSecurityToken&gt; 标记)中发送对安全令牌的请求。

好的,让我们看看我的实现,以防其他人需要它。

这是 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();
    }
}

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

要让您的 IIS 加载此模块,您必须将其添加到 web.config 的 &lt;system.webServer&gt; 部分,如下所示:

<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. 在“项目”列表中,找到“BasicAuthenticationModule”并在右侧窗格中单击“解锁项目”(如果存在!)。
  7. 如果您更改了此设置,请关闭配置编辑器并保存更改。

在客户端,您需要能够将自定义 HTTP 标头添加到传出消息中。最好的方法是实现 IClientMessageInspector 并使用 BeforeSendRequest 函数添加您的标头。我不会解释如何实现 IClientMessageInspector,网上有很多关于该主题的资源。

要将“授权”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;
    }    

你去吧,花了一些时间来解决,但这是值得的,因为我在整个网络上发现了许多类似的未回答的问题。

【讨论】:

  • 这是 WCF 的蹩脚行为,当您无法将其配置为从您的第一条消息开始发送那些 authorization 标头时。而你像猴子一样,必须发明一个轮子。
【解决方案2】:

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

【讨论】:

  • 实际上这是我为了实现这一目标而阅读的第一篇文章之一。我已将 web.config 中的相关 system.servicemodel 部分添加到问题中。
  • 根据错误,听起来您的客户端 system.serviceModel 元素与服务配置的内容不匹配。如果您使用的是 Visual Studio 服务引用,请记住在对服务 system.serviceModel 配置进行任何更改后更新服务引用。否则,只需确保服务和客户端之间的服务元素保持一致即可。
  • 也不是。我绝对确定服务元素在服务和客户端之间是一致的。抛出该错误是因为我实现了自定义 HTTP 模块来验证连接用户。如果 HTTP 标头不包含“Authorization”标头,我将状态设置为 401,这会导致异常。
猜你喜欢
  • 1970-01-01
  • 2013-09-23
  • 1970-01-01
  • 1970-01-01
  • 2021-08-21
  • 2017-04-03
  • 1970-01-01
  • 2012-12-01
  • 2013-08-03
相关资源
最近更新 更多