Action与系统的权限控制剖析

来源:互联网 发布:js调用微信二维码扫描 编辑:程序博客网 时间:2024/05/20 11:53

        权限系统是多数应用系统必不可以少的子系统。曾经为权限模型所困惑。今天突然想到用不同的Action代表不同的权限是实现权限控制是一个很好的做法。
我们知道,所谓的权限控制,就是一个“权限主体对于权限客体做了什么操作”的问题。其中主体代表了权限系统的用户,组,或者角色;而客体代表了权限系统中需要被保护的资源。显然,客体(资源)+操作的组合就代表了权限。
使用MVC框架或者模式的时候,我们往往会有一个类,代表用户的某一个操作。没有框架的时候,可以是一个Servlet,有框架的时候,比如struts,webwork,就是一个Action。我们往往会每个用户动作都会写一个Action。而这个Action往往是诸如:ListUserAction,RemoveUserAction,CreateMessageAction,ModifyUserStatusAction等等,这里的每一个Action都是一个用户的操作,而不经意之间,这些个Action类就代表了一个资源和权限的组合,也就成为了一个权限。同时,我们知道,在MVC框架中,请求一个Action类,要求把这个Action的mapping的名称传递值FrontController Servlet,以便于让Servlet知道请求的是哪一个Action,我们也可以方便的在ServletFilter或者Interceptor中获得所请求的Action别名。这一切都好办了,我们何不直接将Action别名作为用户的权限进行用户权限设置,在每次请求的时候,得到请求的Action别名到权限Map中比对?
 

基于以上这种方式来实现权限控制,具体实现方式可以有所不同,做一些改进:

本质上一个Action代表了一个请求的URL地址,而B/S项目本质上也就是只能根据URL来确定是否具备访问权限,从原理上来说,也是合适的。但是Action代表的URL可能未必和逻辑意义上的权限功能模块完全重合,既有可能大于,也有可能小于。

例如某个角色具备用户管理权限,而这个用户管理功能可能要对应六七个相关的Action,这就是逻辑意义上的权限大于Action,这种情况下在权限设置管理界面里面初始化某角色的权限,则是一件非常累人的事情,为了给一个角色增加一个功能权限,需要你分别设置六七个Action的权限。

再例如你控制一个简单的工作流,那么同一个Action ,会根据传递过去的不同的querystring来分派到不同的角色权限上,这就是同一个Action要针对不同的Role授予不同的权限了。

所以我觉得在Action上面再抽象出来一层Module,针对Module设置权限比较好。

 

我在webwork里是这样做的,首先定义一个接口:

代码
  1. public interface Protected {   
  2.     public boolean hasPermission();   
  3. }   

 

然后用拦截器做检查:

代码
  1. public class PermissionInterceptor implements Interceptor {   
  2.     public String intercept(ActionInvocation invocation) throws Exception {   
  3.         Action action = invocation.getAction();   
  4.         if(action instanceof Protected) {   
  5.             if(!((Protected) action).hasPermission()){   
  6.                 return NOPERMISSION;   
  7.             }   
  8.         }   
  9.         return invocation.invoke();   
  10.     }   
  11. }   

 

任何需要做权限检查的Action 实现这个接口即可,假设我们有一个管理员模块,我们可以写一个BaseAction:

代码
  1. public class BaseAdminAction implements Protected {   
  2.     private SecurityService securityService;   
  3.   
  4.     public boolean hasPermission() {   
  5.         return securityService.hasPermission(RemoteUser.get(), "admin");   
  6.     }   
  7.   
  8.     public void setSecurityService(SecurityService securityService) {   
  9.         this.securityService = securityService;   
  10.     }   
  11. }   

 

在这里采用setter注入SecurityService,具体的校验逻辑都在SecurityService的实现里面,你可以用简单的查表法,也可以用很复杂的user-group-role-permission来实现。

在管理员模块下的所有Action可以extends它就可以了:

代码
  1. public class DeleteGroupAction extends BaseAdminAction {   
  2.     //......   
  3. }   

 

如果你需要更细粒度的控制,或者是一些特殊逻辑权限检查,也可以选择重写这个方法

代码
  1. public class DeleteGroupAction implements Protected {   
  2.     public boolean hasPermission() {   
  3.         return !group.getName().equals("ADMIN_GROUP") && securityService.hasPermission(RemoteUser.get(), "maintain_group");   
  4.     }   
  5. }   

 

这种做法就是Action组合好资源和操作,将其传递给SecurityService做检查

其实我们既可以选择直接在Action.hasPermission方法里面做一些动作,也可以选择注入不同的SecurityService实现,这样就很灵活了。

 

 

robbin 写道
再例如你控制一个简单的工作流,那么同一个Action ,会根据传递过去的不同的querystring来分派到不同的角色权限上,这就是同一个Action要针对不同的Role授予不同的权限了。

 

我怎么觉得诸如queryString之类的权限控制不应在权限系统实现,基本上是个业务实现的问题。我觉得这个queryString往往是代表了“在什么情况下”这个权限有效的问题,如果都考虑进入权限系统的话,权限系统会非常庞大,难以维护。这种权限生效的“situation”的问题,我往往做到业务逻辑层中去check。

Action之上抽象一层Module的做法是不错。可是是不是可以考虑通过角色来解决诸如你这一类的问题呢?比如:用户管理的,我可以设置一个用户管理的角色,这一个角色可以授予执行createUser,removeUser,deleteUser权限,那么,我的用户是这个角色就可以咯。也就是说,是不是权限的粒度粗了,偶还是觉得用“资源+操作”去思考权限的粒度比较合适?比这个粒度粗的权限,就用角色来实现,比这个粒度细的权限就用业务实现。这样权限系统比较适中。

 

的确,这是一个很好的入口,更好的入口是Service层.

对于权限,以我目前的理解,是要分成两个问题:权限引擎和权限引入.

引擎就是要能提供一个可以切入应用逻辑的运算模块,做到:
1.能将从业务逻辑中提取出权限逻辑.
2.能够根据权限逻辑,对于当前操作进行运算,得到权限判断结果.
引擎是个相对独立的部分.

引入就是要能做到尽量透明的在系统中插入权限控制,必须:
1.尽量对业务透明.
2.提供合适的控制粒度.
引入可以采用AOP的Interceptor,只要在合理的层次上插入,就可以完成权限控制了.

在Action层插入,是一个方式.但是,因为Action层次的信息仍然不够丰富.更好的方式是在Service层次插入(对DomainObject操作).

jackyz     2005-01-21 16:56

 

robbin 写道
早先我做过一个系统,就是采用在Service层实现权限控制,但是后来发现不如在Action层实现起来方便。一方面是因为B/S应用中,客户端的请求动作本来就是一个一个的HTTP GET/POST,要做到最准确的权限控制,则必须针对Action。另一方面来说,如果你在Service层做权限控制,意味着你在Action还要写权限判定代码,那么就显得非常烦琐了。

 

如果要精确控制,必须要在比 Action 更细的层次做.举例来说:

论坛的帖子显示界面,如果当前用户对当前帖子有删除权限,则显示删除链接.否则,就不显示.
当前的操作(Action)是显示帖子,但是显示链接的逻辑判断需要对另外一个功能(删除功能)进行判断.这个判断是业务的必须,没有办法省略.

 

代码
  1. public class ViewArticle implements Action {   
  2.     public String execute() {   
  3.         ....   
  4.     }   
  5.     public boolean isDeleteAble() {   
  6.         ....   
  7.     }   
  8.     public boolean isEditAble() {   
  9.         ....   
  10.     }   
  11. }   

 

如果是对 Action 进行控制的话,恐怕就需要构造一个 URL 再行判断了.此外,Role等信息也难于获取.

ruby     2005-01-21 18:07

 

jackyz 写道
robbin 写道
早先我做过一个系统,就是采用在Service层实现权限控制,但是后来发现不如在Action层实现起来方便。一方面是因为B/S应用中,客户端的请求动作本来就是一个一个的HTTP GET/POST,要做到最准确的权限控制,则必须针对Action。另一方面来说,如果你在Service层做权限控制,意味着你在Action还要写权限判定代码,那么就显得非常烦琐了。

 

如果要精确控制,必须要在比 Action 更细的层次做.举例来说:

论坛的帖子显示界面,如果当前用户对当前帖子有删除权限,则显示删除链接.否则,就不显示.
当前的操作(Action)是显示帖子,但是显示链接的逻辑判断需要对另外一个功能(删除功能)进行判断.这个判断是业务的必须,没有办法省略.

 

代码
  1. public class ViewArticle implements Action {   
  2.     public String execute() {   
  3.         ....   
  4.     }   
  5.     public boolean isDeleteAble() {   
  6.         ....   
  7.     }   
  8.     public boolean isEditAble() {   
  9.         ....   
  10.     }   
  11. }   

 

如果是对 Action 进行控制的话,恐怕就需要构造一个 URL 再行判断了.此外,Role等信息也难于获取.

 

如果这样来写ACTION,那Action也太不"纯粹"了,包含了太多和当前action无关的逻辑,既然是要进行界面元素的显示控制,就在界面上控制好了,比如用taglib,至于权限列表,保存在session,显示界面元素时时取出对比一下,有此权限就显示此界面元素,没有就不显示.

jackyz     2005-01-23 11:53

 

ruby 写道
如果这样来写ACTION,那Action也太不"纯粹"了,包含了太多和当前action无关的逻辑,既然是要进行界面元素的显示控制,就在界面上控制好了,比如用taglib,至于权限列表,保存在session,显示界面元素时时取出对比一下,有此权限就显示此界面元素,没有就不显示.

 

确实不纯粹.但是,对于实例级别的权限控制,似乎没有更好的办法.

首先,明确一下.这里的需求是:实例级的权限控制(就是控制到谁对什么资源[的哪一个实例]具有什么操作).而不是功能级的权限控制(就是控制到谁对什么资源[不管哪一个实例都]具有什么操作).
实例级的权限控制,比如,A用户可以删除A帖子,但不能删除B帖子.它与具体资源的实例相关.
功能级的权限控制,比如,A用户可以删除帖子(A用户具有删除帖子的权限,不管什么帖子).它与具体资源的实例无关.
功能级的权限控制,与用户和资源类型相关,对于特定的用户来说,枚举资源类型得到的列表是确定的,确实可以用Session保存列表,在显示逻辑中处理.但是,实例级的权限控制,与具体的实例相关,对特定的用户,不可能枚举所有资源的实例.

这种情况下,只有Action的处理过程本身能够确定对于特定实例(ArticleId=xxx)做出权限判断.

从另一个角度来看,这里的Action实际上是一个"交互层",即,它服务于显示逻辑,显示逻辑需要一个"是否可以删除"的属性,那么它就应该提供,不管是从Session来取.还是从权限引擎来运算,这是另外一个问题.

这个做法应该说是合理的,但是,否有更好的方法,有待探讨.

差沙     2005-01-23 14:42

在action中的拦截只能拦截功能型的权限
而把实例型的的权限校验写在action里不是很好,action应该只是转发

觉得应该在action的下一层来实现这个校验,这样权限校验的耦合度还能第点,权限校验也方便重用。

比方说你要显示删除的时候要进行权限校验,真正删除的时候也要进行同样的校验,这样就应改把全校校验分离出来,我也不懂,瞎想的。

差沙     2005-01-23 14:45

 

ruby 写道
jackyz 写道
robbin 写道
早先我做过一个系统,就是采用在Service层实现权限控制,但是后来发现不如在Action层实现起来方便。一方面是因为B/S应用中,客户端的请求动作本来就是一个一个的HTTP GET/POST,要做到最准确的权限控制,则必须针对Action。另一方面来说,如果你在Service层做权限控制,意味着你在Action还要写权限判定代码,那么就显得非常烦琐了。

 

如果要精确控制,必须要在比 Action 更细的层次做.举例来说:

论坛的帖子显示界面,如果当前用户对当前帖子有删除权限,则显示删除链接.否则,就不显示.
当前的操作(Action)是显示帖子,但是显示链接的逻辑判断需要对另外一个功能(删除功能)进行判断.这个判断是业务的必须,没有办法省略.

 

代码
  1. public class ViewArticle implements Action {   
  2.     public String execute() {   
  3.         ....   
  4.     }   
  5.     public boolean isDeleteAble() {   
  6.         ....   
  7.     }   
  8.     public boolean isEditAble() {   
  9.         ....   
  10.     }   
  11. }   

 

如果是对 Action 进行控制的话,恐怕就需要构造一个 URL 再行判断了.此外,Role等信息也难于获取.

 

如果这样来写ACTION,那Action也太不"纯粹"了,包含了太多和当前action无关的逻辑,既然是要进行界面元素的显示控制,就在界面上控制好了,比如用taglib,至于权限列表,保存在session,显示界面元素时时取出对比一下,有此权限就显示此界面元素,没有就不显示.

 

在页面控制很危险的,可能你的页面上控制不让他显示了,但是用户能手动输出地址呀,这不还是要转到内部去校验么?

ruby     2005-01-23 15:08

 

引用
页面控制很危险的,可能你的页面上控制不让他显示了,但是用户能手动输出地址呀,这不还是要转到内部去校验么?

这儿的"控制",控制的是页面元素的显示与否,而不是进行权限的控制,具体一点讲,我们除了在action(iterceptor)或者更低一层(service)里面进行权限控制以外,还要在页面上管理相关页面元素的显示与否,目的不是为了控制,而是为了页面的"干净",比如,一个不具备"删除帖子"这个权限子的用户登录了以后,最好是不要把帖子的删除功能的"链接"显示出来,这样页面不就更人性一点了?(和我无关都不要给我显示,显示出来的都是我能操作的),如果他知道这个删除功能的link,直接在浏览器里面手写提交,那么在服务器端自然还有进一步的验证,比如在Action里面,比如在Service里面,然后会再导航到相关信息提示或者出错页面.至少前面提到的实例一级的权限控制和功能一级的权限控制,我一般不会用到料度太细的实例一级,(比如对AA,BB用户提交的帖子有删除权限,而对CC这个领导提交的帖子却是只读的)...倒是很想听听这方面的管理,实例一级的控制,那权限的粒度这么小,管理不是非常麻烦?不过可能在电子政务和政府相关项目产品中会用得比较多一点吧

 

控制权限,不但在要页面上,程序内部也必须控制,否则是相当危险的. 一般我的做法是在页面上进行粗粒度的控制,在程序内容进行细粒度的控制.

根据权限控制显示和根据权限控制执行在业务中,肯定都是必须的.缺少显示控制则界面将不够人性化.缺少执行控制则权限形同虚设.

这里举显示的例子,是为了表明,在Action的这个层次,除了一对一的执行控制以外,还有一对多的显示控制.所以,在Action这个层次并不是进行权限对应的合理层次.

控制应该是基于Action之下的Service层次.比如:

如此的框架:

代码
  1. public Interface Action {   
  2.   public String execute() throws ActionException;   
  3. }   
  4. public Interface Service {   
  5.   public void rule() throws ServiceExcception;   
  6.   pulblic Object flow() throws ServiceException;   
  7. }   
  8. public Class ServicePerformer {   
  9.   public boolean check(Service service) {   
  10.     boolean result = false;   
  11.     try {   
  12.       service.rule();   
  13.       result = true;   
  14.     } catch (ServiceException e) {   
  15.     }   
  16.     return result;   
  17.   }   
  18.   public Object perform(Service service) throws ServiceException {   
  19.     Object result = null;   
  20.     service.rule();   
  21.     result = service.flow();   
  22.     return result;   
  23.   }   
  24. }   

 

那么,具体的一个实现可能类似:

代码
  1. public class ViewAction implements Action {   
  2.   // 一个Action引用了两个Service;   
  3.   private Service deletService;   
  4.   private Service detailService;   
  5.   // 服务执行器   
  6.   private ServicePerformer servicePerformer;   
  7.   // 显示对象   
  8.   private ArticleDetail articleDetail;   
  9.   public String execute() throws ActionException {   
  10.     try {   
  11.       articleDetail = servicePerformer.perform(detailService);   
  12.       return SUCCESS;   
  13.     } catch (ServiceException e) {   
  14.       return ERROR;   
  15.     }   
  16.   }   
  17.   // 传递给显示逻辑用的DTO   
  18.   public ArticleDetail getArticleDetail() {   
  19.     return articleDetail;   
  20.   }   
  21.   // 传递给显示逻辑用的显示控制用属性   
  22.   public boolean isDeleteAble() {   
  23.     return servicePerformer.check(deleteService);   
  24.   }   
  25. }   

 

注意,以上代码,仅表概念,并未精心设计.

供探讨.

 

nihongye     2005-01-25 13:00

 

引用
在 url 中可以分辩 /deleteArticle.do 这个操作,但是并不能分辩当前用户是否就是 articleId=123 这个帖子的作者.也就是说,如果不切入 Article 的 DomainObject 从 Article 中取出它的作者和当前用户进行比对, 那么,权限的判断肯定是难于进行的.

 

那么在 acegi 的方案中,对于这样的需求是怎么处理的呢?


1.acegi提供了可配置的voter,一个验证请求交由多个voter进行处理,已经提供了roleVoter.可以加入自定义的voter来处理.

 

2.什么是嵌入权限?

3.我想假如后台需要为多个前台,或者是未来的前台服务的时候,也才有必要引入这种复杂性.

我也是看看,没应用过,有什么不对之处,还望海涵.

jjw     2005-01-25 14:21

不管在是在service还是action加权限验证,实际是RBAC模块是可以分离的,我在去年的时候完成了这个模块,然后项目的时候只要把这个模块加进去就可以了。如果实际项目是可以通过url来控制权限的,那么就可以不用写一行代码。 在RBAC模块里完全可以做到很细的控制。
资源概念(可以是url,还有刚才看到有人说.do后面的参数那不到,参数当然可以拿的的,只不过url被切分成两部分而已,一部分就是参数.可以通过getQueryString得到)
资源就是想要的到的最终物质,我们可以给每一个资源定义一个权限,也可以给某一类资源定义一个权限,而web项目 url的tree特别容易用来管理资源。资源是一棵树,如果你有管理树根的权限那么就有了这颗树的所有权限。
权限概念
权限是对资源的一种保护访问.用户要访问A资源前提是用户必须有A资源的访问权限.
角色概念
实事上我们不会直接把权限赋予给用户,而是通过角色来赋予给用户,因为用户拥有某一种权限是因为用户扮演着某一种角色。
A是个经理,他管理着B公司,他拥有b,c,d的权限。实际是不是A有这个权限,而是因为Abo是经理。因为经理拥有b,c,d权限
所以很显然在权限划分上,我们会把权限赋予给某一个角色,而不是赋予给个人。这样带来的好处是
如果公司换了经理,那么只要再聘用一个人来做经理就可以了,而不会出现因为权限在个人手里导致权限被带走的情况

分组概念(分组也是一棵树,用户就是这里的叶子)
只有角色是不够的,B公司发现A有财务问题成立了一个财务调查小组,然后我们赋予了这个小组财务调查员的角色(注意是赋予小组这个角色).这样这个小组的所有人员
都有财务调查的资格。而不需要给小组的每个人都赋予这个角色(实际上已经拥有了),分组概念也适合部门,因为任何一个部门在公司里或者社会上都在扮演着一个泛的角色。
用户
用户一定是属于某一个分组的,不存在不属于分组的用户.不过用户可以直接扮演(获得)角色,或者通过属于的分组来得到角色
最后一个概念
判断用户有没有访问资源的权限就看这个用户有没有访问这个资源的权限,也就是说分组,分部门,分角色最终是以权限来实现对资源的访问控制

 

 

nihongye 写道
1.acegi提供了可配置的voter,一个验证请求交由多个voter进行处理,已经提供了roleVoter.可以加入自定义的voter来处理.

 

2.什么是嵌入权限?

3.我想假如后台需要为多个前台,或者是未来的前台服务的时候,也才有必要引入这种复杂性.

 

1.voter?在上述应用中,是否可以设想有一个voter,它所做的事情是否就是根据articleId从数据库取出article.author,与当前用户进行比对,如果相符就vote同意?

2.嵌入权限的意思就是,在系统设计之初不对权限做太多的设计,在系统的外部通过可配置的权限模块来给系统引入权限逻辑.

3.这个前台后台没有听太懂.
象上述作例子论坛这样的系统,没有特定的后台管理界面.版主,普通浏览者,匿名用户等等,所有用户都使用相同的界面.只是界面上显示的内容不同.比如,在一个看帖界面中,版主能看到删除链接,点击该链接能执行删除当前帖子的操作.但,匿名用户看不见删除链接,即使用户自己拼出删除链接,也不能执行删除操作.

继续探讨.

nihongye     2005-01-25 20:42

1.是
2.同意.
3.后台和前台不是你说的意思.是指如:
ServiceBeanA代表后台(相对稳定的一层).
ActionC,ActionD,ActionE代表前台,或者URLC,URLD,URLE

原创粉丝点击