• 聊聊Asp.net过滤器Filter那一些事


      最近在整理优化.net代码时,发现几个很不友好的处理现象:登录判断、权限认证、日志记录、异常处理等通用操作,在项目中的action中到处都是。在代码优化上,这一点是很重要着力点。这时.net中的过滤器、拦截器(Filter)就派上用场了。现在根据这几天的实际工作,对其做了一个简单的梳理,分享出来,以供大家参考交流,如有写的不妥之处,多多指出,多多交流。

    概述:

    .net中的Filter中主要包括以下4大类:Authorize(授权),ActionFilter(自定义),HandleError(错误处理)。

    过滤器

    类名

    实现接口

    描述

    授权

    AuthorizeAttribute

    IAuthorizationFilter

    此类型(或过滤器)用于限制进入控制器或控制器的某个行为方法,比如:登录、权限、访问控制等等

    异常

    HandleErrorAttribute

    IExceptionFilter

    用于指定一个行为,这个被指定的行为处理某个行为方法或某个控制器里面抛出的异常,比如:全局异常统一处理。

    自定义

    ActionFilterAttribute

    IActionFilterIResultFilter

    用于进入行为之前或之后的处理或返回结果的之前或之后的处理,比如:用户请求日志详情日志记录

    AuthorizeAttribute:认证授权

    认证授权主要是对所有action的访问第一入口认证,对用户的访问做第一道监管过滤拦截闸口。

    实现方式:需要自定义一个类,继承AuthorizeAttribute并重写OnAuthorization,在OnAuthorization中能够获取到用户请求的所有Request信息,其实我们做的所有认证拦截操作,其所有数据支撑都是来自Request中。

    具体验证流程设计:

    IP白名单:这个主要针对的是API做IP限制,只有指定IP才可访问,非指定IP直接返回

    请求频率控制:这个主要是控制用户的访问频率,主要是针对API做,超出请求频率直接返回。

    登录认证:登录认证一般我们采用的是通过在请求的header中传递token的方式来进行验证,这样即使用与一般的MVC登录认证,也使用与API接口的Auth认证,并且也不依赖于用户前端js设置等。

    授权认证:授权认证就简单了,主要是验证该用户是否具有该权限,如果不具有,直接做下相应的返回处理。

    MVC和API异同:

      命名空间:MVC:System.Web.Http.Filters;API:System.Web.Mvc

      注入方式:在注入方式上,主要包括:全局->控制器Controller->行为Action

      全局注册:针对所有系统的所有Aciton都使用

      Controller:只针对该Controller下的Action起作用

      Action:只针对该Action起作用

    其中全局注册,针对MVC和API还有一些差异:

      MVC在 FilterConfig.cs中注入
        filters.Add(new XYHMVCAuthorizeAttribute());

      API 在 WebApiConfig.cs 中注入

         config.Filters.Add(new XYHAPIAuthorizeAttribute());

    注意事项:在实际使用中,针对认证授权,我们一般都是添加全局认证,但是,有的action又不需要做认证,比如本来的登录Action等等,那么该如何排除呢?其实也很简单,我们只需要在自定定义一个Attribute集成Attribute,或者系统的AllowAnonymousAttribute,在不需要验证的action中只需要注册上对于的Attribute,并在验证前做一个过滤即可,比如:

        // 有 AllowAnonymous 属性的接口直接开绿灯

                if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any())

                {

                    return;

                }

    API AuthFilterAttribute实例代码

    /// <summary>
        /// 授权认证过滤器
        /// </summary>
        public class XYHAPIAuthFilterAttribute : AuthorizationFilterAttribute
        {
            /// <summary>
            /// 认证授权验证
            /// </summary>
            /// <param name="actionContext">请求上下文</param>
            public override void OnAuthorization(HttpActionContext actionContext)
            {
                // 有 AllowAnonymous 属性的接口直接开绿灯
                if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any())
                {
                    return;
                }
    
                // 在请求前做一层拦截,主要验证token的有效性和验签
                HttpRequest httpRequest = HttpContext.Current.Request;
    
                // 获取apikey
                var apikey = httpRequest.QueryString["apikey"];
    
                // 首先做IP白名单校验 
                MBaseResult<string> result = new AuthCheckService().CheckIpWhitelist(FilterAttributeHelp.GetIPAddress(actionContext.Request), apikey);
    
                // 检验时间戳
                string timestamp = httpRequest.QueryString["Timestamp"];
                if (result.Code == MResultCodeEnum.successCode)
                {
                    // 检验时间戳 
                    result = new AuthCheckService().CheckTimestamp(timestamp);
                }
    
                if (result.Code == MResultCodeEnum.successCode)
                {
                    // 做请求频率验证 
                    string acitonName = actionContext.ActionDescriptor.ActionName;
                    string controllerName = actionContext.ActionDescriptor.ControllerDescriptor.ControllerName;
                    result = new AuthCheckService().CheckRequestFrequency(apikey, $"api/{controllerName.ToLower()}/{acitonName.ToLower()}");
                }
    
                if (result.Code == MResultCodeEnum.successCode)
                {
                    // 签名校验
    
                    // 获取全部的请求参数
                    Dictionary<string, string> queryParameters = httpRequest.GetAllQueryParameters();
    
                    result = new AuthCheckService().SignCheck(queryParameters, apikey);
    
                    if (result.Code == MResultCodeEnum.successCode)
                    {
                        // 如果有NoChekokenFilterAttribute 标签 那么直接不做token认证
                        if (actionContext.ActionDescriptor.GetCustomAttributes<XYHAPINoChekokenFilterAttribute>().Any())
                        {
                            return;
                        }
    
                        // 校验token的有效性
                        // 获取一个 token
                        string token = httpRequest.Headers.GetValues("Token") == null ? string.Empty :
                            httpRequest.Headers.GetValues("Token")[0];
    
                        result = new AuthCheckService().CheckToken(token, apikey, httpRequest.FilePath);
                    }
                }
    
                // 输出
                if (result.Code != MResultCodeEnum.successCode)
                {
                    // 一定要实例化一个response,是否最终还是会执行action中的代码
                    actionContext.Response = new HttpResponseMessage(HttpStatusCode.OK);
                    //需要自己指定输出内容和类型
                    HttpContext.Current.Response.ContentType = "text/html;charset=utf-8";
                    HttpContext.Current.Response.Write(JsonConvert.SerializeObject(result));
                    HttpContext.Current.Response.End(); // 此处结束响应,就不会走路由系统
                }
            }
        }

     MVC AuthFilterAttribute实例代码

    /// <summary>
        /// MVC自定义授权
        /// 认证授权有两个重写方法
        /// 具体的认证逻辑实现:AuthorizeCore 这个里面写具体的认证逻辑,认证成功返回true,反之返回false
        /// 认证失败处理逻辑:HandleUnauthorizedRequest 前一步返回 false时,就会执行到该方法中
        /// 但是,我平时在应用过程中,一般都是在AuthorizeCore根据不同的认证结果,直接做认证后的逻辑处理
        /// </summary>
        public class XYHMVCAuthorizeAttribute : AuthorizeAttribute
        {
            /// <summary>
            /// 认证逻辑
            /// </summary>
            /// <param name="filterContext">过滤器上下文</param>
            public override void OnAuthorization(AuthorizationContext filterContext)
            {
    
                // 此处主要写认证授权的相关验证逻辑
                // 该部分的验证一般包括两个部分
                // 登录权限校验
                //   --我们的一般处理方式是,通过header中传递一个token来进行逻辑验证
                //   --当然不同的系统在设计上也不尽相同,有的也会采用session等方式来验证
                //   --所以最终还是根据其项目本身的实际情况来进行对应的逻辑操作
    
                // 具体的页面权限校验
                // --该部分的验证是具体的到页面权限验证
                // --我看有得小伙伴没有做到这一个程度,直接将这一步放在前端js来验证,这样不是很安全,但是可以拦住小白用户
                // --当然有的系统根本就没有做权限控制,那就更不需要这一个逻辑了。
                // --所以最终还是根据其项目本身的实际情况来进行对应的逻辑操作
    
                // 现在用一个粗暴的方式来简单模拟实现过,用系统当前时间段秒厨艺3,取余数
                // 当余数为0:认证授权通过
                //         1:代表为登录,调整至登录页面
                //         2:代表无访问权限,调整至无权限提示页面
    
                // 当然,在这也还可以做一些IP白名单,IP黑名单验证  请求频率验证等等
    
                // 说到这而,还有一点需要注意,如果我们选择的是全局注册该过滤器,那么如果有的页面根本不需要权限认证,比如登录页面,那么我们可以给不需要权限的认证的控制器或者action添加一个特殊的注解 AllowAnonymous ,来排除
    
                // 获取Request的几个关键信息
                HttpRequest httpRequest = HttpContext.Current.Request;
                string acitonName = filterContext.ActionDescriptor.ActionName;
                string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
    
                // 注意:如果认证不通过,需要设置filterContext.Result的值,否则还是会执行action中的逻辑
    
                filterContext.Result = null;
                int thisSecond = System.DateTime.Now.Second;
                switch (thisSecond % 3)
                {
                    case 0:
                        // 认证授权通过
                        break;
                    case 1:
                        // 代表为登录,调整至登录页面
                        // 只有设置了Result才会终结操作
                        filterContext.Result = new RedirectResult("/html/Login.html");
                        break;
                    case 2:
                        // 代表无访问权限,调整至无权限提示页面
                        filterContext.Result = new RedirectResult("/html/NoAuth.html");
                        break;
                }
            }
        }

    ActionFilter自定义过滤器

    自定义过滤器,主要是监控action请求前后,处理结果返回前后的事件。其中API只有请求前后的两个方法。

    重新方法

    方法功能描述

    使用于

    OnActionExecuting

    一个请求在进入到aciton逻辑前执行

    MVCAPI

    OnActionExecuted

    一个请求aciton逻辑执行后执行

    MVCAPI

    OnResultExecuting

    对应的view视图渲染前执行

    MVC

    OnResultExecuted

    对应的view视图渲染后执行

    MVC

     

    在这几个方法中,我们一般主要用来记录交互日志,记录每一个步骤的耗时情况,以便后续系统优化使用。具体的使用,根据自身的业务场景使用。

    其中MVC和API的异同点,和上面说的认证授权的异同类似,不在详细说明。

    下面的一个实例代码:

    API定义过滤器实例DEMO代码

     

    /// <summary>
        /// Action过滤器
        /// </summary>
        public class XYHAPICustomActionFilterAttribute : ActionFilterAttribute
        {
            /// <summary>
            /// Action执行开始
            /// </summary>
            /// <param name="actionContext"></param>
            public override void OnActionExecuting(HttpActionContext actionContext)
            {
    
            }
    
            /// <summary>
            /// action执行以后
            /// </summary>
            /// <param name="actionContext"></param>
            public override void OnActionExecuted(HttpActionExecutedContext actionContext)
            {
                try
                {
                    // 构建一个日志数据模型
                    MApiRequestLogs apiRequestLogsM = new MApiRequestLogs();
    
                    // API名称
                    apiRequestLogsM.API = actionContext.Request.RequestUri.AbsolutePath;
    
                    // apiKey
                    apiRequestLogsM.API_KEY = HttpContext.Current.Request.QueryString["ApiKey"];
    
                    // IP地址
                    apiRequestLogsM.IP = FilterAttributeHelp.GetIPAddress(actionContext.Request);
    
                    // 获取token
                    string token = HttpContext.Current.Request.Headers.GetValues("Token") == null ? string.Empty :
                                  HttpContext.Current.Request.Headers.GetValues("Token")[0];
                    apiRequestLogsM.TOKEN = token;
    
                    // URL
                    apiRequestLogsM.URL = actionContext.Request.RequestUri.AbsoluteUri;
    
                    // 返回信息
                    var objectContent = actionContext.Response.Content as ObjectContent;
                    var returnValue = objectContent.Value;
                    apiRequestLogsM.RESPONSE_INFOR = returnValue.ToString();
    
                    // 由于数据库中最大只能存储4000字符串,所以对返回值做一个截取
                    if (!string.IsNullOrEmpty(apiRequestLogsM.RESPONSE_INFOR) &&
                        apiRequestLogsM.RESPONSE_INFOR.Length > 4000)
                    {
                        apiRequestLogsM.RESPONSE_INFOR = apiRequestLogsM.RESPONSE_INFOR.Substring(0, 2000);
                    }
    
                    // 请求参数
                    apiRequestLogsM.REQUEST_INFOR = actionContext.Request.RequestUri.Query;
    
                    // 定义一个异步委托 ,异步记录日志
                    //  Func<MApiRequestLogs, string> action = AddApiRequestLogs;//声明一个委托
                    // IAsyncResult ret = action.BeginInvoke(apiRequestLogsM, null, null);
    
                }
                catch (Exception ex)
                {
    
                }
            }
        }

     

    HandleError错误处理

    异常处理对于我们来说很常用,很好的利用异常处理,可以很好的避免全篇的try/catch。异常处理箱单很简单,值需要自定义集成:ExceptionFilterAttribute,并自定义实现:OnException方法即可。

    在OnException我们可以根据自身需要,做一些相应的逻辑处理,比如记录异常日志,便于后续问题分析跟进。

    OnException还有一个很重要的处理,那就是对异常结果的统一包装,返回一个很友好的结果给用户,避免把一些不必要的信息返回给用户。比如:针对MVC,那么跟进不同异常,统一调整至友好的提示页面等等;针对API,那么我们可以一个统一的返回几个封装,便于用户统一处理结果。

    MVC 的异常处理实例代码:

     

       /// <summary>
        /// MVC自定义异常处理机制
        /// 说道异常处理,其实我们脑海中的第一反应,也该是try/cache操作
        /// 但是在实际开发中,很有可能地址错误根本就进入不到try中,又或者没有被try处理到异常
        /// 该类就发挥了作用,能够很好的未经捕获的异常,并做相应的逻辑处理
        /// 自定义异常机制,主要集成HandleErrorAttribute 重写其OnException方法
        /// </summary>
        public class XYHMVCHandleError : HandleErrorAttribute
        {
            /// <summary>
            /// 处理异常
            /// </summary>
            /// <param name="filterContext">异常上下文</param>
            public override void OnException(ExceptionContext filterContext)
            {
                // 我们在平时的项目中,异常处理一般有两个作用
                // 1:记录异常的详细日志,便于事后分析日志
                // 2:对异常的统一友好处理,比如根据异常类型重定向到友好提示页面
    
                // 在这里面既能获取到未经处理的异常信息,也能获取到请求信息
                // 在此可以根据实际项目需要做相应的逻辑处理
                // 下面简单的列举了几个关键信息获取方式
    
                // 控制器名称 注意,这样获取出来的是一个文件的全路径 
                string contropath = filterContext.Controller.ToString();
    
                // 访问目录的相对路径
                string filePath = filterContext.HttpContext.Request.FilePath;
    
                // url完整地址
                string url = (filterContext.HttpContext.Request.Url.AbsoluteUri).ExUrlDeCode();
    
                // 请求方式 post get
                string httpMethod = filterContext.HttpContext.Request.HttpMethod;
    
                // 请求IP地址
                string ip = filterContext.HttpContext.Request.GetIPAddress();
    
                // 获取全部的请求参数
                HttpRequest httpRequest = HttpContext.Current.Request;
                Dictionary<string, string> queryParameters = httpRequest.GetAllQueryParameters();
    
                // 获取异常对象
                Exception ex = filterContext.Exception;
    
                // 异常描述信息
                string exMessage = ex.Message;
    
                // 异常堆栈信息
                string stackTrace = ex.StackTrace;
    
                // 根据实际情况记录日志(文本日志、数据库日志,建议具体步骤采用异步方式来完成)
    
    
                filterContext.ExceptionHandled = true;
    
                // 模拟根据不同的做对应的逻辑处理
                int statusCode = filterContext.HttpContext.Response.StatusCode;
    
                if (statusCode>=400 && statusCode<500)
                {
                    filterContext.Result = new RedirectResult("/html/404.html");
                }
                else 
                {
                    filterContext.Result = new RedirectResult("/html/500.html");
                }
            }
        }

     

    API 的异常处理实例代码:

     /// <summary>
        /// API自定义异常处理机制
        /// 说道异常处理,其实我们脑海中的第一反应,也该是try/cache操作
        /// 但是在实际开发中,很有可能地址错误根本就进入不到try中,又或者没有被try处理到异常
        /// 该类就发挥了作用,能够很好的未经捕获的异常,并做相应的逻辑处理
        /// 自定义异常机制,主要集成ExceptionFilterAttribute 重写其OnException方法
        /// </summary>
        public class XYHAPIHandleError : ExceptionFilterAttribute
        {
            /// <summary>
            /// 处理异常
            /// </summary>
            /// <param name="actionExecutedContext">异常上下文</param>
            public override void OnException(HttpActionExecutedContext actionExecutedContext)
            {
                // 我们在平时的项目中,异常处理一般有两个作用
                // 1:记录异常的详细日志,便于事后分析日志
                // 2:对异常的统一友好处理,比如根据异常类型重定向到友好提示页面
    
                // 在这里面既能获取到未经处理的异常信息,也能获取到请求信息
                // 在此可以根据实际项目需要做相应的逻辑处理
                // 下面简单的列举了几个关键信息获取方式
    
                // action名称 
                string actionName = actionExecutedContext.ActionContext.ActionDescriptor.ActionName;
    
                // 控制器名称 
                string controllerName =actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerName;
    
                // url完整地址
                string url = (actionExecutedContext.Request.RequestUri.AbsoluteUri).ExUrlDeCode();
    
                // 请求方式 post get
                string httpMethod = actionExecutedContext.Request.Method.Method;
    
                // 请求IP地址
                string ip = actionExecutedContext.Request.GetIPAddress();
    
                // 获取全部的请求参数
                HttpRequest httpRequest = HttpContext.Current.Request;
                Dictionary<string, string> queryParameters = httpRequest.GetAllQueryParameters();
    
                // 获取异常对象
                Exception ex = actionExecutedContext.Exception;
    
                // 异常描述信息
                string exMessage = ex.Message;
    
                // 异常堆栈信息
                string stackTrace = ex.StackTrace;
    
                // 根据实际情况记录日志(文本日志、数据库日志,建议具体步骤采用异步方式来完成)
                // 自己的记录日志落地逻辑略 ......
    
                // 构建统一的内部异常处理机制,相当于对异常做一层统一包装暴露
                MBaseResult<string> result = new MBaseResult<string>()
                {
                    Code = MResultCodeEnum.systemErrorCode,
                    Message = MResultCodeEnum.systemError
                };
    
                actionExecutedContext.Response = new HttpResponseMessage(HttpStatusCode.OK);
                //需要自己指定输出内容和类型
                HttpContext.Current.Response.ContentType = "text/html;charset=utf-8";
                HttpContext.Current.Response.Write(JsonConvert.SerializeObject(result));
                HttpContext.Current.Response.End(); // 此处结束响应,就不会走路由系统
            }
        }

    总结

    .net过滤器,我个人的一句话理解就是:对action的各个阶段进行统一的监控处理等操作。.net过滤器中,其中每一个种过滤器的执行先后顺序为:Authorize(授权)-->ActionFilter自定义-->HandleError(错误处理)

    好了,就先聊到这而,如果什么地方说的不对之处,多多指点和多多包涵。我自己写了一个练习DEMO,里面会有每一种情况的处理说明。有兴趣的可以取下载下来看一看,谢谢。

    DEMO在GitHub地址为:https://github.com/xuyuanhong0902/XYH.FilterTest.git

    END
    为了更高的交流,欢迎大家关注我的公众号,扫描下面二维码即可关注,谢谢:

    认证授权

    时间戳
  • 相关阅读:
    Tableau(数据抽取)
    Oracle
    Visual Studio 2015与C#6.0新特性
    .net/c# memcached 安装和基本使用
    .net/c# memcached 获取指定前缀缓存键(keys)
    【笔记】Head First 设计模式
    C# WinForm 导出导入Excel/Doc [使用Aspose.Cells.dll]
    【转】《WCF服务编程 第一章 WCF基础》第一回
    WCF 部署到IIS(最基本的配置)
    串口通信(基础)
  • 原文地址:https://www.cnblogs.com/xiaoXuZhi/p/XYH_FilterTest.html
Copyright © 2020-2023  润新知