65.9K
CodeProject 正在变化。 阅读更多。
Home

WCF .NET 4.0 控制台托管的带身份验证和角色的 Json Rest 安全 HTTP Web 服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (19投票s)

2011 年 12 月 6 日

CPOL

9分钟阅读

viewsIcon

95849

downloadIcon

3406

一个使用最新 .NET 平台功能(包括复杂类型的自动序列化/反序列化)的 RESTful Web 服务。

引言

本文涵盖了在控制台应用程序中(不依赖 IIS)创建 SSL 安全的 .NET 4.0 HTTP JSON REST Web 服务。

该服务还提供了一个套接字策略文件服务器,Adobe Flash 客户端(使用 as3httpclientlib)连接时需要。

还提供了一个 Flash 客户端应用程序 (WebServiceSubscribe) 供下载,演示如何连接到 Web 服务。其他任何客户端类型当然也可以连接。

背景

我想分享这些代码的原因是,达到现在这个程度花费了非常长的时间。网上有很多关于在 WCF 中创建 RESTful 服务的教程和代码片段,但没有一个涵盖了在互联网托管服务中实际想要利用的所有功能——只有零散的部分。它们中的很多还以奇怪的方式实现功能,试图让 WCF 做它不想做或不应该做的事情。

很多复杂性都归结于配置——如果配置得当,事情就会简单而顺利地协同工作。

那么,我们开始吧!

Using the Code

让服务启动并运行最重要的部分是配置。WCF Rest Service Template 4.0 提供的配置不足以完成任务,因为它没有提供任何安全或身份验证的配置。当您确实需要这些功能时,您的配置看起来会大不相同。

这是配置

<system.serviceModel>
    <services>
      <service name="WtfService.WtfSvc" behaviorConfiguration="WtfServiceBehaviour" >
        <endpoint address=https://:8000/WtfService 
    binding="webHttpBinding" bindingConfiguration="wtfSslBinding" 
    behaviorConfiguration="WebHttpBehaviour" contract="WtfService.WtfSvc">
        </endpoint>
      </service>
    </services>
    <bindings>
      <webHttpBinding>
        <binding name ="wtfSslBinding">
          <security mode="Transport">
            <transport clientCredentialType="Basic" />
          </security>
        </binding>
      </webHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="WtfServiceBehaviour" >
          <serviceDebug includeExceptionDetailInFaults="true"/>
          <serviceCredentials >
            <userNameAuthentication userNamePasswordValidationMode="Custom" 
        customUserNamePasswordValidatorType=
        "WtfService.WtfUserNamePasswordValidator, WtfService" />
          </serviceCredentials>
          <serviceAuthorization principalPermissionMode="Custom">
            <authorizationPolicies>
              <add policyType="WtfService.WtfAuthorizationPolicy, WtfService" />
            </authorizationPolicies>
          </serviceAuthorization>
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="WebHttpBehaviour">
          <webHttp automaticFormatSelectionEnabled="false" 
        defaultBodyStyle="Wrapped" defaultOutgoingResponseFormat="Json" 
        helpEnabled="true" />
        </behavior>
      </endpointBehaviors>
    </behaviors>
  </system.serviceModel> 

配置解释(如果您已经知道这些含义,可以跳过)

services.service

  • service.behaviourConfiguration - 引用 behaviors.serviceBehaviors ("WtfServiceBehaviour")

services.service.endpoint

  • endpoint.address - 指定服务器将监听请求的地址
  • endpoint.binding - 服务将使用的绑定类型。webHttpBinding 是 .NET 提供的绑定类型。请参阅 http://msdn.microsoft.com/en-us/library/bb412176.aspx
  • endpoint.bindingConfiguration - 指向我们 webHttpBinding 的设置 - 设置在 bindings.webHttpBinding.binding ("wtfSslBinding") 中设置
  • endpoint.behaviorConfiguration - 指向我们 webHttp endpoint 的行为 - 在 endpointBehaviors.behavior ("WebHttpBehaviour") 中设置
  • endpoint.contract - 是我们的 ServiceContract 类的运行时类型

bindings.webHttpBinding.binding

  • security.mode - "Transport" 表示传输层由 SSL 保护
  • security.transport.clientCredentialType - "Basic" 表示我们使用 SSL 上的基本身份验证

behaviors.serviceBehavior.behavior

  • serviceDebug.includeExceptionDetailsInFaults - 仅在开发环境设置为 true
  • serviceCredentials.userNameAuthentication.userNamePasswordValidationMode - "Custom" 表示我们正在实现自己的用户/密码验证类 - 例如,我们可以查询数据库而不是使用基于 Windows 的身份验证
  • serviceCredentials.customUserNamePasswordValidatorType - 我们验证凭据的运行时类
  • serviceAuthorization.principalPermissionMode - "Custom" 表示我们可以为基于角色的服务方法访问实现自己的授权策略
  • serviceAuthorization.authorizationPolicies.add.policyType - 需要实现此类别,以便我们可以将自己的 iPrincipal 实现与运行线程关联,从而我们可以控制其 "IsUserInRole" 方法以实现基于角色的安全性。

endpointBehaviors.behavior

  • webHttp.defaultBodyStyle - "Wrapped" 表示我们的 JSON 结果将包含在 JSON 对象中,例如 "{}"
  • webHttp.defaultOutgoingResponseFormat - "Json" 表示我们的服务以 JSON 而不是 XML 提供响应
  • webHttp.helpEnabled - "true" 表示用户可以浏览到例如 https://:8000/WtfService/Help,框架将自动生成一个描述 Web 服务功能的帮助页面

从控制台应用程序实例化服务

 class Program
    {
        static void Main(string[] args)
        {
            SocketPolicyFileServer server = new SocketPolicyFileServer();
            server.Start();

            var host = new ServiceHost(typeof(WtfSvc));
            host.Open();

            Console.WriteLine("The service is ready at {0}", 
            host.Description.Endpoints[0].ListenUri);
            Console.WriteLine("Press <Enter> to stop the service.");
            Console.ReadLine();

            //Close the ServiceHost.
            host.Close();            
        }
    } 

因为我们在配置文件中设置了所有内容,所以实例化服务非常简单。我们只需创建一个新的 ServiceHost 并传入服务类型。

SocketPolicyFileServer 是我们创建的另一个服务,用于允许 Flash 客户端连接,它通过 TCP 端口 843 提供 SocketPolicyFile.xml 文件。此套接字策略代码的功劳归 http://socketpolicyfile.codeplex.com/ 所有。

密码验证

密码验证发生在覆盖 UserNamePasswordValidator 的类中,并在 config 文件中引用。它看起来像这样——显然您会想要在此处查询真实的数据库

/// <summary>
/// Here is where we actually validate the password
/// </summary>
class WtfUserNamePasswordValidator : UserNamePasswordValidator
{  
    public override void Validate(string userName, string password)
    {
        if (string.IsNullOrEmpty(userName) | string.IsNullOrEmpty(password))
            throw new ArgumentNullException();

        //validate the username and password here against the database
        if (userName != "John" | password != "Doe")
            // This throws an informative fault to the client.
            throw new FaultException("Unknown Username or Incorrect Password");
            // When you do not want to throw an informative fault to the client,
            // throw the following exception.
            // throw new SecurityTokenException("Unknown Username or Incorrect Password");
    }
} 

基于角色的身份验证

密码验证只对用户进行身份验证。但我们还需要对 Web 服务方法进行基于角色的访问。解决此问题的第一步是创建我们自己的 IPrincipal 接口实现,如下所示

public class WtfPrincipal : IPrincipal
    {
        IIdentity _identity;
        string[] _roles;

        public WtfPrincipal(IIdentity identity)
        {
            _identity = identity;
            LoadUsersRoles();
        }
        
        /// <summary>
        /// we should load up the user's roles from the database here based on username
        /// </summary>
        private void LoadUsersRoles()
        {
            //run query on db using _identity.Name
            //then put into _roles
            _roles = new string[] { "WtfUser" };
        }

        public IIdentity Identity
        {
            get { return _identity; }
        }

        public bool IsInRole(string role)
        {
            if (_roles.Contains(role))
                return true;
            else
                return false;   
        }

        public string[] Roles
        {
            get { return _roles; }
        }
    } 

上述代码的重要部分是根据数据库加载用户的角色,然后会调用 IsInRole 来检查用户是否具有相关角色。

要将此 IPrincipal 推送到 WCF 通道中,我们在实现的 IAuthorizationPolicy 中进行操作——并在 config 文件中再次引用。

 public class WtfAuthorizationPolicy : IAuthorizationPolicy
    {
        private static readonly ILog log = 
        LogManager.GetLogger(typeof(WtfAuthorizationPolicy));

        public bool Evaluate(EvaluationContext evaluationContext, ref object state)
        {
            // get the authenticated client identity
            IIdentity client = GetClientIdentity(evaluationContext);

            // set the custom principal
            evaluationContext.Properties["Principal"] = new WtfPrincipal(client);

            return true;
        }

        private IIdentity GetClientIdentity(EvaluationContext evaluationContext)
        {
            object obj;
            if (!evaluationContext.Properties.TryGetValue("Identities", out obj))
                throw new Exception("No Identity found");

            IList<IIdentity> identities = obj as IList<IIdentity>;
            if (identities == null || identities.Count <= 0)
                throw new Exception("No Identity found");

            return identities[0];
        }

        public System.IdentityModel.Claims.ClaimSet Issuer
        {
            get { throw new NotImplementedException(); }
        }

        public string Id
        {
            get { throw new NotImplementedException(); }
        }
    } 

WCF 将调用 Evaluate 函数,从那里我们可以获取 IIdentity,其中将包含发出服务调用的客户端的用户名。然后我们将此 IIdentity 包装在我们的自定义 IPrincipal 中,并将其设置为 evaluationContext.Properties["Principal"]。设置完成后,WCF 将自动调用我们应用于服务类的任何 PrincipalPermission 属性上的 IsUserInRole。有关此内容和代码的更多信息,请参阅此处

服务类

接下来,我们将看看我们的服务类,看看它是如何组合在一起的。请注意,您不需要将 OperationContract 属性应用于方法,也不需要将 DataContract 应用于传递给服务或从服务返回的复杂类型(例如,下面的 Person 类)。

这是代码

[ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = 
            AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    // NOTE: If the service is renamed, remember to update the global.asax.cs file
    public class WtfSvc
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(WtfSvc));

        /// <summary>
        /// Demonstrates http get where parameters are passed in the URL.
        /// Also requires the user to be in the role of 'WtfUser'.
        /// </summary>
        /// <param name="firstName"></param>
        /// <param name="lastName"></param>
        /// <returns></returns>
        [WebGet(UriTemplate = "HelloWorld/{firstName}/{lastName}")]
        [PrincipalPermission(SecurityAction.Demand, Role = "WtfUser")]
        public string HelloWorld(string firstName, string lastName)
        {
            return string.Format("Hello {0} {1}", firstName, lastName);
        }

        /// <summary>
        /// Demonstrates http post where request body contains two parameters 
        /// of simple type.
        /// Also requires the user to be in the role of 'WtfUser'
        /// </summary>
        /// <param name="firstName">comes from the json content of the body 
        /// of the post e.g. "{\"firstName\" : \"John\", \"lastName\" : \"Doe\" }"</param>
        /// <param name="lastName">comes from the json content of the body of the post 
        /// e.g. "{\"firstName\" : \"John\", \"lastName\" : \"Doe\" }"</param>
        /// <returns></returns>
        [WebInvoke(Method = "POST", UriTemplate = "HelloWorldPostSimple")]
        [PrincipalPermission(SecurityAction.Demand, Role = "WtfUser")]
        public string HelloWorldPostSimple(string firstName, string lastName)
        {
            return string.Format("Hello {0} {1}", firstName, lastName);
        }

        /// <summary>
        /// Demonstrates http post where request body contains the json string 
        /// representing an object of type Person.
        /// Also requires the user to be in the role of 'WtfUser'
        /// </summary>
        /// <param name="person">comes from the json content of the body of the post 
        ///e.g. "{\"person\":{\"FirstName\" : \"John\", \"LastName\" : \"Doe\" } }"</param>
        /// <returns></returns>
        [WebInvoke(Method = "POST", UriTemplate = "HelloWorldPostComplex")]
        [PrincipalPermission(SecurityAction.Demand, Role = "WtfUser")]
        public string HelloWorldPostComplex(Person person)
        {
            return string.Format("Hello {0} {1}", person.FirstName, person.LastName);
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    } 

请注意,我们只需要“PrincipalPermission”属性即可使用基于角色的授权来保护我们的方法。

这涵盖了服务器端。接下来我们将讨论一些配置问题,以使 SSL 启动并运行。

创建开发者自签名 SSL 证书

如果您正在运行 Windows 7,创建服务器证书非常容易。您可以从 IIS 管理器中完成此操作(尽管我们不使用 IIS 来托管它)。请按照此处提供的步骤操作

如果您没有 IIS 7.0 来创建证书,这里有一些链接,展示了如何使用 makecert 实用程序来完成此操作

为您的服务保留命名空间

详细步骤请参阅以下 MSDN 文章

但基本上,你只需要做的是

 netsh http add urlacl url=http://+:8000/WtfService user=DOMAIN\user 

使用您相关的端口、服务终结点和凭据。

使 SSL 证书在端口上运行

为了让证书在端口上运行(在我们的例子中是端口 8000),您需要遵循此 MSDN 文章中列出的步骤 - http://msdn.microsoft.com/en-us/library/ms733791.aspx(对于 mmc 管理单元,请参阅此文章 - http://msdn.microsoft.com/en-us/library/ms788967.aspx)。

基本上,你只需要做的是

netsh http add sslcert ipport=0.0.0.0:8000 
certhash=0000000000003ed9cd0c315bbb6dc1c08da5e6 
appid={00112233-4455-6677-8899-AABBCCDDEEFF} 

其中 certhash 是您刚刚创建的证书的哈希值,appid 可以是随机的 guid,或者您可以从 .NET 项目属性中的程序集信息中获取一个。

这应该就是让您的服务 SSL 端运行所需的一切。

要检查一切是否正常工作,您应该打开网络浏览器并浏览 URL https://:8000/WtfService/HelloWorld/John/Doe(凭据使用用户名 John,密码 Doe)进行测试。

ChromeGet.png

由于是自签名证书,Chrome 中会出现 HTTPS 的红叉。

Flash 客户端(如果您不使用 Flash,可以跳到最后)

使用带自签名证书的 as3httpclientlib 的一些前置条件

为了允许 Flash 客户端连接到服务,如果您使用由真实 CA 签名的证书,那么使用 as3httpclientlib 应该开箱即用。但是我们是自签名,因此 https://github.com/gabriel/as3httpclient 上的 as3httpclientlib 对您不起作用。

as3httpclientlib 是建立在 as3cryptolib 之上的,而 as3cryptolib 默认不允许自签名证书。但是浏览 as3cryptolib源代码,我发现在 TLSEngine 中使用的 TLSConfig 类中有一个配置选项,如下所示

// Test first for trust override parameters
// This nice trust override stuff comes from Joey Parrish via As3crypto forums
var certTrusted:Boolean;
if (_config.trustAllCertificates) {
    certTrusted = true; // Blatantly trust everything
} else if (_config.trustSelfSignedCertificates ) {
    // Self-signed certs
   certTrusted = firstCert.isSelfSigned(new Date); 
} else {
    // Certs with a signer in the CA store - realistically, 
    // I should setup an event chain to handle this
    certTrusted = firstCert.isSigned(_store, _config.CAStore );
}  

所以我们想在 as3httpclientlib 中将 config.trustSelfSignedCertificates 属性设置为 true

因此,您可以下载 as3cryptolib 的源代码(您需要 svn 中的源代码,而不是下载页面上的,因为那已经过时了),然后编译它。

然后下载 as3httpclientlib 的源代码,并用您刚刚编译的 as3cryptolib .swc 文件替换原来的引用(as3httpclientlib 引用中的那个已经过时,不支持此配置功能)。然后对 as3httpclientlib 中的 HTTPSocket 类进行以下修改

/**
     * Create the socket.
     * Create Socket or TLSSocket depending on URI scheme (http or https).
     */
    protected function createSocket(secure:Boolean = false):void {      
      if (secure && !_proxy) {
          var config:TLSConfig = new TLSConfig(TLSEngine.CLIENT);
          config.trustSelfSignedCertificates = true;
          _socket = new TLSSocket(null,0,config);
      }
      else _socket = new Socket();
      
      _socket.addEventListener(Event.CONNECT, onConnect);       
      _socket.addEventListener(Event.CLOSE, onClose);
      _socket.addEventListener(IOErrorEvent.IO_ERROR, onIOError);
      _socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError);
      _socket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);      
    } 

您所做的只是创建一个 TLSConfig 实例并将其传递给 TLSSocket 构造函数。然后编译 as3httpclientlib

Flash 客户端和代码

FlashClientGetAndPost.png

这里是您可以用来让 Flash 客户端连接到服务并测试功能的代码

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark" 
               xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" 
               minHeight="600" applicationComplete="init()">
    <fx:Declarations>
        <!-- Place non-visual elements (e.g., services, value objects) here -->
    </fx:Declarations>
    
    <fx:Script>
        <![CDATA[
            import com.adobe.net.*;
            import com.hurlant.util.*;
            import org.httpclient.*;
            import org.httpclient.events.*;
            import org.httpclient.http.*;
                        
            //settings
            private var baseUri:String = "https://lacey:8000/WtfService/"
            
            //http client
            private var client:HttpClient;
            private var request:HttpRequest;
            private var uri:URI;
                        
            private function init():void{
                //create the http client
                client = new HttpClient();
                client.listener.onStatus = onStatus;
                client.listener.onData = onData;
                client.listener.onComplete = onComplete;
                client.listener.onError = onError;                
            }
                
            private function onStatus(event:org.httpclient.events.HttpStatusEvent):void{
                
            }
            
            private function onData(event:org.httpclient.events.HttpDataEvent):void{
                
                this.txtResult.text += "\n" + event.readUTFBytes();
            }
            
            private function onComplete(event:org.httpclient.events.HttpResponseEvent):void{
                
            }
            
            private function onError(event:org.httpclient.events.HttpErrorEvent):void{
                
            }            
            
            protected function btnPostSimple_clickHandler(event:MouseEvent):void
            {
                //call hello at service
                uri = new URI(baseUri + "HelloWorldPostSimple");
                
                var data:ByteArray = new ByteArray();
                data.writeUTFBytes("{\"firstName\" : \"John\", \"lastName\" : \"Doe\" }");
                data.position = 0;
                
                request = new Post();
                request.contentType = "application/json";
                request.body = data;
                request.header.add("Authorization", "Basic " + Base64.encode("John:Doe"));
                
                client.request(uri, request);                
            }
            
            protected function btnPostComplex_clickHandler(event:MouseEvent):void
            {
                //call hello at service
                uri = new URI(baseUri + "HelloWorldPostComplex");
                
                var data:ByteArray = new ByteArray();
                data.writeUTFBytes
           ("{\"person\":{\"FirstName\" : \"John\", \"LastName\" : \"Doe\" } }");
                data.position = 0;
                
                request = new Post();
                request.contentType = "application/json";
                request.body = data;
                request.header.add("Authorization", "Basic " + Base64.encode("John:Doe"));
                
                client.request(uri, request);
            }
            
            protected function btnGet_clickHandler(event:MouseEvent):void
            {
                //call hello at service
                uri = new URI(baseUri + "HelloWorld/John/Doe");
                request = new Get();
                request.header.add("Authorization", "Basic " + Base64.encode("John:Doe"));
                client.request(uri, request);                
            }            
        ]]>
    </fx:Script>
    <s:Button id="btnGet" x="34" y="28" label="Get" click="btnGet_clickHandler(event)"/>
    <s:TextArea id="txtResult" x="33" y="68" width="294" height="257"/>
    <s:Button id="btnPostSimple" x="122" y="28" label="Post Simple"
              click="btnPostSimple_clickHandler(event)"/>
    <s:Button id="btnPostComplex" x="229" y="28" label="Post Complex"
              click="btnPostComplex_clickHandler(event)"/>
    
</s:Application>

请注意,我们的项目需要引用 as3httpclientlibas3cryptolibas3corelib .swc 文件。

就是这样。:)

关注点

我最初考虑在 WCF 之上实现自定义身份验证方法,而不是使用 SSL,这样可以避免 SSL 握手并节省一些带宽(以及解决 Flash 中遇到的问题)。我几乎实现了整个方案,包括 HMAC、RSA、AES,它是一个相当健壮的解决方案。

问题是,我只能从服务器到客户端加密内容,而不能从客户端到服务器。这是因为在 WCF 中(类似于此处)在 MessageEncoder 内部实现加密,由于消息尚未创建,无法在 ReadMessage 端访问 HTTP 头。我意识到唯一正确的方法是实现一个完整的传输通道,但这会变得非常荒谬。我本可以实现一个握手并将 AES 会话密钥放在一个 static 类中(这本身就很混乱),但既然我使用了握手,那我还不如使用 SSL,让事情变得简单得多。我就是这么做的。

结论

我希望本文能帮助您开始在 WCF 中使用 REST。我不知道其他人怎么想,但我喜欢专注于业务逻辑的编码,而不是纠结于技术实现,因为大量时间可能会浪费而没有取得任何有价值的成果,即,既然我已经让所有这些都启动并运行了,我仍然需要开发我最初想要构建的 Web 服务的功能。;)希望这能避免其他人浪费时间。

历史

  • 发布 1.0。 - 文章和代码
© . All rights reserved.