利用 AOP 实现 .NET 上完整的基于角色的访问控制(RBAC)模型

来源:互联网 发布:中国学位论文数据库 编辑:程序博客网 时间:2024/06/01 07:14

一. 背景

.NET 平台上没有完整的 RBAC 机制,.NET 中的安全模型(代码访问安全性:CAS)只是实现到 Role 层次,没有细化到 Task 层次,ASP.NET 2.0 中的诸多安全机制,如 Membership、Web.Config 的安全配置,都只能针对 Role 进行设置,大家在利用这些安全机制,往往需要在程序/代码硬编码(HardCode)角色,这样就无法实现在运行期自定义角色的功能
Windows 2000/2003 中自带的 Authorization Manager 虽然实现了较为完整的RBAC模型,但一般只适用于 Windows 用户,而且也需要手动去进行权限检查(调用 AccessCheck方法)
权限检查是一个通用操作,最好的实现方式就是面向方面的编程(AOP)
二、相关主题介绍

RBAC模型的要素:三个实体:用户、角色、任务(或操作)(User、Role、Task),其稳定性逐渐增强,两个关系,User<->Role、Role<->Task,其中:
User 是日常管理运行时建立
Role 是部署/交付建立
Task 是开发时确定
User<->Role 是日常管理运行时建立
Role<->Task 是部署/交付时建立
一般来说,Task是固定的,是和应用程序紧密绑定的,即使对之进行硬编码,也没有关系
User/Role 部分比较容易实现,例如ASP.NET 2.0中 Membership 的实现
三、具体实现

注:本文中实现 AOP 的思路主要来自于如下文章:Aspect Oriented Programming using .NET - AOP in C# (http://www.developerfusion.co.uk/show/5307/3/) ,这是我看到的、在.NET 上实现 AOP最简捷/方便的方法,它不便提供了原理介绍,也提供了 Visual Studio 2005 的 Sample Project ,其中有 Security Check 和 Logging 的 AOP 功能。它的优点在于,在实现 AOP 的同时,不需要再去建立接口(这是很多人的做法),直接在原有类上进行少量改动,即可实现完整的 AOP 功能。

1. 定义描述“Task”(任务)的 Attribute

using System;

namespace BusinessLogic.Security
...{
    /**////
    /// 用于定义系统中的操作
    ///
    [AttributeUsage(AttributeTargets.All,AllowMultiple=false,Inherited=true)]
    public sealed class Task : Attribute
    ...{
        private string _name,_description;

        public string Name
        ...{
            get ...{ return _name; }
            set ...{ _name = value; }
        }

        public string Description
        ...{
          get ...{ return _description; }
          set ...{ _description = value; }
        }
       
        public Task(string name,string description)
        ...{
            _name = name;
            _description = description;
        }

        public Task()
        ...{
        }
    }
}
2. 编写权限检查的 AOP 类 SecurityAspect,完成权限检查的功能

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Contexts;
using System.Runtime.Remoting.Activation;

namespace BusinessLogic.Security
...{
    //消息接收器
    internal class SecurityAspect : IMessageSink
    ...{
        //内部变量
        private IMessageSink m_next;

        //构造方法
        internal SecurityAspect(IMessageSink next)
        ...{
            m_next = next;
        }

        IMessageSink 实现#region IMessageSink 实现
        public IMessageSink NextSink
        ...{
            get ...{ return m_next; }
        }

        //同步处理消息
        public IMessage SyncProcessMessage(IMessage msg)
        ...{
            Preprocess(msg);
            IMessage returnMethod = m_next.SyncProcessMessage(msg);
            return returnMethod;
        }

        //异步处理消息(不实现)
        public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
        ...{
            throw new InvalidOperationException();
        }
        #endregion

        自定义的 AOP 方法#region 自定义的 AOP 方法
        private void Preprocess(IMessage msg)
        ...{
            //只处理方法调用
            if (!(msg is IMethodMessage)) return;

            //获取方法中定义的 Task 属性,交给权限检查类去检查
            IMethodMessage call = msg as IMethodMessage;
            MethodBase mb = call.MethodBase;
            object[] attrObj = mb.GetCustomAttributes(typeof(Task), false);
            if (attrObj != null)
            ...{
                Task attr = (Task)attrObj[0];

                if(!string.IsNullOrEmpty(attr.Name))
                    AzHelper.PermissionCheck(attr.Name);
            }

            //  Type type = Type.GetType(call.TypeName);
        }

        #endregion
    }


    public class PermissionCheckProperty : IContextProperty, IContributeObjectSink
    ...{
        IContributeObjectSink 实现,将 AOP 类加入消息处理链#region IContributeObjectSink 实现,将 AOP 类加入消息处理链
        public IMessageSink GetObjectSink(MarshalByRefObject o, IMessageSink next)
        ...{
            return new SecurityAspect(next);
        }
        #endregion

        IContextProperty 实现#region IContextProperty 实现
        public string Name
        ...{
            get ...{ return "PermissionCheckProperty"; }
        }
        public void Freeze(Context newContext)
        ...{
        }
        public bool IsNewContextOK(Context newCtx)
        ...{
            return true;
        }
        #endregion
    }

    //特性定义,用于 Consumer
    [AttributeUsage(AttributeTargets.Class)]
    public class PermissionCheckAttribute : ContextAttribute
    ...{
        public PermissionCheckAttribute() : base("PermissionCheck") ...{ }
        public override void GetPropertiesForNewContext(IConstructionCallMessage ccm)
        ...{
            ccm.ContextProperties.Add(new PermissionCheckProperty());
        }
    }
}?

3. 定义用于权限检查的两个类:AzMan、AzHelper

这两个类的功能是从 XML 配置文件中读入 Role 和 Task 的映射关系,以确定 Role 中是否包含 Task 的引用,从而确定当前 Role 是否具有对此 Task 的权限。

注:这里可根据项目的实际情况,如果你的 Role 和 Task 的映射关系是存放在 Windows 的授权管理器(Authorizatiom Manager)或数据库中,你可以使用自已
的方法来替换下列类。

在本例中,我的 Role 和 Task 的关系是存放在 XML 文件中,XML文件的格式如下所示:

<?xml version="1.0" encoding="utf-8"?>
<ACL>
  <Tasks>
    <Task Name="AddItem" Description="增加" />
    <Task Name="ModifyItem" Description="修改" />
    <Task Name="RemoveItem" Description="删除" />
    <Task Name="ListItem" Description="获取列表" />
  </Tasks>
  <Roles>
    <Role Name="Manager">
      <Task Name="AddItem" />
      <Task Name="ModifyItem" />
      <Task Name="RemoveItem" />
      <Task Name="ListItem" />
    </Role>
  </Roles>
</ACL>AzMan.cs 完成角色/任务映射关系的检查


using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;

namespace BusinessLogic.Security
...{
    public class AzMan
    ...{
        public static bool AccessCheck(string taskName, string[] roles, XmlDocument aclDoc)
        ...{
            XmlNode rootNode = aclDoc.DocumentElement;
            XmlNodeList roleNodes,taskNodes;
            bool IsPermissiable = false;

            for (int i = 0; i < roles.Length; i++)
            ...{
                roleNodes = rootNode.SelectNodes("Roles/Role[@Name='" + roles[i] + "']");

                if (roleNodes != null)
                ...{
                    taskNodes = roleNodes.Item(0).SelectNodes("Task[@Name='" + taskName + "']");
                    if (taskNodes.Count != 0)
                    ...{
                        IsPermissiable = true;
                        break;
                    }
                }
            }

            return IsPermissiable;
        }
    }
}AzHelper.cs 助手类,协助其他类,更好地调用 AzMan 类的方法,以及基于性能考虑,对Role<-->Task的XML配置文件进行缓存:

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Web;
using System.Web.Security;
using System.Diagnostics;
using System.Reflection;
using System.Web.Caching;


namespace BusinessLogic.Security
...{
    public class AzHelper
    ...{

        /**////
        /// 检查当前用户是否具有执行当前任务的权限,如果有权限,则不做任何处理
        /// 如果不具有权限,则引发异常
        ///
       
        public static void PermissionCheck(string taskName)
        ...{
            if (HttpContext.Current != null)
            ...{
                XmlDocument aclDoc = (XmlDocument)HttpContext.Current.Cache["ACLDoc"];

                if (aclDoc == null)
                ...{
                    CacheXml();
                    aclDoc = (XmlDocument)HttpContext.Current.Cache["ACLDoc"];
                }

                string[] roles = Roles.GetRolesForUser();

                if (!AzMan.AccessCheck(taskName, roles, aclDoc))
                    throw new UnauthorizedAccessException("访问被拒绝,当前用户不具有操作此功能的权限!");
            }

        }

        /**////
        /// 检查当前用户是否具有执行指定任务的权限
        ///
        /// 任务名称
        /// True/False 是否允许执行
        public static bool IsPermissible(string taskName)
        ...{
            if (HttpContext.Current != null)
            ...{
                XmlDocument aclDoc = (XmlDocument)HttpContext.Current.Cache["ACLDoc"];

                if (aclDoc == null)
                ...{
                    CacheXml();
                    aclDoc = (XmlDocument)HttpContext.Current.Cache["ACLDoc"];
                }

                string[] roles = Roles.GetRolesForUser();

                aclDoc.Load(HttpContext.Current.Server.MapPath("~/App_Data/ACL.xml"));

                return AzMan.AccessCheck(taskName, roles, aclDoc);
            }
            else return true;
        }

        /**////
        /// 缓存 XML 文件
        ///
        private static void CacheXml()
        ...{
            string fileName = HttpContext.Current.Server.MapPath("~/App_Data/ACL.xml");
            XmlDocument aclDoc = new XmlDocument();
            aclDoc.Load(fileName);
            HttpContext.Current.Cache.Insert("ACLDoc", aclDoc, new CacheDependency(fileName));
        }
    }
}
4. 业务逻辑类的实现

由于大多数工作都在 AOP 中实现了,所以业务逻辑类的实现较为简单,主要分为以下几个步骤:

在类的层次定义要求 AOP 方式权限检查的 Attribute: [PermissionCheck()]
使类继承自 ContextBoundObject 对象
在方法层次上利用 Task Attribute 来定义其对应的操作(注:多个方法可以定义为同一个 Task)
例如:ItemManager.cs


namespace BusinessLogic
...{
    [PermissionCheck()]
    public class ItemManager : ContextBoundObject
    ...{
        [Task("AddItem","增加")]
        public void AddItem(Item item)
        ...{
            //...
        }
     }
}

这样就可以了,CLR 会在运行时检查类的 PermissionCheck?Attribute,然后寻找方法上的 Task ,取出当前用户对应的 Role ,再去进行匹配检查,如果不能执行此操作,会抛出 UnauthorizedAccessException 的异常,在外部进行处理即可(如在 ASP.NET 中增加 ErrorPage 等)

5. 其他相关功能的实现

Q:如果我写程序时,在各个业务逻辑类定义了大量的 Task ,如果统一提取出来?

A:利用反射可取出程序集中定义的所有 Task ,代码如下:

List<string> dic = new List<string>();

StringBuilder sXml = new StringBuilder("");

string curDir = this.GetCurrentPath();
Assembly ass = Assembly.LoadFile(curDir + "//AppFramework.BusinessLogic.dll");
           
   foreach (Type t in ass.GetTypes())
   ...{
      MethodInfo[] mis = t.GetMethods();
      foreach (MethodInfo mi in mis)
      ...{
         object[] attrs = mi.GetCustomAttributes(false);
         if (attrs.Length > 0)
         ...{
            foreach (object attr in attrs)
            ...{
               if (attr.GetType().ToString().IndexOf("Task") >= 0)
               ...{
                   Task ta = (Task)attr;
                   //检查重复的 Task
                   if (dic.IndexOf(ta.Name) < 0)
                   ...{
                      dic.Add(ta.Name);
                      sXml.Append(string.Format("/r/n   ", ta.Name, ta.Description));
                   }
               }
           }
       }
   }
   //这就是所有的 Task 定义
   sXml.Append("/r/n");
}

此段代码是将 Task 定义保存到 XML 文件中,如果你想保存到 SQL Server/Authorzatiom Manager 中,对代码稍加修改即可。

Q:程序中的 Role 如何实现?

A:如果是 ASP.NET 应用程序,可以直接利用其中的 MemberShip Role 机制,还是比较简单的

Q:如果我想在界面上预先实现一些控制,如某用户不能进行某项操作,则直接将其对应的 Button 禁止或隐藏(Disable/Invisible)掉,如何做?

A:这可以利用 ASP.NET 2.0 中的表达式功能,直接检查当前用户的角色是否可以执行 Task ,如果不行,则利用返回的 Bool 值直接设置 Button 等控件的属性,做法如下:

1)在 App_Code 下定义表达式类 PermissionCheckExpressionBuilder.cs


[ExpressionEditor(typeof(PermissionCheckExpressionBuilderEditor))]
[ExpressionPrefix("PermissionCheck")]
public class PermissionCheckExpressionBuilder : ExpressionBuilder
...{
    public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
    ...{
        string taskName = entry.Expression;
        return new CodePrimitiveExpression(AzHelper.IsPermissible(taskName));
    }
}

public class PermissionCheckExpressionBuilderEditor : System.Web.UI.Design.ExpressionEditor
...{
    public override object EvaluateExpression(string expression, object parseTimeData, Type propertyType, IServiceProvider serviceProvider)
    ...{
        //return expression + ":" + parseTimeData + ":" + propertyType + ":" + serviceProvider;
        string taskName = expression;
        return AzHelper.IsPermissible(taskName);
    }
}


2)在 Web.Config 中加入上述表达式定义,以便可以直接在页面上引用

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
      <expressionBuilders>
        <add expressionPrefix="PermissionCheck" type="PermissionCheckExpressionBuilder"/>
      expressionBuilders>
configuration>3)直接在页面控件的相应属性上绑定表达式,如:

如果能执行此操作则显示,否则则隐藏

<asp:Button ID="Button1" runat="server" Text="AddItem" Visible=""  /> 如果能执行此操作则启用,否则则禁止

<asp:Button ID="Button2" runat="server" Text="AddItem" Enabled="" />4)如果想在代码中自行检查权限,可以直接调用相应方法,如:

protected void Button1_Click(object sender, EventArgs e)
...{
    AzHelper.PermissionCheck("AddItem");

    //..其他操作
}
5)如何建立 User<-->Role 的映射,Role<-->Task的映射

前者较为简单,ASP.NET 2.0 中就已经具有此功能,当然你也可以利用其 API 来实现自己的定义界面。

对于 Role-Task 的映射来说,首先利用上面的代码从程序集中取出所有 Task ,保存在 XML 文件中,然后在进行配置时,可以显示 Role 和 Task ,来进行映射。

如下图所示:

角色与任务的映射

基于角色的权限控制

基于角色的权限控制

原创粉丝点击