一. 背景
二、相关主题介绍
三、具体实现
注:本文中实现 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 实现 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 方法 自定义的 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 类加入消息处理链 IContributeObjectSink 实现,将 AOP 类加入消息处理链 public IMessageSink GetObjectSink(MarshalByRefObject o, IMessageSink next) { return new SecurityAspect(next); } #endregion
IContextProperty 实现 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 中实现了,所以业务逻辑类的实现较为简单,主要分为以下几个步骤:
例如: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 ="<%$ PermissionCheck:AddItem %>" />
< asp:Button ID ="Button2" runat ="server" Text ="AddItem" Enabled ="<%$ PermissionCheck:AddItem %>" />
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 ,来进行映射。
如下图所示:
角色与任务的映射
用户与角色的映射