自托管WCF REST服务和基本身份验证

nbe*_*ans 11 authentication rest wcf basic-authentication wcf-rest-contrib

我已经创建了一个自托管的WCF REST服务(其中一些来自WCF REST Starter Kit Preview 2).这一切都很好.

我现在正在尝试向服务添加基本身份验证.但是我在WCF堆栈中遇到了一些相当大的障碍,导致我不能这样做.

似乎HttpListener(自托管WCF服务在WCF堆栈中内部使用较低级别)阻止了我WWW-Authenticate在自生成401 Unauthorized响应上插入标头的尝试.为什么?

如果我忘记了这个WWW-Authenticate标题(微软似乎也这样做了),我可以让身份验证工作.但这就是问题所在.如果我没有发回WWW-Authenticate标题,那么Web浏览器将不会显示其标准的"登录"对话框.用户将只面对401 Unauthorized错误页面而无法实际登录.

计算机和人类都应该可以访问REST服务(至少在GET请求级别).因此,我觉得WCF REST在这里并不遵守REST的基本部分.有人同意我的意见吗?

有没有人使用自托管WCF REST服务进行基本身份验证?如果是这样,你是怎么做到的?

PS:显然我打算使用不安全的基本身份验证的前提是我也会为我的服务提供HTTPS/SSL.但那是另一回事.

PPS:我已经尝试过WCF REST Contrib(http://wcfrestcontrib.codeplex.com/),但问题完全相同.看来此库尚未在自托管方案中进行测试.

谢谢.

nbe*_*ans 13

不幸的是,我已经确定(通过分析WCF参考源代码和用于HTTP会话嗅探的Fiddler工具的帮助),这是WCF堆栈中的一个错误.

使用Fiddler,我注意到我的WCF服务的行为与使用基本身份验证的任何其他网站不同.

要清楚,这应该是应该发生的事情:

  1. 浏览器发送GET请求时不知道甚至需要密码.
  2. Web服务器拒绝具有401 Unauthorized状态的请求,并包含WWW-Authenticate包含有关可接受的身份验证方法的信息的标头.
  3. 浏览器提示用户输入凭据.
  4. 浏览器重新发送GET请求并包含Authentication带有凭据的适当标头.
  5. 如果凭据正确,则Web服务器将响应200 OK并显示网页.如果凭据错误,Web服务器将响应401 Unauthorized并包含与WWW-Authenticate步骤2中相同的标头.

我的WCF服务实际上发生的是:

  1. 浏览器发送GET请求时不知道甚至需要密码.
  2. WCF注意到Authentication请求中没有标头,并盲目拒绝具有401 Unauthorized状态的请求并包含WWW-Authenticate标头.到目前为止一切正常.
  3. 浏览器会提示用户输入凭据.还是正常的.
  4. 浏览器重新发送GET请求,包括相应的Authentication标头.
  5. 如果凭据正确,则Web服务器响应200 OK.一切都很好.但是,如果凭据错误,则WCF会响应403 Forbidden并且不包含任何其他标头,例如WWW-Authenticate.

当浏览器获得403 Forbidden状态时,它不会认为这是一次失败的身份验证尝试.此状态代码旨在通知浏览器其尝试访问的URL不受限制.它与任何方式的身份验证无关.这具有可怕的副作用,当用户错误地键入其用户名/密码(并且服务器拒绝403)时,Web浏览器不会重新提示用户再次键入其凭据.事实上,Web浏览器认为身份验证已成功,因此会在会话的其余部分存储这些凭据!

考虑到这一点,我寻求澄清:

RFC 2617(http://www.faqs.org/rfcs/rfc2617.html#ixzz0eboUfnrl)没有提到使用403 Forbidden状态代码的任何地方.事实上,它在这个问题上实际上要说的是:

如果源服务器不希望接受随请求一起发送的凭证,则它应该返回401(未授权)响应.响应必须包括WWW-Authenticate头字段,其包含适用于所请求资源的至少一个(可能是新的)质询.

WCF没有这些.它既没有正确发送401 Unauthorized状态代码.它也不包括WWW-Authenticate标题.

现在在WCF源代码中找到冒烟的枪:

我发现在HttpRequestContext类中有一个名为的方法ProcessAuthentication,其中包含以下内容(摘录):

if (!authenticationSucceeded) 
{
   SendResponseAndClose(HttpStatusCode.Forbidden);
}
Run Code Online (Sandbox Code Playgroud)

我在很多方面为微软辩护,但这是不可原谅的.

幸运的是,我让它达到了"可接受"的水平.这只是意味着如果用户意外地错误地输入了他们的用户名/密码,那么获得另一次尝试的唯一方法是完全关闭他们的Web浏览器并重新启动它以重试.所有这些都是因为根据规范,WCF 没有使用a 401 UnauthorizedWWW-Authenticate标头响应失败的身份验证尝试.


小智 6

我发现基于这一解决方案链接.

第一种方法是在一个名为CustomUserNameValidator的类中覆盖方法Validate,该类继承自System.IdentityModel.Selectors.UserNamePasswordValidator:

Imports System.IdentityModel.Selectors
Imports System.ServiceModel
Imports System.Net

Public Class CustomUserNameValidator
    Inherits UserNamePasswordValidator

Public Overrides Sub Validate(userName As String, password As String)
    If Nothing = userName OrElse Nothing = password Then
        Throw New ArgumentNullException()
    End If

    If Not (userName = "user" AndAlso password = "password") Then
        Dim exep As New AddressAccessDeniedException("Incorrect user or password")
        exep.Data("HttpStatusCode") = HttpStatusCode.Unauthorized
        Throw exep
    End If
End Sub
End Class
Run Code Online (Sandbox Code Playgroud)

诀窍是将异常"HttpStatusCode"的属性更改为HttpStatusCode.Unauthorized

第二个是在App.config中创建serviceBehavior,如下所示:

<behaviors>
  <endpointBehaviors>
    <behavior name="HttpEnableBehavior">
      <webHttp />
    </behavior>
  </endpointBehaviors>
  <serviceBehaviors>
    <behavior name="SimpleServiceBehavior">
      <serviceMetadata httpGetEnabled="true"/>
      <serviceDebug includeExceptionDetailInFaults="false"/>
      <serviceCredentials>
        <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="Service.CustomUserNameValidator, Service"/>
      </serviceCredentials>
    </behavior>
  </serviceBehaviors>
</behaviors>
Run Code Online (Sandbox Code Playgroud)

最后,我们必须指定webHttpBinding的配置并将其应用于端点:

<bindings>
  <webHttpBinding>
    <binding name="basicAuthBinding">
      <security mode="TransportCredentialOnly">
        <transport clientCredentialType="Basic" realm=""/>
      </security>
    </binding>
  </webHttpBinding>
</bindings>

<service behaviorConfiguration="SimpleServiceBehavior" name="Service.webService">
    <host>
      <baseAddresses>
        <add baseAddress="http://localhost:8000/webService" />
      </baseAddresses>
    </host>
    <endpoint address="http://localhost:8000/webService/restful" binding="webHttpBinding" bindingConfiguration="basicAuthBinding" contract="Service.IwebService" behaviorConfiguration="HttpEnableBehavior"/>
</service>
Run Code Online (Sandbox Code Playgroud)