WCF .NET 4.0 控制台托管的带身份验证和角色的 Json Rest 安全 HTTP Web 服务
一个使用最新 .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.aspxendpoint.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 实用程序来完成此操作
- http://msdn.microsoft.com/en-us/library/ms733813.aspx
- http://www.digitallycreated.net/Blog/38/using-makecert-to-create-certificates-for-development
为您的服务保留命名空间
详细步骤请参阅以下 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)进行测试。
由于是自签名证书,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 客户端和代码

这里是您可以用来让 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>
请注意,我们的项目需要引用 as3httpclientlib、as3cryptolib 和 as3corelib .swc 文件。
就是这样。:)
关注点
我最初考虑在 WCF 之上实现自定义身份验证方法,而不是使用 SSL,这样可以避免 SSL 握手并节省一些带宽(以及解决 Flash 中遇到的问题)。我几乎实现了整个方案,包括 HMAC、RSA、AES,它是一个相当健壮的解决方案。
问题是,我只能从服务器到客户端加密内容,而不能从客户端到服务器。这是因为在 WCF 中(类似于此处)在 MessageEncoder
内部实现加密,由于消息尚未创建,无法在 ReadMessage
端访问 HTTP 头。我意识到唯一正确的方法是实现一个完整的传输通道,但这会变得非常荒谬。我本可以实现一个握手并将 AES 会话密钥放在一个 static
类中(这本身就很混乱),但既然我使用了握手,那我还不如使用 SSL,让事情变得简单得多。我就是这么做的。
结论
我希望本文能帮助您开始在 WCF 中使用 REST。我不知道其他人怎么想,但我喜欢专注于业务逻辑的编码,而不是纠结于技术实现,因为大量时间可能会浪费而没有取得任何有价值的成果,即,既然我已经让所有这些都启动并运行了,我仍然需要开发我最初想要构建的 Web 服务的功能。;)希望这能避免其他人浪费时间。
历史
- 发布 1.0。 - 文章和代码