WCF4.0 –- RESTful WCF Services (4)
(Basic Security)
在REST架构的WCF服务中,它不像一般的WCF服务绑定,有配套的安全模式,实现起来那么简单。REST
WCF服务只能在传输层加密,而一般的WCF 服务可以在消息层加密。因此 REST WCF服务启用ASP.NET兼容模式后,它的安全是由ASP.NET来保证的。本篇文章主要介绍在
REST WCF 中如何实现最简单的 Username 验证。
在SOAP协议的WCF中,可以通过SOAPHeader(MessageHeader)来实现用户名密码的传输,早在WebService时代我们就这么用过了。在REST
WCF中,我们可以利用 HttpHeader 来完成这一目标。 (你可不会想在每个服务契约里加上用户和密码的参数吧...)
首先在服务中加入如下方法用于校验,Header的信息:如果 Header 中 Authorization
的字符串不是"fangxing/123" 那么就将返回 405 MethodNotAllowed
的错误。这个字符串的内容可以自定义,反正服务端根据某种规则检查这个字符串。
private bool CheckAuthorization() { var ctx = WebOperationContext.Current; var auth = ctx.IncomingRequest.Headers[HttpRequestHeader.Authorization]; if (string.IsNullOrEmpty(auth) || auth != "fangxing/123") { ctx.OutgoingResponse.StatusCode = HttpStatusCode.MethodNotAllowed; return false; } return true; } |
然后在每一个服务契约的实现中,都去调用它。
[WebGet(UriTemplate = "All")] public List<Task> GetTask() { if (!CheckAuthorization()) return null; return GetData(); } [WebGet(UriTemplate = "{taskId}")] public Task GetTaskById(string taskId) { if (!CheckAuthorization()) return null; return GetData().FirstOrDefault(t => t.Id==taskId); }
|
现在的服务,如果直接通过浏览器访问,将得到 405 MethodNotAllowed
的错误:
客户端只要相应的验证信加到 RequestHeader 中去,就可以访问了。客户端可以使用单例模式设计
Client 对象。
这样就不用每次调用都去加验证信息了。
var url = "http://localhost:3433/TaskService/All"; var client = new HttpClient(); client.DefaultHeaders.Add("Authorization", "fangxing/123"); var resp = client.Get(url); |
这里使用的是 Microsoft.Http.HttpClient (WCF REST Starter
Kit) 而非 System.Net.WebClient
回头看服务端代码,每个服务实现中都需要加上 CheckAuthorization() 是不是很烦?
OK,我们知道这个 REST WCF服务是承载在一个Web Application上的, 通过往 RouteTable
中注册 WebServiceHostFactory 来激活服务对象的。 那么只要对这个 WebServiceHostFactory
做些“手脚”,就可以实现服务端验证的统一拦截,代码如下。(一般的 WCF 也可以利用此方法对 MessageHeader
进行拦截校验)
public class SecureWebServiceHostFactory : WebServiceHostFactory { protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses) { var host = base.CreateServiceHost(serviceType, baseAddresses); host.Authorization.ServiceAuthorizationManager = new MyServiceAuthorizationManager(); return host; } public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses) { var host = base.CreateServiceHost(constructorString, baseAddresses); host.Authorization.ServiceAuthorizationManager = new MyServiceAuthorizationManager(); return host; } } public class MyServiceAuthorizationManager : ServiceAuthorizationManager { protected override bool CheckAccessCore(OperationContext operationContext) { var ctx = WebOperationContext.Current; var auth = ctx.IncomingRequest.Headers[HttpRequestHeader.Authorization]; if (string.IsNullOrEmpty(auth) || auth != "fangxing/123") { ctx.OutgoingResponse.StatusCode = HttpStatusCode.MethodNotAllowed; return false; } return true; } } |
RegisterRoutes 里的工厂类也需要相应的修改下:
var securewebServiceHostFactory = new SecureWebServiceHostFactory(); RouteTable.Routes.Add(new ServiceRoute("TaskService", securewebServiceHostFactory, typeof(TaskService))); |
这样服务端代码就可以去掉 CheckAuthorization() 而把验证工作都交给 SecureWebServiceHostFactory
了。
这种验证方式,其实也是现在 Windows Auzer Access Control 的原型。 只不过这个
Authoriztion 的服务是专门的Services罢了。
1. 客户端先从发布令牌的服务获取令牌;
2. 客户端拿着令牌提交到现在的服务;
3.服务端将客户端令牌拿到发布令牌的服务上校验。
WCF4.0 –- RESTful WCF Services (实例)
(并发同步服务 SyncService)
最近写自动化测试时遇到一个问题: 我们在进行一个并发测试的过程中,需要所有客户端测试代码,在某个时机同步。回想所学到的,线程同步手段很多了,同一台PC上的进程间同步也可以通过Metux实现,多PC的时候怎么办。因为最近在学习REST
WCF,自然想到它,用它来做个同步服务,即可以解决多线程,多进程,多PC同步,还可以支持跨语言,真是一举多得。(类似的解决方案还有PNUNIT,它是通过.Net
Remoting实现的,因为它还要写配置,还要起Lancher/Agent,有点烦)。
1. SyncService 的主要功能——Barrier(栏栅):
借用PNUNIT的概念Barrier,也就是异步过程中的同步点,进到Barrier里的所有对象都要等待其他对象进入。这些对象可以是不同的线程,进程(不同PC,不同语言实现的客户端),过程如下图:3个客户端启动之后,有快有慢,但是在Barrier处进行一次同步,先到的等待后到的。
举个实际例子: 假如我们要实现两个客户端通信的功能的测试,必须是两个客户端同时上线。那么我们可以在代码中设计一个barrier,让双方都确认上线之后,再进行通信测试。
(1) 准备Barrier
这里有点要特别说明的地方var init = SyncService.Init("Barrier_Logon", "Client1", "Client2"); // 启动Client1 Process.Start("Client1.exe"); // 启动Client2 Process.Start("Client2.exe"); |
(2) Client1
// client1登录 var client1 = Login("Client1"); // 同步,等待Client2登录 var enter = SyncService.Enter("Barrier_Logon", "Client1"); // client1 和 client2 相互通信 ... |
(3) Client2 和 Client1 类似
// client2登录 var client2 = Login("Client2"); // 同步,等待Client1登录 var enter = SyncService.Enter("Barrier_Logon", "Client2"); // client1 和 client2 相互通信 ... |
2. SyncService 的消息交换功能——SetMessage/GetMessage:
我们还可以通过SyncService中的消息容器进行消息传递。如下图:
在异步的两段代码中,设置同步点,保证 GetMessage 是在 SetMessage 之后发生。这一点是并行测试中是很常见的处理。
Client1的代码:
// 设置消息给client2 var set = SyncService.SetMessage("Barrier", "key", "hello client2"); // 进入Barrier, 等待client2 var enter = SyncService.Enter("Barrier", "Client1"); |
Client2的代码:
// 进入Barrier,等待client1 var enter = SyncService.Enter("Barrier", "Client2"); // 取得消息 var get = SyncService.GetMessage("Barrier", "key"); // 确认获得消息,是"hello client2" Assert.AreEqual(get, "hello client2"); |
3. SyncService的实现
如果上面的并行处理代码理解了的话,SyncService的实现就很好推断出来了。服务端维护一个Dictionary<string,
SyncGroup>的容器,每个客户端Enter时,调用对应的ManualResetEvent.Set()解锁。然后WaitAll其他的ManualResetEvent,从而实现同步。
using System; using System.Collections.Generic; using System.Linq; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Web; using System.Text; using System.Threading; using System.Runtime.Serialization; namespace SyncService { [ServiceContract] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public class SyncService { private static Dictionary<string, SyncGroup> _syncPool = new Dictionary<string, SyncGroup>(); [WebGet(UriTemplate="Init/{barrier}/{targetnames}")] public string Init(string barrier, string targetnames) { var ctx = WebOperationContext.Current; try { lock (_syncPool) { _syncPool[barrier] = new SyncGroup(); var syncGroup = _syncPool[barrier]; var targets = targetnames.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); Array.ForEach(targets, t => syncGroup.ResetEventDict.Add(t, new ManualResetEvent(false))); } return "ok"; } catch (Exception ex) { return ex.Message; } } [WebGet(UriTemplate = "Enter/{barrier}/{targetname}/{timeout=60000}")] public string Enter(string barrier, string targetname, string timeout) { var ctx = WebOperationContext.Current; try { var syncObj = _syncPool[barrier]; var target = syncObj.ResetEventDict[targetname]; target.Set(); var intTimeout = int.Parse(timeout); var success = WaitHandle.WaitAll(syncObj.ResetEventDict.Values.ToArray(), intTimeout); if (success) return "ok"; else return "timeout"; } catch (Exception ex) { return ex.Message; } } [WebGet(UriTemplate = "SetMessage/{barrier}/{key}/{message=null}")] public string SetMessage(string barrier, string key, string message) { var ctx = WebOperationContext.Current; try { var syncObj = _syncPool[barrier]; lock (syncObj) { var query = syncObj.Messages.FirstOrDefault(m => m.Key == key); syncObj.Messages.Remove(query); var messageInfo = new MessageInfo { BarrierName = barrier, Key = key, Message = message, UpdateDateTime = DateTime.Now }; syncObj.Messages.Add(messageInfo); } return "ok"; } catch (Exception ex) { return ex.Message; } } [WebGet(UriTemplate = "GetMessage/{barrier}/{key}")] public string GetMessage(string barrier, string key) { var ctx = WebOperationContext.Current; try { var syncObj = _syncPool[barrier]; var query = syncObj.Messages.FirstOrDefault(m => m.Key == key); return query.Message; } catch (Exception ex) { return ex.Message; } } [WebGet(UriTemplate = "ListMessages/{barrier=all}", ResponseFormat=WebMessageFormat.Xml)] public List<MessageInfo> ListMessages(string barrier) { var ctx = WebOperationContext.Current; try { var messages = new List<MessageInfo>(); if (barrier == "all") _syncPool.Values.ToList().ForEach(t => messages.AddRange(t.Messages)); else messages = _syncPool[barrier].Messages; return messages; } catch { return null; } } [WebGet(UriTemplate="Check", ResponseFormat=WebMessageFormat.Xml)] public string Check() { return "Welcome to the SyncService! " + DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToLongTimeString(); } } [DataContract] [KnownType(typeof(MessageInfo))] public class SyncGroup { internal Dictionary<string, ManualResetEvent> ResetEventDict { get; set; } [DataMember] public string Name { get; set; } [DataMember] public List<MessageInfo> Messages { get; set; } [DataMember] public Dictionary<string, string> States { get; set; } public SyncGroup() { Messages = new List<MessageInfo>(); ResetEventDict = new Dictionary<string, ManualResetEvent>(); } } [DataContract] public class MessageInfo { [DataMember] public string BarrierName { get; set; } [DataMember] public string Key { get; set; } [DataMember] public string Message { get; set; } [DataMember] public string Identity { get; set; } [DataMember] public DateTime UpdateDateTime { get; set; } } } |
默认使用JSON格式,另外为了查看当前的同步的状况和消息,可以通过 ListStates/ListMessages
查看。
(1) 初始化Barrier则发送: http://server/SyncService/Init/MyBarrier/Client1,Client2
(2) 客户端进入Barrier则发送: http://server/SyncService/Enter/MyBarrier/Client1/10000
(最后是timeout设定)
(3) 设置消息则发送: http://server/SyncService/SetMessage/MyBarrier/Key/MessageContent
(4) 取得消息则发送: http://server/SyncService/GetMessage/MyBarrier/Key
(5) 查看所有的“锁”则发送:http://server/SyncService/ListStates
(或者指定某个Barrier: /MyBarrier)
(6) 查看所有的消息则发送:http://server/SyncService/ListMessages(或者指定某个Barrier:
/MyBarrier)
(7) 清空所有SyncGroup则发送:http://server/SyncService/Restart
是的,全部的操作全部是 HttpRequest 的"GET",
因此各种客户端都可以轻松调用,很方便。 (用WCF创建这样一个服务也非常简单全部代码一百多行,正所谓天下武功无快不破:)
上篇:WCF4.0
–- RESTful WCF Services(一)
|