一. 背景
- .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.AccessCh