【Asp.Net】过滤器使用记录


前言

在有一些所有请求都可能需要的操作时,我们可以添加过滤器来完成对请求的拦截,然后进行我们的操作,从而减少代码的冗余。
关键字: Ajax 跨域 重定向 身份验证拦截 异常拦截

问题

我们在对所有的请求拦截以后,没有考虑 Url请求ajax请求 的区别,就对所有请求一致返回一个页面(登录页、自定义错误页等等),于是 ajax 得到的请求结果为 html ,这不是我们期望的

场景

一、身份验证拦截,Ajax请求跨域重定向到页面后不加载,场景分析

业务场景

目前项目需要使用公司的OAuth身份验证平台做单点登录(Single Sign On,SSO),于是要在目前的MVC项目中添加身份验证相关的业务逻辑,项目中大量表格展示数据,使用了很多Ajax。我对项目大部分请求做了身份验证,Ajax请求也做了拦截。

问题

对未登录用户发起的的Ajax请求,返回重定向的登录页,但是实际页面并不会加载得到的登录页面。

分析

Ajax本就为页面局部刷新修改而做的,返回的页面只会被当做返回的普通数据。

在过滤器中身份验证未通过后,请求被重定向(302)了项目的login方法,然后根据项目信息重定向(302)到认证平台的对应登录页面(200),浏览器拦截跨域请求资源,此时返回给Ajax的Http状态码为0(就是没有返回状态码的情况)。于是,需要针对返回到Ajax的状态码为0时,做处理。
被拦截Ajax请求的情况

处理方法

通过强制刷新页面,利用页面请求拦截重新跳转。

在全局js中添加Ajax的请求错误回调设置方法(还有其他方法ajaxComplete之类的,见W3CSchool)

/*ajax请求失败回调默认方法*/
$(document).ajaxError(function (event, xhr, options, exc) {
    if (xhr.status === 0) {
        window.location.reload();    // 强制刷新页面
    }
    else if (xhr.status === 302) {
        console.log(xhr);
    }
    else if (xhr.status === 401) {
        window.location.href = '/login';
        console.log(xhr);
    }
    else if (xhr.status === 404) {
        console.log(xhr);
    }
    else if (xhr.status === 500) {
        console.log(xhr);
    }
});

小结

这里暂时处理,返回状态码0的时候直接强制刷新页面,然后对页面刷新的身份验证拦截重定向到登录页。
考虑修改,验证请求类型为Ajax请求时,身份验证失败直接返回失败响应状态码,并使用Js直接重定向到登录页面,省去无效重定向。

续:

使用Request.IsAjaxRequest();可以判断请求类型,然后就方便处理了。可以对异步请求返回json对页面请求返回视图即可。

MVC身份验证过滤器

类似下面的,过滤器主要进行身份验证,跳转SSO的认证平台及存储获取的令牌等操作由/login进行,不在此记录。

using System.Web;
using System.Web.Mvc;

namespace Filter
{
    /// <summary>
    /// 身份验证过滤器
    /// 使用方法:[CustAuthorize]特性
    /// 1.特性可以使用在Class和Method上,Class上表示内部所有方法需要验证
    /// 2.可以Class上[CustAuthorize]并在不需要验证的方法上[AllowAnonymous]
    /// </summary>
    public class CustAuthorizeAttribute : AuthorizeAttribute
    {
        /// <summary>
        /// 请求的地址
        /// </summary>
        private string requestUrl { get; set; }

        /// <summary>
        /// 请求授权时执行
        /// </summary>
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            base.OnAuthorization(filterContext);   //进入AuthorizeCore
        }

        /// <summary>
        /// 自定义授权检查(返回False则授权失败)
        /// </summary>
        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            if (httpContext.Session["tk"] != null && !string.IsNullOrEmpty(httpContext.Session["tk"].ToString()))
            {
                string tk = httpContext.Session["tk"].ToString();    // 令牌,获取到以后存在Session了,
                bool check = Check.check(tk);    // 验证方法
                if (check)
                    return true;
            }
            httpContext.Session["tk"] = null;
            return false;     // 进入HandleUnauthorizedRequest 
        }

        /// <summary>
        /// 处理授权失败的HTTP请求
        /// </summary>
        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest())  // ajax请求
            {
                filterContext.HttpContext.Response.StatusCode = 401;    //返回前端 js对此类返回状态拦截响应或刷新页面(转页面请求重定向至登录)
                var json = JsonConvert.SerializeObject(new { state = "0", message = "权限验证失败!请重新登录" });
                filterContext.HttpContext.Response.Write(json);
                filterContext.HttpContext.Response.End();
            }
            else // 未登录请求页面地址
            {
                filterContext.HttpContext.Session["requestUrl"] = filterContext.HttpContext.Request.Url.AbsolutePath;
                // 重定向到登录
                filterContext.Result = new RedirectResult("~/login");
            }
        }
    }
}

登录及回调方法

    public class OAuthController : Controller
    {
        [Route("login")]
        public ActionResult Login()
        {
            string redirect = "~/index";
                       
            // 未登录
            if (string.IsNullOrEmpty(Session["tk"]?.ToString()))
            {
                // 获取认证服务器页面地址(请求地址+参数:回调地址等)
                redirect = SSOServer.GetLoginPageUrl(
                    $"http://{HttpContext.Request.Url?.Host}:{HttpContext.Request.Url?.Port}/loginCallBack");
            }

            return Redirect(redirect);
        }

        [Route("loginCallBack")]
        public ActionResult LoginCallBack(string tk)
        {
            string redirect = "~/index";

            if (string.IsNullOrEmpty(tk)) // 无令牌
            {
                redirect = "~/login";
            }
            else // 登录返回令牌
            {
                UserInfo user = SSOServer.GetUserInfo(tk); // 认证服务器获取用户信息
                if (user == null || (user.userType != UserType.Admin))   // 角色限制
                {
                    SSOServer.Logout(tk); // 登出
                    Session.Clear();
                    redirect = "~/Error/Forbidden";    //自定义错误页
                }
                else
                {
                    Session["tk"] = tk;
                    string requestUrl = Session["requestUrl"]?.ToString();    // 跳转回登录前页面
                    Session["requestUrl"] = null;
                    if (!string.IsNullOrEmpty(requestUrl) && !requestUrl.Contains("login"))
                        redirect = requestUrl;
                }
            }

            return Redirect(redirect);
        }

        [Route("logout")]
        [CustAuthorize]
        public ActionResult Logout()
        {
            SSOServer.Logout(Session["tk"].ToString());
            Session.Abandon();
            return Redirect("~/login");
        }
    }

二、MVC自定义动态错误页面后,Ajax错误回调方法接收不到对应状态码

关键字: Ajax http状态码

业务场景

MVC项目的错误页面自定义(即通过控制器获取Razor视图而不是HTML静态页面)后,Ajax的请求接收到Http状态码只有200的情况。
(静态的页面,MVC可以在Web.config中配置状态码)

分析

我在Global.asax里做了简单的500错误拦截,拦截后直接跳转了自定义的错误页面,动态重定向到指定控制器方法后,正常返回页面,所以状态码是200。

处理方法

  1. 需要返回错误状态
    Response.StatusCode = 500;
    控制器对应方法返回动态的错误页面时,先修改响应状态码,然后Ajax就可以收到错误了,可以像上面一个问题里拦截回调处理,做提示一下之类的操作。

  2. 正确的异常拦截姿势
    这里是目前我觉得比较正确的处理方法,参考asp.net MVC 过滤器使用案例:统一处理异常顺道精简代码 —— 无恨星晨

using Utils.LogUtils;
using System;
using System.Web;
using System.Web.Mvc;

namespace Filter
{
    public class HandleExceptionAttribute: HandleErrorAttribute
    {
        public override void OnException(ExceptionContext ctx)
        {
            // 异常及路由信息
            Exception innerException = ctx.Exception;
            string controllerName = (string)ctx.RouteData.Values["controller"];
            string actionName = (string)ctx.RouteData.Values["action"];
            //Logger.ErrorLog(typeof(HandleExceptionAttribute), innerException,
                    "OnException捕获:{0}->{1}:", controllerName, actionName);

            if ((new HttpException(null, innerException).GetHttpCode() == 500))
            {
                ActionResult result = null;
                if (!ctx.HttpContext.Request.IsAjaxRequest())    // 页面请求
                {
                    HandleErrorInfo model = new HandleErrorInfo(innerException, controllerName, actionName);
                    result = new ViewResult
                    {
                        ViewName = "~/Views/Shared/Error.cshtml",
                        MasterName = this.Master,
                        ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
                        TempData = ctx.Controller.TempData
                    };
                }
                else    // 异步请求
                {
                    result = new JsonResult
                    {
                        Data = new Model.VO.MessageVO {
                            state = "error",
                            message = $"OnException捕获:{controllerName}->{actionName}:{innerException}"
                        }
                    };
                }

                ctx.Result = result;
                ctx.ExceptionHandled = true;    // 避免再次进行默认异常处理
                ctx.HttpContext.Response.Clear();
                ctx.HttpContext.Response.StatusCode = 500;
                ctx.HttpContext.Response.TrySkipIisCustomErrors = true;
            }
            else   // 其他可能的异常,使用默认处理
            {
                base.OnException(ctx);
            }
        }
    }
}

总结

许久之后再看到这个问题,只感叹当时的积累不足,没有弄清楚过滤器的正确用法。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM