MVC系列_权限管理总结(附MVC权限管理系统源码)
2012-08-07 18:32
295 查看
在上一节中我们总结了关于权限控制的方式,我们这一节讲解关于权限控制中角色权限的授予处理等等并做本系列的总结.
首先,我们来谈谈权限控制中角色权限的控制,上一节只是针对权限拦截中比较粗的控制,如果我们需要对每一个动作做细致的权限认证,我们仍然进一步设计权限处理.
比如:我们分配给一个角色只有浏览日志的权限,不允许他进行其他动作,针对这种细的粒度,我们就必须专门进行处理,就是关于角色动作验证的处理
当然,我们的数据库设计就必须就需要进一步改进.
这是我们的EF关系图:
主要看看关于tbModule,tbPermission部分,都是采用树形设计,为什么这样设计呢?
首先,我们必须要承认,Module也就是模块,应该是多层次的,就像我们的菜单,是很多级的一样,这种多层次需要我们设计成树形,授予权限,我们就可以很轻松的设计成树形进行处理.
其次,关于tbPermssion,这个是权限,但是这个权限应该也是树形结构,比如我们设计一个菜单模块权限细分分为多种,增删改查,访问,等等,一个访问可能对应多种ajax请求,比如一个ajax读取Grid信息,一个ajax读取Tree信息,如果我们要做细致处理就要对应做多级处理,虽然,小型项目用不到,但是,我们的数据库设计拥有很大的灵活性.
权限可以细分,我们授权的时候,也不会不方便,只需要前端处理的合理,做好关联的处理,
但是,如果一个Update可能有多个子集Action方法,比如Get方法,例如:部门信息管理,我们一个更新动作,是先Get部门信息,然后在进行修改以后Update,所以,Get动作就是Update的子集操作,如果我们这个都控制,小型项目会变得太过复杂,怎么处理这种东西呢?
这时候我们之前的Attribute设计的多种权限处理就派上用场了.
首先,对于多种ajax动作,如果不是对数据库很较大影响的,对于小型项目,我们根本不用管它,直接标记为只要登陆即可访问,比如Get,GetGridTree之类的,读取信息这类的子集动作,直接给他个默认的LoginAllowView标记(登录即可访问),对于更高粒度,我们当然要进行细致的控制.
这样,我们的小型项目的Action控制基本上就两级,也就是控制在增删改查这个级别,
如果我们需要更高粒度的控制,只需要去掉LoginAllowView标记,在程序中对应添加Permission,并继续给我们的Action添加子集操作,比如Add关联子集的那些动作,全部录入到数据库中,虽然过程会长一点,但是我们可以通过UI设计的更友好来处理它.
把每一个具体操作关联的Action都录入处理,我们就可以了,当然我们的数据库设计就必须合理,必须有ParentID字段,用来控制树形结构.
针对权限的处理我们就到一段落,接下来,上演关于LigerUI项目中学到的一个东西,公共查询组件的设计:
曾几何时我们拼接查询语句来设计各种搜索的高级功能,搜索,成了一块心病,这是一年前,刚开始学的时候做一个搜索的设计,拼接查询语句,
每一次的模块都是靠后台手工编码sql来设计逻辑
看到这里是不是已经吐了?????
我看了以前的东西都已经受不了了.....
也接触到了LigerUI关于Filter过滤器组件以及后台的设计,也发现一位牛人海南胡勇的组合搜索设计,
这给了我一个相当大的思路,就是设计一个通用组合搜索组件,提高复用率,设计一种解析翻译规则,我们只需要按照规则提供数据,把sql解析的工作交给组件.
这种设计是相当的震撼,而且web的开发方式可以把where放在客户端,可以在不改变后台代码的前提下,更大程度上去设计查询.
LigerUI的作者那个权限管理,我们了好久,Filter也看了好久,这里谈谈心得:
主要的模块就是这几块:
FilterGroup:查询条件组合数组
FilterParam:查询参数集合
FilterRule:规则数组
FilterTranslator:翻译机(专门负责把数据以及条件翻译成sql语句)
重要:当然为了安全性考虑我们必须对翻译机过程中的数据做校验处理
[code]///用于存放过滤参数,比如一个是名称,一个是值,等价于sql中的Parameters
[/code]
[code]*创建时间:2012/7/2222:05:45
[/code]
剩下个工作就是交给翻译机进行翻译:(对作者的版本做了修改)
[code]*创建时间:2012/7/2222:05:49
[/code]
可能大家说,这玩意怎么用呀?LigerUI做了一个专门针对组合查询的组件,也可以自己去写,有了开源的代码,相信我们自己也可以写出自己的组件.
我们前台的搜索设计就更容易了:
看看我的日志搜索模块怎么设置的搜索
[code]$("#formsearch").ligerForm({
[/code]
也就是说,我们只需要设置规则,甚至可以自己去按照json格式传递给后台我们的规则就可以了,比如:
如果我们的Grid想设计条件,可以直接这么加
直接把这个where条件json化传递给后台就可以实现我们的按照条件查询Grid功能了.
这时候,大家想到了什么?
我们把条件where的部分更多的分担在UI层,我们的后台业务逻辑只需要解析where就可以再不改变业务逻辑的的条件下实现更复杂的业务逻辑.
其实,海南胡勇那位牛人设计的winform查询组件也是这个道理.
如果你还停留在拼接查询语句阶段,可以看看他们的设计.
最后,整个项目实际上,我非常不满意的是架构,这部分太差了,没有真正的公司工作经验,仅仅是粗浅的理解,深入学习,<<企业架构模式>>这本书买了也看不懂,缺少真正的工作经验,谈架构就是扯淡,所以,大家见谅.
如果觉得不错就推荐一下,支持一下吧!呵呵!
感谢,博客园的众多牛人提供了太多的学习资料和项目,让我们这些没有毕业的学生也有更多的学习资料.
这里也提供源码分享给更多学习的人.
注:推荐使用IE9,或者谷歌!IE8出现了BUG......呃
附带源码网盘地址:http://pan.baidu.com/netdisk/singlepublic?fid=511632_3611299055
设计的主要截图:
首先,我们来谈谈权限控制中角色权限的控制,上一节只是针对权限拦截中比较粗的控制,如果我们需要对每一个动作做细致的权限认证,我们仍然进一步设计权限处理.
比如:我们分配给一个角色只有浏览日志的权限,不允许他进行其他动作,针对这种细的粒度,我们就必须专门进行处理,就是关于角色动作验证的处理
当然,我们的数据库设计就必须就需要进一步改进.
这是我们的EF关系图:
主要看看关于tbModule,tbPermission部分,都是采用树形设计,为什么这样设计呢?
首先,我们必须要承认,Module也就是模块,应该是多层次的,就像我们的菜单,是很多级的一样,这种多层次需要我们设计成树形,授予权限,我们就可以很轻松的设计成树形进行处理.
其次,关于tbPermssion,这个是权限,但是这个权限应该也是树形结构,比如我们设计一个菜单模块权限细分分为多种,增删改查,访问,等等,一个访问可能对应多种ajax请求,比如一个ajax读取Grid信息,一个ajax读取Tree信息,如果我们要做细致处理就要对应做多级处理,虽然,小型项目用不到,但是,我们的数据库设计拥有很大的灵活性.
权限可以细分,我们授权的时候,也不会不方便,只需要前端处理的合理,做好关联的处理,
但是,如果一个Update可能有多个子集Action方法,比如Get方法,例如:部门信息管理,我们一个更新动作,是先Get部门信息,然后在进行修改以后Update,所以,Get动作就是Update的子集操作,如果我们这个都控制,小型项目会变得太过复杂,怎么处理这种东西呢?
这时候我们之前的Attribute设计的多种权限处理就派上用场了.
首先,对于多种ajax动作,如果不是对数据库很较大影响的,对于小型项目,我们根本不用管它,直接标记为只要登陆即可访问,比如Get,GetGridTree之类的,读取信息这类的子集动作,直接给他个默认的LoginAllowView标记(登录即可访问),对于更高粒度,我们当然要进行细致的控制.
这样,我们的小型项目的Action控制基本上就两级,也就是控制在增删改查这个级别,
如果我们需要更高粒度的控制,只需要去掉LoginAllowView标记,在程序中对应添加Permission,并继续给我们的Action添加子集操作,比如Add关联子集的那些动作,全部录入到数据库中,虽然过程会长一点,但是我们可以通过UI设计的更友好来处理它.
把每一个具体操作关联的Action都录入处理,我们就可以了,当然我们的数据库设计就必须合理,必须有ParentID字段,用来控制树形结构.
针对权限的处理我们就到一段落,接下来,上演关于LigerUI项目中学到的一个东西,公共查询组件的设计:
曾几何时我们拼接查询语句来设计各种搜索的高级功能,搜索,成了一块心病,这是一年前,刚开始学的时候做一个搜索的设计,拼接查询语句,
每一次的模块都是靠后台手工编码sql来设计逻辑
看到这里是不是已经吐了?????
我看了以前的东西都已经受不了了.....
也接触到了LigerUI关于Filter过滤器组件以及后台的设计,也发现一位牛人海南胡勇的组合搜索设计,
这给了我一个相当大的思路,就是设计一个通用组合搜索组件,提高复用率,设计一种解析翻译规则,我们只需要按照规则提供数据,把sql解析的工作交给组件.
这种设计是相当的震撼,而且web的开发方式可以把where放在客户端,可以在不改变后台代码的前提下,更大程度上去设计查询.
LigerUI的作者那个权限管理,我们了好久,Filter也看了好久,这里谈谈心得:
主要的模块就是这几块:
FilterGroup:查询条件组合数组
FilterParam:查询参数集合
FilterRule:规则数组
FilterTranslator:翻译机(专门负责把数据以及条件翻译成sql语句)
重要:当然为了安全性考虑我们必须对翻译机过程中的数据做校验处理
///<summary>
[code]///用于存放过滤参数,比如一个是名称,一个是值,等价于sql中的Parameters
///</summary>
publicclassFilterParam
{
publicFilterParam(stringname,objectvalue)
{
this.Name=name;
this.Value=value;
}
publicstringName{get;set;}
publicobjectValue{get;set;}
///<summary>
///转化为ObjectParameter可变参数
///</summary>
///<returns></returns>
publicObjectParameterToObjParam()
{
ObjectParameterparam=newObjectParameter(this.Name,this.Value);
returnparam;
}
///<summary>
///为查询语句添加参数
///</summary>
///<paramname="commandText">查询命令</param>
///<returns></returns>
publicstaticstringAddParameters(stringcommandText,IEnumerable<FilterParam>listfilter)
{
foreach(FilterParamparaminlistfilter)
{
if(param.Value.IsValidInput())
{
commandText=commandText.Replace("@"+param.Name,"'"+param.Value.ToString()+"'");
}
}
returncommandText;
}
///<summary>
///转化为ObjectParameter可变参数
///</summary>
///<paramname="listfilter"></param>
///<returns></returns>
publicstaticObjectParameter[]ConvertToListObjParam(IEnumerable<FilterParam>listfilter)
{
List<ObjectParameter>list=newList<ObjectParameter>();
foreach(FilterParamparaminlistfilter)
{
list.Add(param.ToObjParam());
}
returnlist.ToArray();
}
[/code]
/*作者:tianzh
[code]*创建时间:2012/7/2222:05:45
*
*/
/*作者:tianzh
*创建时间:2012/7/2215:34:19
*
*/
namespaceTZHSWEET.Common
{
publicclassFilterRule
{
///<summary>
///过滤规则
///</summary>
publicFilterRule()
{
}
///<summary>
///过滤规则
///</summary>
///<paramname="field">参数</param>
///<paramname="value">值</param>
publicFilterRule(stringfield,objectvalue)
:this(field,value,"equal")
{
}
///<summary>
///实例化
///</summary>
///<paramname="field">参数</param>
///<paramname="value">值</param>
///<paramname="op">操作</param>
publicFilterRule(stringfield,objectvalue,stringop)
{
this.field=field;
this.value=value;
this.op=op;
}
///<summary>
///字段
///</summary>
publicstringfield{get;set;}
///<summary>
///值
///</summary>
publicobjectvalue{get;set;}
///<summary>
///操作
///</summary>
publicstringop{get;set;}
///<summary>
///类型
///</summary>
publicstringtype{get;set;}
}
}
[/code]
剩下个工作就是交给翻译机进行翻译:(对作者的版本做了修改)
/*作者:tianzh
[code]*创建时间:2012/7/2222:05:49
*
*/
usingSystem;
usingSystem.Collections;
usingSystem.Collections.Generic;
usingSystem.Text;
usingSystem.Linq;
usingSystem.Data.Objects;
namespaceTZHSWEET.Common
{
///<summary>
///将检索规则翻译成wheresql语句,并生成相应的参数列表
///如果遇到{CurrentUserID}这种,翻译成对应的参数
///</summary>
publicclassFilterTranslator
{
//几个前缀/后缀
///<summary>
///左中括号[(用于表示数据库实体前的标识)
///</summary>
protectedcharleftToken='[';
///<summary>
///用于可变参替换的标志
///</summary>
protectedcharparamPrefixToken='@';
///<summary>
///右中括号(用于表示数据库实体前的标识)
///</summary>
protectedcharrightToken=']';
///<summary>
///组条件括号
///</summary>
protectedchargroupLeftToken='(';
///<summary>
///右条件括号
///</summary>
protectedchargroupRightToken=')';
///<summary>
///模糊查询符号
///</summary>
protectedcharlikeToken='%';
///<summary>
///参数计数器
///</summary>
privateintparamCounter=0;
//几个主要的属性
publicFilterGroupGroup{get;set;}
///<summary>
///最终的Where语句(包括可变参占位符)
///</summary>
publicstringCommandText{get;privateset;}
///<summary>
///查询语句可变参数数组
///</summary>
publicIList<FilterParam>Parms{get;privateset;}
///<summary>
///是否为EntityToSql生成where翻译语句(EntityToSql就需要在实体前面加it,例如it.ID=@IDandit.Name-@Name)
///否则为普通的SQL语句可变参拼接
///</summary>
publicboolIsEntityToSql{get;set;}
publicFilterTranslator()
:this(null)
{
IsEntityToSql=false;
}
///<summary>
///构造函数
///</summary>
///<paramname="group"></param>
publicFilterTranslator(FilterGroupgroup)
{
this.Group=group;
this.Parms=newList<FilterParam>();
}
///<summary>
///翻译语句成sql的where查询条件
///</summary>
publicvoidTranslate()
{
this.CommandText=TranslateGroup(this.Group);
}
///<summary>
///对多组规则进行翻译解析
///</summary>
///<paramname="group">规则数组</param>
///<returns></returns>
publicstringTranslateGroup(FilterGroupgroup)
{
StringBuilderbulider=newStringBuilder();
if(group==null)return"1=1";
varappended=false;
bulider.Append(groupLeftToken);
if(group.rules!=null)
{
foreach(varruleingroup.rules)
{
if(appended)
bulider.Append(GetOperatorQueryText(group.op));
bulider.Append(TranslateRule(rule));
appended=true;
}
}
if(group.groups!=null)
{
foreach(varsubgroupingroup.groups)
{
if(appended)
bulider.Append(GetOperatorQueryText(group.op));
bulider.Append(TranslateGroup(subgroup));
appended=true;
}
}
bulider.Append(groupRightToken);
if(appended==false)return"1=1";
returnbulider.ToString();
}
///<summary>
///注册用户匹配管理,当不方便修改ligerRM.dll时,可以通过这种方式,在外部注册
///currentParmMatch.Add("{CurrentUserID}",()=>UserID);
///currentParmMatch.Add("{CurrentRoleID}",()=>UserRoles.Split(',')[0].ObjToInt());
///</summary>
///<paramname="match"></param>
publicstaticvoidRegCurrentParmMatch(stringkey,Func<int>fn)
{
if(!currentParmMatch.ContainsKey(key))
currentParmMatch.Add(key,fn);
}
///<summary>
///匹配当前用户信息,都是int类型
///对于CurrentRoleID,只返回第一个角色
///注意这里是用来定义隐藏规则,比如,用户只能自己访问等等,
///</summary>
privatestaticDictionary<string,Func<int>>currentParmMatch=newDictionary<string,Func<int>>()
{};
///<summary>
///翻译规则
///</summary>
///<paramname="rule">规则</param>
///<returns></returns>
publicstringTranslateRule(FilterRulerule)
{
StringBuilderbulider=newStringBuilder();
if(rule==null)return"1=1";
//如果字段名采用了用户信息参数
if(currentParmMatch.ContainsKey(rule.field))
{
varfield=currentParmMatch[rule.field]();
bulider.Append(paramPrefixToken+CreateFilterParam(field,"int"));
}
else//这里实现了数据库实体条件的拼接,[ID]=xxx的形式
{
//如果是EFToSql
if(IsEntityToSql)
{
bulider.Append("it."+rule.field+"");
}
else
{
bulider.Append(leftToken+rule.field+rightToken);
}
}
//操作符
bulider.Append(GetOperatorQueryText(rule.op));
varop=rule.op.ToLower();
if(op=="like"||op=="endwith")
{
varvalue=rule.value.ToString();
if(!value.StartsWith(this.likeToken.ToString()))
{
rule.value=this.likeToken+value;
}
}
if(op=="like"||op=="startwith")
{
varvalue=rule.value.ToString();
if(!value.EndsWith(this.likeToken.ToString()))
{
rule.value=value+this.likeToken;
}
}
if(op=="in"||op=="notin")
{
varvalues=rule.value.ToString().Split(',');
varappended=false;
bulider.Append("(");
foreach(varvalueinvalues)
{
if(appended)bulider.Append(",");
//如果值使用了用户信息参数比如:in({CurrentRoleID},4)
if(currentParmMatch.ContainsKey(value))
{
varval=currentParmMatch[value]();
bulider.Append(paramPrefixToken+CreateFilterParam(val,"int"));
}
else
{
bulider.Append(paramPrefixToken+CreateFilterParam(value,rule.type));
}
appended=true;
}
bulider.Append(")");
}
//isnull和isnotnull不需要值
elseif(op!="isnull"&&op!="isnotnull")
{
//如果值使用了用户信息参数比如[EmptID]={CurrentEmptID}
if(rule.value!=null&¤tParmMatch.ContainsKey(rule.value.ObjToStr()))
{
varvalue=currentParmMatch[rule.value.ObjToStr()]();
bulider.Append(paramPrefixToken+CreateFilterParam(value,"int"));
}
else
{
bulider.Append(paramPrefixToken+CreateFilterParam(rule.value,rule.type));
}
}
returnbulider.ToString();
}
///<summary>
///创建过滤规则参数数组
///</summary>
///<paramname="value"></param>
///<paramname="type"></param>
///<returns></returns>
privatestringCreateFilterParam(objectvalue,stringtype)
{
stringparamName="p"+++paramCounter;
objectval=value;
////原版在这里要验证类型
//if(type.Equals("int",StringComparison.OrdinalIgnoreCase)||type.Equals("digits",StringComparison.OrdinalIgnoreCase))
//val=val.ObjToInt();
//if(type.Equals("float",StringComparison.OrdinalIgnoreCase)||type.Equals("number",StringComparison.OrdinalIgnoreCase))
//val=type.ObjToDecimal();
FilterParamparam=newFilterParam(paramName,val);
this.Parms.Add(param);
returnparamName;
}
///<summary>
///获取解析的参数
///</summary>
///<returns></returns>
publicoverridestringToString()
{
StringBuilderbulider=newStringBuilder();
bulider.Append("CommandText:");
bulider.Append(this.CommandText);
bulider.AppendLine();
bulider.AppendLine("Parms:");
foreach(varparminthis.Parms)
{
bulider.AppendLine(string.Format("{0}:{1}",parm.Name,parm.Value));
}
returnbulider.ToString();
}
#region公共工具方法
///<summary>
///获取操作符的SQLText
///</summary>
///<paramname="op"></param>
///<returns></returns>
publicstaticstringGetOperatorQueryText(stringop)
{
switch(op.ToLower())
{
case"add":
return"+";
case"bitwiseand":
return"&";
case"bitwisenot":
return"~";
case"bitwiseor":
return"|";
case"bitwisexor":
return"^";
case"divide":
return"/";
case"equal":
return"=";
case"greater":
return">";
case"greaterorequal":
return">=";
case"isnull":
return"isnull";
case"isnotnull":
return"isnotnull";
case"less":
return"<";
case"lessorequal":
return"<=";
case"like":
return"like";
case"startwith":
return"like";
case"endwith":
return"like";
case"modulo":
return"%";
case"multiply":
return"*";
case"notequal":
return"<>";
case"subtract":
return"-";
case"and":
return"and";
case"or":
return"or";
case"in":
return"in";
case"notin":
return"notin";
default:
return"=";
}
}
#endregion
}
}
[/code]
可能大家说,这玩意怎么用呀?LigerUI做了一个专门针对组合查询的组件,也可以自己去写,有了开源的代码,相信我们自己也可以写出自己的组件.
我们前台的搜索设计就更容易了:
看看我的日志搜索模块怎么设置的搜索
//搜索表单应用ligerui样式
[code]$("#formsearch").ligerForm({
fields:[
{display:"用户名",name:"UserName",newline:true,labelWidth:100,width:220,space:30,type:"text",
attr:{op:"equal"},cssClass:"field"}
,
{display:"IP地址",name:"IPAddress",newline:false,labelWidth:100,width:220,space:30,type:"text",cssClass:"field"},
{display:"开始时间",name:"CreateDate",newline:true,labelWidth:100,width:220,space:30,type:"date",cssClass:"field",attr:{"op":"greaterorequal"}},
{display:"结束时间",name:"CreateDate",newline:false,labelWidth:100,width:220,space:30,type:"date",cssClass:"field",attr:{"op":"lessorequal"}}
],
appendID:false,
toJSON:JSON2.stringify
});
//增加搜索按钮,并创建事件
LG.appendSearchButtons("#formsearch",grid);
[/code]
也就是说,我们只需要设置规则,甚至可以自己去按照json格式传递给后台我们的规则就可以了,比如:
如果我们的Grid想设计条件,可以直接这么加
直接把这个where条件json化传递给后台就可以实现我们的按照条件查询Grid功能了.
这时候,大家想到了什么?
我们把条件where的部分更多的分担在UI层,我们的后台业务逻辑只需要解析where就可以再不改变业务逻辑的的条件下实现更复杂的业务逻辑.
其实,海南胡勇那位牛人设计的winform查询组件也是这个道理.
如果你还停留在拼接查询语句阶段,可以看看他们的设计.
最后,整个项目实际上,我非常不满意的是架构,这部分太差了,没有真正的公司工作经验,仅仅是粗浅的理解,深入学习,<<企业架构模式>>这本书买了也看不懂,缺少真正的工作经验,谈架构就是扯淡,所以,大家见谅.
如果觉得不错就推荐一下,支持一下吧!呵呵!
感谢,博客园的众多牛人提供了太多的学习资料和项目,让我们这些没有毕业的学生也有更多的学习资料.
这里也提供源码分享给更多学习的人.
注:推荐使用IE9,或者谷歌!IE8出现了BUG......呃
附带源码网盘地址:
设计的主要截图:
相关文章推荐
- 一步一步Asp.Net MVC系列_权限管理总结(附MVC权限管理系统源码)
- 销售C#ASP.NET通用权限管理系统组件源码经验总结:软件源码能卖钱【收钱也很累】
- MVC + EF + Bootstrap 2 权限管理系统入门级(附源码)
- 【商业版】2010年ASP.NET C#通用权限管理系统组件源码销售100套以上的经验总结【2011年配套源码要涨价了】
- 【商业版】2010年ASP.NET C#通用权限管理系统组件源码销售100套以上的经验总结【2011年配套源码要涨价了】
- spring boot+mvc+mybatis(通用mapper)+druid+jsp+bootstrap实现后台权限管理系统源码
- 【商业版】2010年ASP.NET C#通用权限管理系统组件源码销售100套以上的经验总结【2011年配套源码要涨价了】
- MVC + EF + Bootstrap 2 权限管理系统入门级(附源码)
- 【商业版】2010年ASP.NET C#通用权限管理系统组件源码销售100套以上的经验总结【2011年配套源码要涨价了】
- MVC + EF + Bootstrap 2 权限管理系统入门级(附源码)
- 销售C#ASP.NET通用权限管理系统组件源码经验总结:软件源码能卖钱【收钱也很累】
- 销售C#ASP.NET通用权限管理系统组件源码经验总结:软件源码能卖钱【收钱也很累】
- 通用权限管理系统[基于asp.net(c# 4.0) + MVC 4 + extjs 4.2 + PetaPoco](一、搭架子 - 有源码)
- 权限管理和信息化系统快速开发框架源码学学习总结
- (转)一步一步Asp.Net MVC系列_权限管理之权限控制
- Winform开发框架之客户关系管理系统(CRM)的开发总结系列2-基于框架的开发过程
- 一步一步Asp.Net MVC系列_权限管理之权限控制
- 【转载】MVC巧用枚举做权限管理 | NET开发中你可能会用到的常用方法总结 |学习 ASP.NET MVC
- 通用权限管理系统组件 (GPM - General Permissions Manager) 中实现文件发送接收功能,附源码
- C# ASP.NET 权限设计 完全支持多数据库多语言包的通用权限管理系统组件源码