如何写OWIN中间件

一、简介

在ASP.NET5里Web Server与应用之间的沟通采用OWIN(Open Web Interface for .NET);其实如果写过NodeJS就明白,这种中间件的形式在NodeJS各种WEB框架到处使用。然而从系统架构的角度来说,将ASP.NET将不再依赖于IIS,也可以用Self-Hosting模式,我们甚至可以采用一个Linux服务器来部署ASP.NET程序。

从上至下,OWIN架构共有四层:

  • Application
    Web程序。
  • Application Framework(Middleware)
    请求处理模块组,这些模块也叫中间件,请求时会根据中间件加载顺利逐一处理,只要不中断处理请求,OWIN会一直下去,直到所有的中间件都执行完毕。而这些中间件非常多,小到记录日志,大到Web API、MVC、SignalR等等。
  • Server
    构建OWIN环境和Request。
  • Host
    诸如:IIS、Katana.exe或者自己开发Web服务器。

二、OWIN有什么好处?

如果说System.Web.dll是一个什么都能干的万能框架,那么OWIN就是就像一把利刀。假如我需要提供表态文件(图片、CSS、JavaScript),导入静态文件的处理模块;假如我需要提供微信OAuth,导入一个处理模块。所有任何你想要的功能都可以像吃自助餐一样,你需要什么?加入什么!

我们写一个极为简单的应用,请求时返回一个文本格式。

using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(OWINDemo.Startup))]
namespace OWINDemo
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.Run(context =>
            {
                context.Response.ContentType = "text/plain";
                return context.Response.WriteAsync("Hello, world.");
            });
        }
    }
}

这个应用在任何请求的情况下都直接返回一个 Hello, world.,这里并没有什么Web API、登录、Cookie。然而我们并不可能只是单纯这样,我们需要路由、安全认证机制等等,只需要导入所需要的模块,所有的这一些模块就是所谓的中间件管道。

三、加载OWIN模块

1、Web API中间件

继续上面的示例,我们引入Web API中间件,让Web应用变成一个Web API应用。添加Microsoft.AspNet.WebApi.Owin,然后把Web API中间件配置信息加入到 Startup.cs 里:

var routes = new HttpRouteCollection();
routes.MapHttpRoute("Default", "{controller}/{id}", new { id = RouteParameter.Optional });
app.UseWebApi(new HttpConfiguration(routes));

接着,为了让Web API有个有效请求路径,还需要加下一个Contoller,代码如下:

public class ValuesController : ApiController
{
    public int Get(int v)
    {
        return v;
    }
}

当我们访问 /values/get?v=100,会得到一个 100 的数字;而如果访问默认首页依然返回 Hello, world.

2、Cors中间件

继续上面的示例,API接口跨域是一个问题,因此我们还需要让API接口支持CORS(跨域资源共享),好在M$也提供了相应的模块,添加Microsoft.Owin.Cors,然后配置信息加入到 Startup.cs 里:

app.UseCors(new CorsOptions());

好了,我们的API接口也已经支持CORS。

怎么样?是不是太酷了,Web应用变得非常的轻,需要什么加入什么。如果你需要些什么模块,建议你先到Nuget搜索:owin,也许就已经有人写好了。

四、开发Owin

那么如何开发像上面一样OWIN呢?其实上面的示例当中 Hello, world. 就是一个完全的OWIN中间件,只不过它实在太简单,以至于让我们好像忽略OWIN的存在感。

1、Lambda表达式

app.Use(async (context, next) =>
{
    context.Response.Write("Lambda 表达式");
    await next();
});

他非常适合简单业务或用于诊断的中间件。

2、Middleware 类型

创建一个类,并且必须包括构建函数第一个参数必须是处理通道的下一个待处理函数和一个 Invoke 函数接收的是OWIN环境变量。

using AppFunc = Func<IDictionary<string, object>, Task>;

public class MyMiddlewareClass
{
    private readonly AppFunc next;

    public MyMiddlewareClass(AppFunc next)
    {
        this.next = next;
    }

    public async Task Invoke(IDictionary<string, object> environment)
    {
        await next.Invoke(environment);
    }
}

然后在 Startup.cs 加入配置:

app.Use(typeof(MyMiddlewareClass));

OWIN会反射调用Invoke;如果你对反射比较反感,那么介绍一种实例的方式。

3、Middleware 实例

相比较第二种,只是在这基础上将构建函数更改为一个 Initialize 方法。

using AppFunc = Func<IDictionary<string, object>, Task>;

public class InstanceMiddleware
{
    private AppFunc next;

    public void Initialize(AppFunc next)
    {
        this.next = next;
    }

    public async Task Invoke(IDictionary<string, object> environment)
    {
        await next.Invoke(environment);
    }
}

在加载上也不一样,只需要实例一个 InstanceMiddleware 交给OWIN通道就行。

app.Use(new InstanceMiddleware());

4、OwinMiddleware

第二、三种你会发现,你无法给把信息输出到页面。那么我们可以考虑采用 OwinMiddleware;他是 Microsoft.Owin 的一个抽象类,并且提供一个 IOwinContext 用来访问OWIN环境。

public class LoggingMiddleware : OwinMiddleware
{
    public LoggingMiddleware(OwinMiddleware next)
        : base(next)
    {
    }

    public async override Task Invoke(IOwinContext context)
    {
        context.Response.Write("LoggingMiddleware");
        await Next.Invoke(context);
    }
}

配置模块:

app.Use(new LoggingMiddleware());

以上四种编写方式各有适用环境,从代码的角度看,将现有模块转化成OWIN模块成本非常低,而这些模块我们还可以在另一项目得到重用。

ASP.NET中的Json Web Token

一、简介

随着像AngularJs、Backbone、ReastJS等这种MVVC越来越流行,后端开发越来越脱离语言本身的渲染依赖,更甚者把部分业务直接转到前端来做;而后端变得更加的专注于数据处理,让前后端完全分离,这样后端可测试性变得更高、更简单。

针对这些MVVC现在最流行就是后端向前端提供Restful API接口,API应该是无状态,这意味者没有登陆、退出、当前用户会话,更不可能依赖Cookie;因为我们无法保证所有请求都来源于浏览器,有可能是移动端或者其他设备。

那么目前有两种方式来实现Token:

1、Basic Auth

如果按HTTP规范是指每一次请求时需要在Header带上凭证,包括用户名和密码。但一般不会这么做,我们可能会在登陆页面里根据提供的用户名和密码来获取一个Token,然后每一次请求带上Token做为钥匙来访问受限的API。

2、OAuth

相比较Basic Auth更安全,更标准。不过后端的开发难度也相对于高一点,在.NET里面目前有DotNetOpenAuth提供完整的开发资源。

其实不管是Basic Auth或OAuth他们都是为了更安全的获取Token。而JWT(Json Web Token)如果非要分应该算是Basic Auth的一种Token格式之一,其实OAuth 2.0的规范并没有明确规范Token的实现方式,那么如果你把JWT理解为OAuth的一种也不算是错的。

二、什么是JWT

JWT是一种用于认证的Token格式,这个Token可以帮助我们实现在两个系统之间以一种安全的方式传递信息。包含三部分:header、payload、signature。

1、header

用Base-64编码存放Token类型和编码方式。

{
    typ: 'JWT',
    alg: 'HS256' // 采用HMAC SHA-256算法
}
// result:e3R5cDogJ0pXVCcsYWxnOiAnSFMyNTYnfQ==

2、payload

用Base-64编码存放任意信息,比如:用户信息、权限信息等。

{
    name: 'asdf', // 用户名
    role: 'manage', // 权限
    exp: 1443925718155 // 过期时间
}
// result:e25hbWU6ICdhc2RmJyxyb2xlOiAnbWFuYWdlJyxleHA6IDE0NDM5MjU3MTgxNTV9

3、signature

包括header、payload和密钥的混合体,密钥当然是最重要的,要是泄露了那么你的Token也就被破解了。假定密钥为:1234567890,按 HmacSHA256(header+payload, 密钥) 所得到的Token为:b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad。

最后将header、payload、signature合并起来就是Token了。

上面JSON体中的Key键JWT是有一个明确的结构体规范的,可以参考:JWT Claims

三、一个完整的Web API示例

以下我只能尽可能的简化去演示这个过程,如果你对JWT感兴趣可以查看完整的源代码

1、JWT编码和解码代码

代码并无特别之处,只不过将以上JWT三个步骤的具体实现。

/// <summary>
/// 只支持HS256算法
/// </summary>
public class JWT
{
    /// <summary>
    /// Token解码
    /// </summary>
    public T Decode<T>(string token, string key, bool noVerify = false)
    {
        var segments = token.Split('.');
        if (segments.Length != 3) throw new ArgumentException("Token格式不正确");

        var headerSeg = segments[0];
        var payloadSeg = segments[1];
        var signatureSeg = segments[2];

        var header = StringToJson<JWTHeader>(Base64Decrypt(headerSeg));
        var payload = StringToJson<T>(Base64Decrypt(payloadSeg));

        if (!noVerify)
        {
            if (!HmacSignature(headerSeg + "." + payloadSeg, key).Equals(signatureSeg))
                throw new Exception("无效Token");
        }

        return payload;
    }

    /// <summary>
    /// Token编码
    /// </summary>
    public string Encode<T>(T payload, string key, JWTHeader header = null)
    {
        var segments = new List<string>();
        // 1、header
        if (header == null) header = new JWTHeader();
        segments.Add(Base64Encrypt(JsonToString(header)));
        // 2、payload
        segments.Add(Base64Encrypt(JsonToString(payload)));
        // 3、signature
        segments.Add(HmacSignature(String.Join(".", segments), key));

        // 最终以.号拼接做为Token
        return String.Join(".", segments);
    }

    #region helper

    private string Base64Encrypt(string str)
    {
        return Convert.ToBase64String(Encoding.UTF8.GetBytes(str));
    }

    private string Base64Decrypt(string str)
    {
        return Encoding.UTF8.GetString(Convert.FromBase64String(str));
    }

    private string HmacSignature(string secret, string value)
    {
        var secretBytes = Encoding.UTF8.GetBytes(secret);
        var valueBytes = Encoding.UTF8.GetBytes(value);
        string signature;

        using (var hmac = new HMACSHA256(secretBytes))
        {
            var hash = hmac.ComputeHash(valueBytes);
            signature = Convert.ToBase64String(hash);
        }
        return signature;
    }

    private string JsonToString<T>(T obj)
    {
        return Newtonsoft.Json.JsonConvert.SerializeObject(obj);
    }

    private T StringToJson<T>(string value)
    {
        if (String.IsNullOrEmpty(value)) return default(T);
        return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(value);
    }

    #endregion
}

2、获取Token

第一件事自然是通过用户名和密码来获取Token。

[HttpGet, HttpPost, Route("login")]
public TokenResult Login(string username, string password)
{
    // 为了简化不做用户名和密码验证
    // 有效期:7天
    var expire = DateTime.UtcNow.AddDays(7);
    var res = new TokenResult()
    {
        expires = expire
    };
    var payloadObj = new JWTPayload()
    {
        name = username,
        role = "manage",
        exp = expire
    };
    res.token = new JWT().Encode<JWTPayload>(payloadObj, PassportConfig.SecretKey);

    return res;
}

当我们请求 passport/login?username=asdf&password=1 后,会返回一个JSON结果:

{
token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiMSIsInJvbGUiOiJtYW5hZ2UiLCJleHAiOjE0NDQ1NDQxMzU0MTl9.zUbqWY/vBerzM3BjzSFnFxEbWlcW1g92pNsmStt5xXc=",
expires: 1444544135419
}

token 表示于下如果我们需要访问受限API时,只需要带上这个Token即可。

expires 表示过期时间。

3、验证Token

Web API我们可以通过OWIN做一个中间件,每一次请求时需要先通过中间件来验证Token是否有效。但这里我没打算这么写,写一个OWin的成本高于一个属性。所以这里我采用 AuthorizationFilterAttribute 属性。

public override void OnAuthorization(HttpActionContext actionContext)
{
    if (actionContext == null) throw new Exception("actionContext");

    // 过滤带有AllowAnonymous
    if (SkipAuthorization(actionContext)) return;

    // 分别从Query、Body、Header获取Token字符串
    const string key = "access_token";
    string token = "";
    // header
    if (actionContext.Request.Headers.Contains(key))
        token = actionContext.Request.Headers.GetValues(key).First();
    // query
    // body

    if (String.IsNullOrEmpty(token))
        actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "请先登录。");

    try
    {
        var payload = new JWT().Decode<JWTPayload>(token, PassportConfig.SecretKey);
        // 检查token是否已经过期
        if (payload.exp < DateTime.UtcNow)
            actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Token已经过期。");
    }
    catch (Exception ex)
    {
        actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, ex.Message);
    }
}

四、总结

JWT这种JSON Token格式相比较我们自建的安全认证更简单,而安全上面也能够得到保障。首先对于公司产品而言,同一个Token能否在不同业务线上能够快速、有效的认证非常重要;其次本身Token已经附带着用户信息,虽然他几乎接近明文存放在Token中间,但这和安全并不冲突。

AngularJS如何下载Excel文件

在AngularJS中要下载一个Excel文件到底有多难呢?

最简单的方法

这当然是放一个 a 链接元素在页面搞定。

<a href="/path/file.xlsx" target="_blank">下载文件</a>

可如果我们涉及到一些身份验证,而且又是通过 Cookie,浏览器会很怪的一并发送到服务端,是不是一切都很好呢?

如果……

像上面说的如果我需要自定义请求头,例如:OWIN等身份验证的情况下,怎么办呢?

问题

也许我们可以非常简单的通过 ajax 发送一个 get 请求,并填写相应 headers,比如:

$http.get({
    url: '/path/file.xlsx',
    method: 'get',
    headers: {
        Authorization: 'Bearer pTVhzRZgA6yW-fp8c5vcxzBxr6vuIBYQrlo0ASIVxgkfN6'
    }
}).success(function (data) {
    // 怎么保存?
});

有一个办法就是我们可以通过 HTML5a 元素,指定一段 Base64 数据编码,我们可以生成一个 a 链接,然后点击下载。

这种方式在我的实验中,发现对于 Excel 支持不好,对于大一点的文件,下载回来都是无法打开。

Blob

Blob 存储的是二进制,实则就是一个 JavaScript 下的一个 File 对象,目前被大部分流行浏览器所支持。

我这里还找到一个 FileServer.js 是对 Blob 保存的具体实现。

以下是我结合 FileServer.js 写的一个AngularJS指令,好了,废话不多说:

App.directive('downFile', ['$http',function ($http) {
    return {
        restrict: 'A',
        scope: true,
        link: function (scope, element, attr) {
            var ele = $(element);
            ele.on('click', function (e) {
                ele.prop('disabled', true);
                e.preventDefault();
                $http({
                    url: attr.downFile,
                    method: 'get',
                    responseType: 'arraybuffer'
                }).success(function (data, status, headers) {
                    ele.prop('disabled', false);
                    var type;
                    switch (attr.downFileType) {
                        case 'xlsx':
                            type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
                            break;
                    }
                    if (!type) throw '无效类型';
                    saveAs(new Blob([data], { type: type }), decodeURI(headers()["x-filename"]));  // 中文乱码
                }).error(function (data, status) {
                    alert(data);
                    ele.prop('disabled', false);
                });
            });
        }
    };
}]);

相对于 View 的具体实现:

<button down-file="/order/export/{{item.id}}" down-file-type="xlsx" class="btn btn-green btn-sm">导出</button>

以下是 ASP.NET API 的具体实现:

HttpResponseMessage response = new HttpResponseMessage();
response.StatusCode = HttpStatusCode.OK;
response.Content = new ByteArrayContent(pck.GetAsByteArray());
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
response.Content.Headers.Add("x-filename", System.Web.HttpUtility.UrlEncode(item.title, System.Text.Encoding.UTF8) + ".xlsx"); // 中文乱码
return response;

中文文件名乱码问题

文件名为中文时获取到的 x-filename 会是乱码,所以需要进行编码,在示例中已经标识鸟。

写文章时比较仓促,所以示例中只对 Excel 进行转化,可以根据需求加入各种文件格式类型。

以上,希望帮助到各位。

关于ASP.NET API的FromBody参数

越来越喜欢 asp.net web api,很大原因是因为非常干净。而其中 POST 是作为修改交互极为重要的一个请求。当然为了体验,和 asp.net web api交互总是离不开 jQuery 或者其实类似 ajax 请求方法,但往往会有一些小小诡异,比如:

咦,为什么都是null?

所以这篇文章会讲解一些 POST 请求时可能的一些问题。

POST原始参数数据到Asp.net web API

public void Post(string value){}

原则上我们只需要按标准的使用 $.post,比如:

$.post('/api/values', { value: 'asdf' });

可结果是:

POST http://localhost:53924/api/Values 404 (Not Found) 

WHY???很明显路由未找到,可这与我们期待不同呀。

必须标记参数[FromBody]

这是一种概念问题,因为对于 POST 时,按我们想当然自然是会自动将 POST 内容直接给 value。而对于路由而言这是一个完整的请求参数,自然就返回一个 404 错误。

我们需要给参数加上 [FromBody] 属性,比如:

public void Post([FromBody]string value){}
$.post('/api/values', { value: 'asdf'}, function(d) { console.log(d); });
// 输出:null

以上我们可以正常的请求成功。但输出的结果还是 null,这不应该我传递是 asdf,这一点我后面再提。

Asp.net web API 参数绑定规则

默认下,ASP.NET web API按以下两种规则绑定参数:

  1. 简单类型主要包括原始类型(int、bool、double等)含(TimeSpan、Guid待)以及String,默认会尝试从 URI 获取。
  2. 复杂类型会尝试从请求体获取,并通过媒体类型格局化器进行解析。

每一个方法只能有一个[FromBody]

除非你自定义一个模型绑定方法,否则你只能使用一个 [FromBody],而对于 POST大部分情况下是多个参数,所以最好的是采用复杂类型。

为什么只允许一个,这应该从认识 RESTful 架构开始,对于一个资源对应一个特定方法,而把数据分成几个松散参数也违背 ASP.NET API 吧。

public class Album {
    public string name {get;set;}
}
public void Post(int id, Album album){}
$.post('/api/values?id=1', { name: 'asdf'}, function(d) { console.log(d); });

以上,这是将简单类型和复杂类型同时使用。

[FromBody]参数必须进行键值编码

回过头看 null 问题,ASP.NET web API需要我们为参数指定 [FromBody],这是一种特定格式。正因为这种特定格式,会依赖 Content-Type类型来解析请求体应该采用什么格式,而绝大部分客户端和服务端采用的是键值对和JSON两种。

  1. 键值对:jQuery默认就是采用这种方式,而对于ASP.NET web API默认是空值做为取值条件,所以我们应该 $.post('/api/values', { '': 'asdf' }); 这样 value 参数才能够正常绑定。

  2. JSON:当Content-Type:application/json时,这也是大多数客户端所习惯采用的比如(AngularJS),对于这种情况我们应该 $.ajax( { type:'post', url: '/api/values', contentType: 'application/json', data: '"asdf"' }).done(function(d) { console.log(d); });

415 Unsupported Media Type

当我们传递的请求体内容无法被正常解析时,会得到这个错误。所以在传递请求体内容时要注意。

© 2017 卡片机色彩 沪ICP备13032872号-3

Theme by cipchk

to top