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

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

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

过滤器

类名

实现接口

描述

授权

AuthorizeAttribute

IAuthorizationFilter

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

异常

HandleErrorAttribute

IExceptionFilter

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

自定义

ActionFilterAttribute

IActionFilter和IResultFilter

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

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实例代码

复制代码
///
/// 授权认证过滤器
///
public class XYHAPIAuthFilterAttribute : AuthorizationFilterAttribute
{
///
/// 认证授权验证
///
/// 请求上下文
public override void OnAuthorization(HttpActionContext actionContext)
{
// 有 AllowAnonymous 属性的接口直接开绿灯
if (actionContext.ActionDescriptor.GetCustomAttributes().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实例代码

复制代码
///
/// MVC自定义授权
/// 认证授权有两个重写方法
/// 具体的认证逻辑实现:AuthorizeCore 这个里面写具体的认证逻辑,认证成功返回true,反之返回false
/// 认证失败处理逻辑:HandleUnauthorizedRequest 前一步返回 false时,就会执行到该方法中
/// 但是,我平时在应用过程中,一般都是在AuthorizeCore根据不同的认证结果,直接做认证后的逻辑处理
///
public class XYHMVCAuthorizeAttribute : AuthorizeAttribute
{
///
/// 认证逻辑
///
/// 过滤器上下文
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逻辑前执行

MVC、API

OnActionExecuted

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

MVC、API

OnResultExecuting

对应的view视图渲染前执行

MVC

OnResultExecuted

对应的view视图渲染后执行

MVC

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

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

下面的一个实例代码:

API定义过滤器实例DEMO代码

复制代码
///
/// Action过滤器
///
public class XYHAPICustomActionFilterAttribute : ActionFilterAttribute
{
///
/// Action执行开始
///
///
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 的异常处理实例代码:

复制代码
///
/// MVC自定义异常处理机制
/// 说道异常处理,其实我们脑海中的第一反应,也该是try/cache操作
/// 但是在实际开发中,很有可能地址错误根本就进入不到try中,又或者没有被try处理到异常
/// 该类就发挥了作用,能够很好的未经捕获的异常,并做相应的逻辑处理
/// 自定义异常机制,主要集成HandleErrorAttribute 重写其OnException方法
///
public class XYHMVCHandleError : HandleErrorAttribute
{
///
/// 处理异常
///
/// 异常上下文
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 的异常处理实例代码:

复制代码
///
/// API自定义异常处理机制
/// 说道异常处理,其实我们脑海中的第一反应,也该是try/cache操作
/// 但是在实际开发中,很有可能地址错误根本就进入不到try中,又或者没有被try处理到异常
/// 该类就发挥了作用,能够很好的未经捕获的异常,并做相应的逻辑处理
/// 自定义异常机制,主要集成ExceptionFilterAttribute 重写其OnException方法
///
public class XYHAPIHandleError : ExceptionFilterAttribute
{
///
/// 处理异常
///
/// 异常上下文
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,里面会有每一种情况的处理说明。有兴趣的可以取下载下来看一看,谢谢。

END
https://www.bilibili.com/medialist/detail/ml984341513
https://www.bilibili.com/medialist/play/ml984432813
https://www.bilibili.com/medialist/detail/ml984432813
https://www.bilibili.com/medialist/detail/ml984437613
https://www.bilibili.com/medialist/detail/ml984437413
https://www.bilibili.com/medialist/detail/ml984437313
https://www.bilibili.com/medialist/detail/ml984436913
https://www.bilibili.com/medialist/detail/ml984436713
https://www.bilibili.com/medialist/detail/ml984436013
https://www.bilibili.com/medialist/detail/ml984435213
https://www.bilibili.com/medialist/detail/ml984434913
https://www.bilibili.com/medialist/detail/ml984434613
https://www.bilibili.com/medialist/detail/ml984360013
https://www.bilibili.com/medialist/detail/ml984359813
https://www.bilibili.com/medialist/detail/ml984359013
https://www.bilibili.com/medialist/detail/ml984359413
https://www.bilibili.com/medialist/detail/ml984358913
https://www.bilibili.com/medialist/detail/ml984358213
https://www.bilibili.com/medialist/detail/ml984357713
https://www.bilibili.com/medialist/detail/ml984357513
https://www.bilibili.com/medialist/detail/ml984357413
https://www.bilibili.com/medialist/detail/ml984265613
https://www.bilibili.com/medialist/detail/ml984356813
https://www.bilibili.com/medialist/detail/ml984265513
https://www.bilibili.com/medialist/detail/ml984265313
https://www.bilibili.com/medialist/detail/ml984265213
https://www.bilibili.com/medialist/detail/ml984264913
https://www.bilibili.com/medialist/detail/ml984264813
https://www.bilibili.com/medialist/detail/ml984264613
https://www.bilibili.com/medialist/detail/ml984264513
https://www.bilibili.com/medialist/detail/ml984264213
https://www.bilibili.com/medialist/detail/ml984264113
https://www.bilibili.com/medialist/detail/ml984263913
https://www.bilibili.com/medialist/detail/ml984263513
https://www.bilibili.com/medialist/detail/ml984263113
https://www.bilibili.com/medialist/detail/ml984262713
https://www.bilibili.com/medialist/detail/ml984262413
https://www.bilibili.com/medialist/detail/ml984262613
https://www.bilibili.com/medialist/detail/ml984261913
https://www.bilibili.com/medialist/detail/ml984184213
https://www.bilibili.com/medialist/detail/ml984184013
https://www.bilibili.com/medialist/detail/ml984183813
https://www.bilibili.com/medialist/detail/ml984183513
https://www.bilibili.com/medialist/detail/ml984183313
https://www.bilibili.com/medialist/detail/ml984182913
https://www.bilibili.com/medialist/detail/ml984182713
https://www.bilibili.com/medialist/detail/ml984182513
https://www.bilibili.com/medialist/detail/ml984182613
https://www.bilibili.com/medialist/detail/ml984181413
https://www.bilibili.com/medialist/detail/ml984180913
https://mvp.leju.com/article/6675660308700813932.html
https://mvp.leju.com/article/6675660286743632762.html
https://mvp.leju.com/article/6675660269702174975.html
https://mvp.leju.com/article/6675660249347218293.html
https://mvp.leju.com/article/6675660227729775211.html
http://www.linkshop.com.cn/club/dispfile.aspx?rootid=1060517
http://www.linkshop.com.cn/club/dispfile.aspx?rootid=1060518
http://www.linkshop.com.cn/club/dispfile.aspx?rootid=1060519
http://www.linkshop.com.cn/club/archives/2020/1060541.shtml
http://www.linkshop.com.cn/club/archives/2020/1060545.shtml
http://www.linkshop.com.cn/club/archives/2020/1060548.shtml
http://www.nasyun.com/thread-71381-1-1.html
http://www.nasyun.com/thread-71382-1-1.html
https://blog.csdn.net/kldsifbwasfsa0/article/details/106624294
https://blog.csdn.net/kldsifbwasfsa0/article/details/106624358
https://blog.csdn.net/mark1122/article/details/106624715
https://blog.csdn.net/mark1122/article/details/106624749
https://blog.csdn.net/weixin_47927365/article/details/106624808
https://giphy.com/channel/kmu5kk
https://giphy.com/channel/nwqy75
https://giphy.com/channel/l6bbt4
https://giphy.com/channel/n2v1m7
https://giphy.com/channel/oww3pe
https://giphy.com/channel/d61fh0
https://giphy.com/channel/o63n6g
https://giphy.com/channel/8x8xf6
https://giphy.com/channel/92apjx
https://giphy.com/channel/yqj5yt
https://giphy.com/channel/7a7qct
https://giphy.com/channel/nunf4v
https://giphy.com/channel/1i663s
https://giphy.com/channel/gqkti5
https://giphy.com/channel/lt0ja5
https://giphy.com/channel/2iak30
https://giphy.com/channel/6o5p24
https://giphy.com/channel/bb8r7s
https://giphy.com/channel/tt5z34
https://giphy.com/channel/k22kxx
https://giphy.com/channel/4d02mf
https://giphy.com/channel/i8bioa
https://giphy.com/channel/pifsbg
https://giphy.com/channel/tj31st
https://giphy.com/channel/4p5xew
https://giphy.com/channel/5wexef
https://giphy.com/channel/3f7jfk
https://giphy.com/channel/9nr2vx
https://giphy.com/channel/7asbcb
https://giphy.com/channel/6c9hbk
https://giphy.com/channel/oy0f2w
https://giphy.com/channel/fg32uc
https://giphy.com/channel/a57jp0
https://giphy.com/channel/sqhy9h
https://giphy.com/channel/jcgjgb
https://giphy.com/channel/nav43m
https://giphy.com/channel/859koo
https://giphy.com/channel/5t85j3
https://giphy.com/channel/0ay0ap
https://giphy.com/channel/d51nnd
https://giphy.com/channel/a8aacd
https://giphy.com/channel/zaisz8
https://giphy.com/channel/kurilr
https://giphy.com/channel/iywsqj
https://giphy.com/channel/zi6ls7
https://giphy.com/channel/tlkjks
https://giphy.com/channel/8aias5
https://giphy.com/channel/mvebr1
https://giphy.com/channel/9fxg0g
https://giphy.com/channel/9m6n85
https://giphy.com/channel/7yqhwy
https://giphy.com/channel/16owgm
https://giphy.com/channel/v6ow2f
https://giphy.com/channel/5en7l0
https://giphy.com/channel/absmld
https://giphy.com/channel/uc2umf
https://giphy.com/channel/is5trl
https://giphy.com/channel/fx0dem
https://giphy.com/channel/s4w7un
https://giphy.com/channel/h6rz9g
https://giphy.com/channel/yxgaoi
https://giphy.com/channel/vwyiw2
https://giphy.com/channel/1ofgxf
https://giphy.com/channel/hi773j
https://giphy.com/channel/dj1l99
https://giphy.com/channel/ooyg81
https://giphy.com/channel/w4w7zn
https://giphy.com/channel/3bddjv
https://giphy.com/channel/dyydg2
https://giphy.com/channel/wv63cl
https://giphy.com/channel/ip3i9a
https://giphy.com/channel/0nn4py
https://giphy.com/channel/h6fror
https://giphy.com/channel/ia7c6i
https://giphy.com/channel/z77x36
https://giphy.com/channel/jwq5z1
https://giphy.com/channel/a75420
https://giphy.com/channel/qyify8
https://giphy.com/channel/4z08qs
https://giphy.com/channel/z35zaj
https://giphy.com/channel/izy78q
https://giphy.com/channel/u5dmwc
https://giphy.com/channel/h3z661
https://giphy.com/channel/33gjno
https://giphy.com/channel/ewmzmi
https://giphy.com/channel/9293s0
https://giphy.com/channel/eyw2zw
https://giphy.com/channel/n2hw8g
https://giphy.com/channel/e0x4we
https://giphy.com/channel/x78ov6
https://giphy.com/channel/vjl234
https://giphy.com/channel/x0yabt
https://giphy.com/channel/8xtdwm
https://giphy.com/channel/rj5ra9
https://giphy.com/channel/gqq9kk
https://giphy.com/channel/z98h64
https://giphy.com/channel/uwoe4l
https://giphy.com/channel/0kcbf3
https://giphy.com/channel/qa5hww
https://giphy.com/channel/69fcv8
https://giphy.com/channel/ia736f
https://giphy.com/channel/t8tir0
https://giphy.com/channel/myvmx6
https://giphy.com/channel/lr40cv
https://giphy.com/channel/81863v
https://giphy.com/channel/pa4gqw
https://giphy.com/channel/uudtc3
https://giphy.com/channel/qyauya
https://giphy.com/channel/cd3u7j
https://giphy.com/channel/bb4fqg
https://giphy.com/channel/5f4hhw
https://giphy.com/channel/x5o02q
https://giphy.com/channel/jg6siq
https://giphy.com/channel/oq7kkk
https://giphy.com/channel/629wp4
https://giphy.com/channel/p6sj02
https://giphy.com/channel/g3f4i8
https://giphy.com/channel/jzzzt8
https://giphy.com/channel/3ykkz6
https://giphy.com/channel/8nh1r3
https://giphy.com/channel/g3l3em
https://giphy.com/channel/55gd8f
https://giphy.com/channel/1fzaqh
https://giphy.com/channel/4ax96x
https://giphy.com/channel/8sar44
https://giphy.com/channel/cjj05k
https://giphy.com/channel/0gw0n1
https://giphy.com/channel/tckucr
https://giphy.com/channel/hojk8z
https://giphy.com/channel/n1mhwy
https://giphy.com/channel/1euutd
https://giphy.com/channel/6cn3fg
https://giphy.com/channel/alavti
https://giphy.com/channel/vnpep3
https://giphy.com/channel/o6wfm3
https://giphy.com/channel/jihq36
https://giphy.com/channel/7u5vvt
https://giphy.com/channel/5i86kk
https://giphy.com/channel/bsect1
https://giphy.com/channel/ce19em
https://giphy.com/channel/8en313
https://giphy.com/channel/4l1owk
https://giphy.com/channel/7rtzpy
https://giphy.com/channel/ze20nn
https://giphy.com/channel/ds4bc5
https://giphy.com/channel/1ubte7
https://giphy.com/channel/xg64hp
https://giphy.com/channel/x7g79d
https://giphy.com/channel/zwgq0x
https://giphy.com/channel/8kxsgb
https://giphy.com/channel/6bgpg8
https://giphy.com/channel/iq83cb
https://giphy.com/channel/rzpr8z
https://giphy.com/channel/assqxo
https://giphy.com/channel/cmsktn
https://giphy.com/channel/ybf528
https://giphy.com/channel/w1n7me
https://giphy.com/channel/ynnyl3
https://giphy.com/channel/bgyj83
https://giphy.com/channel/687veg
https://giphy.com/channel/qdqmrr
https://giphy.com/channel/zbsq5y
https://giphy.com/channel/px58fp
https://giphy.com/channel/s4vlsu
https://giphy.com/channel/qqgry6
https://giphy.com/channel/v6270c
https://giphy.com/channel/j1vame
https://giphy.com/channel/fjjrzx
https://giphy.com/channel/j0uede
https://giphy.com/channel/zyqw6h
https://giphy.com/channel/q7x2gh
https://giphy.com/channel/qrp7ib
https://giphy.com/channel/f60qqf
https://giphy.com/channel/f8g2n8
https://giphy.com/channel/59bc43
https://giphy.com/channel/s5s0x6
https://giphy.com/channel/z56a1o
https://giphy.com/channel/d9kdh2
https://giphy.com/channel/a42viz
https://giphy.com/channel/kpcqap
https://giphy.com/channel/0jwa2x
https://giphy.com/channel/26hi88
https://giphy.com/channel/xpnegy
https://giphy.com/channel/3pzvvx
https://giphy.com/channel/6ee3va
https://giphy.com/channel/5k6bnl
https://giphy.com/channel/6nns92
https://giphy.com/channel/v8q982
https://giphy.com/channel/kxjq2h
https://giphy.com/channel/p6o4yf
https://giphy.com/channel/6z07rq
https://giphy.com/channel/2r9k0b
https://giphy.com/channel/1xw286

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值