Web分布式部署,跨应用程序Forms身份验证的集成方案

最近一个项目要求进行分布式部署、保证在双十一期间系统的正常运行,虽然该系统平时访问量不是很大,但是基于业务需要,必须在至少两台服务器上部署。

该系统需要登录后才可以使用,首先需要解决分布式部署的用户状态共享问题,在项目中使用的是Forms身份验证,

如果是用Session,可以考虑使用微软的Azure Redis Cache(https://msdn.microsoft.com/library/azure/dn690522.aspx)将session存储到Redis中。

1、针对Forms配置项的改造,主要是在web.cofig中配置了machineKey,参考https://msdn.microsoft.com/zh-cn/library/eb0zx8fc.aspx

    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
    <authentication mode="Forms">
      <forms name="Quote" loginUrl="~/Account/Login" protection="All" timeout="2880" defaultUrl="~/Home/index" path="/" />
    </authentication>
    <!--跨应用程序进行 Forms 身份验证 https://msdn.microsoft.com/zh-cn/library/eb0zx8fc.aspx-->
    <!--<machineKey validationKey="[your key here]" decryptionKey="[your key here]" validation="SHA1" />-->
    <machineKey validationKey="77A439696CB986680CEE71CB179BBFFA75AA0FE3AB875B278EE8C54536F2B364E1BDAB809BA26D4263C33863D29B4040CD55D9665E8002D26F04A80C701A4067" decryptionKey="79378FA6BD4BE839D0B8C1E94367A820C77F38FA9CD8C7F0" validation="SHA1"/>
    <authorization>
      <allow users="*" />
    </authorization>
    <anonymousIdentification enabled="true" cookieName=".DotNet" />

用户登录后 创建一个票据,放在cookie中,保存方法

        /// <summary>
        /// 创建一个票据,放在cookie中
        /// 票据中的数据经过加密,解决cookie的安全问题。
        /// </summary>
        /// <param name="userInfo">登录用户</param>
        /// <param name="issueDateTime">发布时间</param>
        /// <param name="experation">过期时间</param>
        /// <param name="isPersistent">持久性</param>
        public static void SetCookie(BaseUserInfo userInfo, DateTime? issueDateTime = null, DateTime? experation = null, bool isPersistent = true)
        {
            if (issueDateTime == null)
            {
                issueDateTime = DateTime.Now;
            }
            if (experation == null)
            {
                //设置COOKIE过期时间
                double userLoginExperation;
                if (double.TryParse(ConfigurationManager.AppSettings["UserLoginExperation"], out userLoginExperation))
                {
                    experation = DateTime.Now.AddHours(userLoginExperation);
                }
                else
                {
                    experation = DateTime.Now.AddHours(16);
                }
            }
            BaseSystemInfo.UserInfo = userInfo;
            BaseSystemInfo.UserInfo.ServicePassword = BaseSystemInfo.ServicePassword;
            BaseSystemInfo.UserInfo.ServiceUserName = BaseSystemInfo.ServiceUserName;
            BaseSystemInfo.UserInfo.SystemCode = BaseSystemInfo.SystemCode;
            JavaScriptSerializer javaScriptSerializer = new JavaScriptSerializer();
            string userData = javaScriptSerializer.Serialize(BaseSystemInfo.UserInfo);
            //生成验证票据,其中包括用户名、生效时间、过期时间、是否永久保存和用户数据等。
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, userInfo.NickName, (DateTime)issueDateTime, (DateTime)experation, isPersistent, userData, FormsAuthentication.FormsCookiePath);
            HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));
            cookie.Expires = (DateTime)experation;
            HttpResponse response = HttpContext.Current.Response;
            //指定客户端脚本是否可以访问[默认为false]
            cookie.HttpOnly = true;
            //指定统一的Path,比便能通存通取
            cookie.Path = "/";
            //设置跨域,这样在其它二级域名下就都可以访问到了 同一个网站下 
            //cookie.Domain = "jinhuoyan.net";
            response.AppendCookie(cookie);
        }

 

此处提供一下MachineKey的生成方法:

        #region public ActionResult CreateMachineKey(int m,int n) 生成machineKey密钥
        /// <summary>
        /// 生成machineKey密钥
        /// </summary>
        /// <param name="m"></param>
        /// <param name="n"></param>
        /// <remarks>
        /// 参数:
        /// 第一个参数是用于创建 decryptionKey 属性的字节数。
        /// 第二个参数是用于创建 validationKey 属性的字节数。
        /// 注意:所创建的十六进制字符串的大小是从命令行传入值的大小的两倍。例如,如果您为密钥指定 24 字节,则转换后相应的字符串长度为 48 字节。
        /// decryptionKey 的有效值为 8 或 24。此属性将为数据加密标准 (DES) 创建一个 16 字节密钥,或者为三重 DES 创建一个 48 字节密钥。
        /// validationKey 的有效值为 20 到 64。此属性将创建长度从 40 到 128 字节的密钥。
        /// 代码的输出是一个完整的<machineKey>元素,您可以将其复制并粘贴到Machine.config文件中。
        /// </remarks>
        [CheckLogin]
        public ActionResult CreateMachineKey(int m, int n)
        {
            //String[] commandLineArgs = System.Environment.GetCommandLineArgs();
            //string decryptionKey = CreateKey(System.Convert.ToInt32(commandLineArgs[1]));
            //string validationKey = CreateKey(System.Convert.ToInt32(commandLineArgs[2]));
            string decryptionKey = CreateKey(m);
            string validationKey = CreateKey(n);
            string result = string.Format("<machineKey validationKey=\"{0}\" decryptionKey=\"{1}\" validation=\"SHA1\"/>", validationKey, decryptionKey);
            return Content(result);
        }

        [NonAction]
        static String CreateKey(int numBytes)
        {
            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
            byte[] buff = new byte[numBytes];
            rng.GetBytes(buff);
            return BytesToHexString(buff);
        }
        [NonAction]
        static String BytesToHexString(byte[] bytes)
        {
            StringBuilder hexString = new StringBuilder(64);

            for (int counter = 0; counter < bytes.Length; counter++)
            {
                hexString.Append(String.Format("{0:X2}", bytes[counter]));
            }
            return hexString.ToString();
        }
        #endregion

 

2、创建一个属性,用于在Action上标注对应的菜单的Code

    /// <summary>
    /// CustomerResourceAttribute
    /// 自定义的对方法应用的属性,在Action上标注权限菜单对应的Code
    /// 
    /// 修改纪录
    /// 
    /// 2015-10-11 版本:1.0 SongBiao 创建文件。   
    /// 
    /// <author>
    ///     <name>SongBiao</name>
    ///     <date>2015-10-11</date>
    /// </author>
    /// </summary>

    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
    public sealed class CustomerResourceAttribute : Attribute
    {
        private readonly string _resourceName;

        public CustomerResourceAttribute(string resourceName)
        {
            _resourceName = resourceName;
        }
        /// <summary>
        /// 资源名称
        /// </summary>
        public string ResourceName
        {
            get { return _resourceName; }
        }

        /// <summary>
        /// 资源描述
        /// </summary>
        public string Descript { get; set; }
    }

3、创建一个用户检测用户是否登录的标签

    /// <summary>
    /// CheckLoginAttribute
    /// 用于检测用户是否处于登录状态的标签
    /// 某些功能只需要用户登录就可以使用
    /// 
    /// 修改纪录
    /// 
    /// 2015-10-11 版本:1.0 SongBiao 创建文件。   
    /// 
    /// <author>
    ///     <name>SongBiao</name>
    ///     <date>2015-10-11</date>
    /// </author>
    /// </summary>

    public class CheckLoginAttribute :System.Web.Mvc.AuthorizeAttribute //AuthorizeAttribute
    {
        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            bool pass = false;
            if (!httpContext.Request.IsAuthenticated)
            {
                httpContext.Response.StatusCode = 401;//无权限状态码
                pass = false;
            }
            else
            {
                pass = true;
            }
            return pass;
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            base.HandleUnauthorizedRequest(filterContext);
            if (filterContext.HttpContext.Response.StatusCode == 401)
            {
                filterContext.Result = new RedirectResult("/");
            }
        }
    }

4、创建一个身份验证过滤器,实现对匿名访问、登录后可访问、验证是否具有对应菜单(action)的处理

    /// <summary>
    /// 身份验证过滤器
    /// 1、匿名访问
    /// 2、登录就可以访问
    /// 3、需要验证是否有菜单或按钮或资源的权限
    /// 
    /// 
    /// 修改纪录
    /// 
    /// 2015-10-11 版本:1.0 SongBiao 创建文件。   
    /// 
    /// <author>
    ///     <name>SongBiao</name>
    ///     <date>2015-10-11</date>
    /// </author>
    /// </summary>

    public class PermissionCheckAttribute : IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }
            if (filterContext.HttpContext.Request.Url == null)
            {
                throw new ArgumentNullException("filterContext");
            }
            string pageUrl = filterContext.HttpContext.Request.Url.AbsolutePath; //OperateContext.GetThisPageUrl(false);
            //是否是Ajax请求
            var bAjax = filterContext.HttpContext.Request.IsAjaxRequest();

            //1、允许匿名访问 用于标记在授权期间要跳过 AuthorizeAttribute 的控制器和操作的特性 
            var actionAnonymous = filterContext.ActionDescriptor.GetCustomAttributes(typeof(AllowAnonymousAttribute), true) as IEnumerable<AllowAnonymousAttribute>;
            var controllerAnonymous = filterContext.Controller.GetType().GetCustomAttributes(typeof(AllowAnonymousAttribute), true) as IEnumerable<AllowAnonymousAttribute>;
            if ((actionAnonymous != null && actionAnonymous.Any()) || (controllerAnonymous != null && controllerAnonymous.Any()))
            {
                return;
            }
            //2、判断登录状态 Controller  Action 标签 某些功能只需判断是否登录 用户没登录 调到登录页面  
            var checkLoginControllerAttr =filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof (CheckLoginAttribute), true) as IEnumerable<CheckLoginAttribute>;
            if (checkLoginControllerAttr != null && checkLoginControllerAttr.Any())
            {
                return;
            }
            var checkLoginActionAttr = filterContext.ActionDescriptor.GetCustomAttributes(typeof(CheckLoginAttribute), true) as IEnumerable<CheckLoginAttribute>;
            if (checkLoginActionAttr != null && checkLoginActionAttr.Any())
            {
                return;
            }
            //3、有些要判断是否有某个菜单 action的权限 具体判断某个用户是否有某个权限
            //用于标记在授权期间需要CustomerResourceAttribute 的操作的特性
            var attNames = filterContext.ActionDescriptor.GetCustomAttributes(typeof(CustomerResourceAttribute), true) as IEnumerable<CustomerResourceAttribute>;
            //用户具有的菜单
            var moduleList = OperateContext.Current.ModuleList;
            if (moduleList == null || !moduleList.Any())
            {
                //没有获取到任何菜单 拒绝访问
                filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { Controller = "Message", action = "General", bAjaxReq = bAjax, message = "没有获取您拥有的权限菜单" }));
            }
            else
            {
                //判断用户的权限菜单中的code是否与控制器上标示的资源的code一致
                var joinResult = (from aclEntity in moduleList
                                  join attName in attNames on aclEntity.Code equals attName.ResourceName
                                  select attName).Any();
                if (joinResult)
                {
                    return;
                }
                else
                {
                    //没有对应的权限 拒绝访问
                    filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { Controller = "Message", action = "DenyAccess", bAjaxReq = bAjax, message = "您没有访问权限:" + pageUrl }));
                }
            }
        }
    }

这里注意下OperateContext.Current.ModuleList,这个是取得用户具有的菜单,通用权限系统底层提供有对应的接口。

5、在FilterConfig中加入权限验证

    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new PermissionCheckAttribute());
        }
    }

6、应用方法

对某个Action加入自定义验证权限的标签

不需要验证登录状态的

只要登录就可以访问的

 

至此,通用权限管理系统底层的权限控制、跨应用程序Forms身份验证集成完成了。如有疑问,欢迎提出。

posted @ 2015-10-15 20:43  三人成虎  阅读(731)  评论(2编辑  收藏  举报