在上篇文章中和你们一块儿学习了创建基本的WebAPI应用,马上就有人想到了一些问题:
1.客户端和WebService之间文件传输
2.客户端或者服务端的安全控制
要解决这些问题,要了解一下WebAPI的基本工做方式。json
(一)WebAPI中工做的Classapi
在MVC中你们都知道,获取Request和Response使用HttpRequest和HttpResponse两个类,在WebAPI中使用两外两个类:
HttpRequestMessage 和HttpResponseMessage,分别用于封装Requset和Response。除了这两个类以外,还有一个常见的抽象 类:HttpMessageHandler,用于过滤和加工HttpRequestMessage和HttpResponseMessage数组
(二)解决第一个问题安全
其 实第一个问题之因此被提出来应该是和客户端有关,若是客户端的请求是咱们手写提交的,好比使用HttpClient封装的请求,则要传递文件以前,咱们一 般会进行一次序列化,转化为二进制数组之类的,在网络上传输。这样的话,在Controller中的Action参数里,咱们只须要接收这个二进制数组类 型的对象就能够了。
可是若是客户端是Web Form呢,好比咱们提交一个Form到指定的Controller的Action中,这个Action要接收什么类型的参数呢?
或者咱们问另一个问题,若是我将Web Form提交到一个WebAPI的Action中 ,我要怎么去取出这个表单中的数据呢?
其 实咱们应该想到:咱们的Action设置的参数之因此可以被赋值,是由于WebAPI的架构中在调用Action时将HTTP请求中的数据解析出来分别赋 值给Action中的参数,若是真是这样的话,咱们只须要在Action中获取到HTTP请求,而后直接获取请求里面的数据,就能解决上面的问题。
这 种想法是正确的,只不过,此时的HTTP请求已经不是最原始的HTTP Request,而是已经被转化成了HttpRequestMessage,在Action中,咱们能够直接调用base.Requet来获得这个 HttpRequestMessage实例,经过这个实例咱们就能够为所欲为的取出HTTP请求中想要的数据。网络
2.1从RequestMessage中获取普通表单数据多线程
这里的普通表单是指不包含File的表单,也就是说表单的enctype值不是multipart/form-data,这时,表单的数据默认状况下是以Json来传递的
以下页面架构
<form name="form" action="~/api/FormSubmit?key=11234" method="post"> <input type="text" name="key" id="txtKey" /> <br /> <input type="text" name="value" id="txtValue" /> <br /> <input type="submit" id="btnSubmit" value="Submit" /> </form>
捕获到的请求为异步
提交到对应的Action为:async
[HttpPost] public async void submitForm() { StringBuilder sb = new StringBuilder(); HttpContent content = Request.Content; JsonObject jsonValue = await content.ReadAsOrDefaultAsync<JsonObject>(); foreach (var x in jsonValue) { sb.Append(x.Key); string va ; if (x.Value.TryReadAs<string>(out va)) { sb.Append(va); } } }
这样最后能够获得 Json的值:{"key":"123","value":"123"} sb处理后的值为:key123value123ide
注:在该action中使用到了关键字async和await,这些在4.5中新提出的关键字主要是用于进行多线程取值的,在MVCAPI的设计中,大部分的方法都被设计成相似于下面的方法
public static Task<T> ReadAsOrDefaultAsync<T>(this HttpContent content);
返 回值是一个Task,这种返回新线程的方法虽然能够提升系统的响应能力,可是多线程取值会给编码带来不便,因此新出的关键字await用于阻塞当前线程并 获取目标线程的返回值,在方法体中使用await关键字后要求将方法声明为async用来表示该方法是异步的,而且返回值必须为void或者将返回者封装 在一个Task中
固然,若是你不喜欢这种写法,上面的action也能够写为:
Task readTask = content.ReadAsOrDefaultAsync<JsonObject>().ContinueWith((task) => { jsonValue = task.Result; }); readTask.Wait();
2.2从RequestMessage中获取multipart表单数据
将view页面改写为
<form name="form" action="~/api/FormSubmit?key=11234" method="post" enctype="multipart/form-data" > <input type="text" name="key" id="txtKey" /> <br /> <input type="text" name="value" id="txtValue" /> <br /> <input type="file" name="file" id="upFile" /> <br /> <input type="submit" id="btnSubmit" value="Submit" /> </form>
此时捕获到得请求是
这里的文件内容被捕获软件解析成字符串,固然若是我上传的是其余的非文本格式的文件,文件会被转化为二进制数组
这时若是咱们不更改action,而直接调用,会发生错误,缘由很明显,这个HTTP的报文内容是没法被转换为JSON的,这时咱们须要将表单的报文解析成另一种格式
IEnumerable<HttpContent> bodyparts = await content.ReadAsMultipartAsync(); foreach (var bodypart in bodyparts) { string name; name = bodypart.Headers.ContentDisposition.Name; sb.Append(name + ":"); if (bodypart.Headers.Contains("filename")) { Stream stream = await bodypart.ReadAsStreamAsync(); StreamReader reader = new StreamReader(stream); sb.Append(reader.ReadToEnd()); sb.Append("----"); } else { string val = await bodypart.ReadAsStringAsync(); sb.Append(val); sb.Append("----"); } }
获得的处理后的sb值为:
{"key":123----"value":123----"file":******{文件的内容}*****----}
整合后的Action为
[HttpPost] public async void submitForm() { StringBuilder sb = new StringBuilder(); HttpContent content = Request.Content; if (content.IsMimeMultipartContent()) { IEnumerable<HttpContent> bodyparts = await content.ReadAsMultipartAsync(); foreach (var bodypart in bodyparts) { string name; name = bodypart.Headers.ContentDisposition.Name; sb.Append(name + ":"); if (bodypart.Headers.Contains("filename")) { Stream stream = await bodypart.ReadAsStreamAsync(); StreamReader reader = new StreamReader(stream); sb.Append(reader.ReadToEnd()); sb.Append("----"); } else { string val = await bodypart.ReadAsStringAsync(); sb.Append(val); sb.Append("----"); } } } else { JsonObject jsonValue = await content.ReadAsOrDefaultAsync<JsonObject>(); foreach (var x in jsonValue) { sb.Append(x.Key); string va; if (x.Value.TryReadAs<string>(out va)) { sb.Append(va); } } } }
(三)WebAPI工做方式
要想解决第二个问题就没这么容易了,咱们须要更深刻的理解WebAPI的工做方式。
其实对于WebAPI来讲,它最初被设计为和WCF同样的:客户端、服务端两套结构,咱们到如今之因此尚未提到客户端,是由于咱们的请求别的方式来封装成HTTP请求或接收HTTP相应的,好比AJAX和Form表单提交。
在这里先给出一个服务端的响应工做流,让你们有个大致上的认识
(不得已在图片中加了水印,由于看到本身辛苦写的东西被人直接拿走,也不给出原文连接,内心真的很差受..但愿不会影响你们的阅读...)
因为图片大小限制,全部的HttpRequestMessage被简写为HttpRequestMsg,HttpResponseMessage被简写了HttpResponseMsg
大 家能够看到,HTTP的请求最早是被传递到HOST中的,若是WebAPI是被寄宿在IIS上的,这个HOST就是IIS上,HOST是没有能力也没有必 要进行请求的处理的,请求经过HOST被转发给了HttPServer此时已经进入WebAPI的处理加工范围,HttpServer是 System.Net.HTTP中的一个类,经过HttpServer,请求被封装成了WebAPI中的请求承载 类:HttpRequestMessage,这个封装后的请求能够通过一系列自定义的Handler来处理,这些handler串联成一个 pipeline,最后请求会被传递给HttpControlDispather,这个类经过对路由表的检索来肯定请求将被转发到的具体的 Controller中的Action。
Client端的处理与服务端相似,直接上图:
其实根据微软的说法,他们自己就被设计成相似可是能够独立运行的结构
ASP.NET Web API has a pipeline for processing HTTP messages on both the client and server. The client and server sides are designed to be symmetrical but independent; you can use each half by itself. Both sides are built on some common objects:
- HttpRequestMessage represents the HTTP request.
- HttpResponseMessage represents the HTTP response.
- HttpMessageHandler objects process the request and response.
直接看图,在客户端,Handlers pipeline最终是被传递到HttpClientHandler上的,由他负责HttpRequestMessage到HTTP请求的转换。
这里只说明一下Request,Response与其相似。
(四)解决第二个问题
由 此咱们早就能够看出,想要解决第二个问题,能够直接在Handler PipeLine中进行,这种AOP风格的过滤器(拦截器)在REST的Webservice的安全验证中应用很广,通常你们比较乐于在HTTP头或者在 HTTP请求的URL中加上身份验证字段进行身份验证,下面举一个在Http头中添加身份验证信息的小例子
4.1客户端
客户端的customhandler用于将身份验证信息添加入报头
class RequestUpHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { request.Headers.Add("key", "11234"); return base.SendAsync(request, cancellationToken); } }
注:
1.customhandler继承自DelegatingHandler类,上面已经说过,WebAPI的客户端和服务端被设计为相互对应的两套结构,因此不管是在客户端仍是服务端,customhandler都是继承自DelegatingHandler类
2.DelegatingHandler的sendAsync方法即是处理请求和接受请求时会被调用的方法,该方法返回值是HttPResponseMessage,接收的值为HttpRequestMessage,符合咱们的通常认知
3.方法的最后,调用base.SendAsync是将Request继续向该pipeline的其余customHandler传递,并获取其返回值。因为该方法不包含Response的处理逻辑,只需直接将上一个CustomHandler的
返回值直接返回
客户端主程序
static void Main(string[] args) { HttpClient client = new HttpClient(new RequestUpHandler() { InnerHandler = new HttpClientHandler() }); HttpResponseMessage response = client.GetAsync("http://localhost:60023/api/FormSubmit").Result; response.Content.ReadAsAsync<string>().ContinueWith((str) => { Console.WriteLine(str.Result); }); Console.Read(); }
客 户端的主程序建立了一个HttpClient,HttpClient能够接受一个参数,该参数就是CustomHandler,此处咱们嵌入了咱们定义的 RequestUpHandler,用于对Request报头进行嵌入身份验证码的处理,CustomHandler经过InnerHandler属性嵌 入其内置的下一个CustomHandler,此处,因为没有下一个CustomerHandler,咱们直接嵌入HttpClientHandler用 于将HttpRequestMessage转化为HTTP 请求、将HTTP响应转化为HttpResponseMessage
4.2服务端
服务端的customHandler用于解析HTTP报头中的身份认证码
protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { int matchHeaderCount = request.Headers.Count((item) => { if ("key".Equals(item.Key)) { foreach (var str in item.Value) { if ("11234".Equals(str)) { return true; } } } return false; }); if (matchHeaderCount>0) { return base.SendAsync(request, cancellationToken); } return Task.Factory.StartNew<HttpResponseMessage>(() => { return new HttpResponseMessage(HttpStatusCode.Forbidden); }); }
注:代码的处理逻辑很简单:若是身份验证码匹配成功,则经过base.SendAsync继续将请求向下传递,不然返回直接中断请求的传递,直接返回一个响应码为403的响应,指示没有权限。
注意因为SendAsync的返回值须要封装在Task之中,因此须要使用Task.Factory.StartNew将返回值包含在Task中
将customHandler注入到HOST中
本例中WebAPI HOST在IIS上,因此咱们只需将咱们定义的CustomHandler在Application_Start中定义便可
protected void Application_Start() { //省略其余逻辑代码 GlobalConfiguration.Configuration.MessageHandlers.Add(new HttpUrlHandler()); }
因为WebAPI Host在IIS上,因此HttpServer和HttpControllerDispatcher不用咱们手工处理
在加上上面的处理后,若是没有身份验证码的请求,会获得以下的响应