今天要介绍的是 Filter
,这是我非常喜欢的功能,Filter 有点像管道 Middleware 的延伸,会在管道结束后执行,Filter 的作用是,可在 Action 执行前
和执行后
对 Request 进行加工处理,我们可以把一些通用的程式逻辑抽离,例如验证、例外处理和修改回传内容等等,在透过 Attribute 挂载到 Action 上,如此做可使 Action 更专注于本身的工作 关注点分离
,独立的 Filter 模组更易于抽换和扩充,提高程式的内聚力
和降低耦合度
,让程式更好维护。
而在 Webform 时代我们如果想达到类似的功能,可以使用传统的 ASP.NET 管道模型,也就是 Middleware 的前身,在 ASP.NET 的管道中,Request 会经过多个 Module 最后才会抵达 Handler,Handler 就是我们比较常写的 ASPX 和 ASHX,Handler 结束后会再通过原来的 Module 原路返回,因此我们可以利用 Module 来对 Request 进行加工处理,不过没有办法像 Filter 一样灵活,这篇因为重点在 Filter 所以就不对 Module 多做介绍。
接下来要进入本篇的重点,在开发 API 时我们希望所有的 API 能有一致的输出格式,这样使用者就不需要为每个 API 去做不同的接收方式,让 API 使用起来更方便,Filter 可以分为下面三类,后面我会利用这三种不同的 Filter 来统一 API 的输出格式。
AuthorizationFilter
: 在所有 Filter 之前执行,用于验证 Request 是否合法。ActionFilter
: 里面有两个方法分别对应 Action 执行前和执行后。ExceptionFilter
: 会在发生异常时执行。Filter 流程图:
上图蓝色箭头为 Request 经过 Filter 的正常流程,首先会经过 AuthorizationFilter,验证使用者资讯是否合法,接着通过 ActionFilter,最后到达 Action,结束后在经过 ActionFilter 返回,AuthorizationFilter 只有进来时才会执行,而橘色箭头是,如果在 ActionFilter 或 Action 内程式出现异常 Exception,那么就会被 ExceptionFilter 拦截做异常的处理,这里一定有人会问如果在 AuthorizationFilter 内出现异常呢,没错这里是需要注意的地方,我们不应该在 AuthorizationFilter 抛出异常,因为它不会被 ExceptionFilter 拦截处理。
统一的输出格式
新增 ResultViewModel 类别,作为我们所有 API 的统一介面,
类别内有下面三个属性:
success
: 代表请求是否执行成功。msg
: 存放异常的错误讯息。data
: 存放请求成功后回传的资料。程式码:
namespace ViewModel{ public class ResultViewModel { public bool success { get; set; } public string msg { get; set; } public object data { get; set; } }}
新增 ResultAttribute 类别,继承 ActionFilter 覆写 OnActionExecuted 方法,该 Filter 的作用是包装 Action 回传的资料,将资料放入 ResultViewModel 的 data 属性内,再回传出去,这个 Filter 可以搭配 IgnoreResult Attribute 使用,如果我们希望有些 Controller 或 Action 的回传资料不要经过包装处理,例如档案下载,那么可以挂上 IgnoreResult 就会忽略这些 Action。
程式码:
namespace Filters{ public class ResultAttribute : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) { if (actionExecutedContext.Exception != null) { return; } var ignoreResult1 = actionExecutedContext.ActionContext.ActionDescriptor.GetCustomAttributes<IgnoreResultAttribute>().FirstOrDefault(); var ignoreResult2 = actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<IgnoreResultAttribute>().FirstOrDefault(); if (ignoreResult1 != null || ignoreResult2 != null) { return; } var objectContent = actionExecutedContext.Response.Content as ObjectContent; var data = objectContent?.Value; var result = new ResultViewModel { success = true, data = data }; actionExecutedContext.Response = actionExecutedContext.Request.CreateResponse(result); } }}
程式码:
namespace Filters{ public class IgnoreResultAttribute : Attribute { }}
例外处理
新增 CustomException 类别,有时候如果我们不希望回传的错误讯息,包含了敏感的资讯,例如执行SQL语法出错时,会回传部分的SQL语句,可能包含了栏位资讯或资料等等,因此就可以利用 CustomException 来识别这个 Exception 是不是已经被我们处理过,可否传到外部去。
程式码:
namespace Exceptions{ public class CustomException : Exception { public CustomException(string message) : base(message) { } }}
新增 ExceptionAttribute 类别,继承 ExceptionFilter,该 Filter 会拦截异常,将错误讯息填入 ResultViewModel 的 msg 属性内,然后将包装后的异常回传,不过这里我并没有判断 Exception 是否是 CustomException,因为我还是习惯将所有异常讯息都回传,这个可以视专案需求调整。
程式码:
namespace Filters{ public class ExceptionAttribute : ExceptionFilterAttribute { public override void OnException(HttpActionExecutedContext actionExecutedContext) { var result = new ResultViewModel { success = false, msg = actionExecutedContext.Exception.Message }; actionExecutedContext.Response = actionExecutedContext.Request.CreateResponse(result); } }}
权限控制
新增 CustomAuthorizeAttribute 类别,继承 AuthorizationFilter,功能很单纯,仅用来判断使用者是否有登入,这里有使用到 上一篇 实作的 UserManager,而更细的身分判断我通常会写在 Action 内,Filter 只做第一层最简单的防护,CustomAuthorize 还可以搭配原 Web API 内建的 AllowAnonymous Attribute 使用,挂上这个 Attribute 的 Controller 或 Action 将不会执行验证权限的动作,代表这是个公开的 API,任何人都可以存取,Filter 内我有用 try catch 包住所有程式,就如同上面提到的,我们不应该在 AuthorizationFilter 内抛出异常,因为它不会被 ExceptionFilter 捕捉。
程式码:
namespace Filters{ public class CustomAuthorizeAttribute : AuthorizationFilterAttribute { protected readonly UserManager _userManager; public CustomAuthorizeAttribute() { _userManager = new UserManager(); } public override void OnAuthorization(HttpActionContext actionContext) { try { if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0) { return; } if (actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0) { return; } var user = _userManager.GetCurrentUser(); if (user == null) { throw new CustomException("没有权限。"); } } catch (Exception ex) { if (!(ex is CustomException)) { ex = new CustomException("权限验证异常。"); } var result = new ResultViewModel { success = false, msg = ex.Message }; actionContext.Response = actionContext.Request.CreateResponse(result); } } }}
如何使用
我会新增一个 BaseController 并在 Controller 挂上 CustomAuthorize,然后让其它 Controller 继承,这样就可以让所有的 API 受到最基本的权限验证保护,而公开的 API 再加上 AllowAnonymous Attribute 关闭验证,这样做有个好处,可以防止未来新增 Action,或由别人来维护程式时忘记加上权限验证,导致资料外洩,接着新增一个 TestFilterController 并挂上 Exception 和 Result 来测试一下各个 Filter 的功能。
程式码:
namespace Api{ [CustomAuthorize] public class BaseController : ApiController { public BaseController() { } }}
namespace Api{ [Result] [Exception] [RoutePrefix("api/testFilter")] public class TestFilterController : BaseController { //正常 [HttpGet] [Route("getStudents_1")] public List<Student> GetStudents_1() { return CreateStudents(); } //忽略权限验证 [AllowAnonymous] [HttpGet] [Route("getStudents_2")] public List<Student> GetStudents_2() { return CreateStudents(); } //忽略 ResultFilter [IgnoreResult] [HttpGet] [Route("getStudents_3")] public List<Student> GetStudents_3() { return CreateStudents(); } //抛出异常 [HttpGet] [Route("getStudents_4")] public List<Student> GetStudents_4() { throw new CustomException("取得资料失败"); } private List<Student> CreateStudents() { return new List<Student> { new Student { Id = 100, Name = "小明" }, new Student { Id = 101, Name = "小华" }, }; } } public class Student { public int Id { get; set; } public string Name { get; set; } }}
资料夹结构
测试结果
在未登入
状态下对四个 API 的测试结果:
正常: api/testFilter/getStudents_1
忽略权限验证: api/testFilter/getStudents_2
忽略 ResultFilter: api/testFilter/getStudents_3
抛出异常: api/testFilter/getStudents_4
在登入
状态下对四个 API 的测试结果:
正常: api/testFilter/getStudents_1
忽略权限验证: api/testFilter/getStudents_2
忽略 ResultFilter: api/testFilter/getStudents_3
抛出异常: api/testFilter/getStudents_4
结语
这篇我们使用 Filter 来统一 API 的回传格式和例外处理,除了方便使用者使用外,程式逻辑的抽离让 Action 能更专注于自己的工作,可自由组合的 Filter,让 API 面对各种不同需求时有更大的弹性,且将结果的处理拉到 Action 之外,才不会破坏原来的写法,依旧可由 Action 的回传型态看出 API 回传的资料结构,不会因为要统一格式,就造成 Action 都回传 ResultViewModel,程式的可读性变差。
今天就介绍到这里,感谢大家观看。
参考资料
使用Asp.Net MVC打造Web Api (16) - 统一输入/出格式以及异常处理策略
使用Asp.Net MVC打造Web Api (20) - 整合AOP功能
[铁人赛 Day14] ASP.NET Core 2 系列 - Filters