1 概述
几乎所有的项目都会涉及到权限控制的问题。在很多时候,设计者往往根据具体的需求来做权限控制而不理会通用性,但这样的设计往往造成一个项目一种模式,给日后的维护和升级扩展带来难度。
对于一个软件产品,其初级版本往往是简单的,因此权限管理也会相对简单,但随着不断的升级,功能的增加使业务模型变得越来越复杂,设计者就会发现原来的权限控制机制过于简陋了或者缺乏弹性了。这个时候再调整往往意味着要修改大量的历史代码并重新进行测试,这无疑是痛苦和让人印象深刻的。
因此,在项目设计的初期我们就需要为此作好充分的准备,这个准备包括两个方面:
l 选择一个正确的权限管理模型
l 建立一个独立的和业务代码无关的易于扩展的权限管理机制
2 RBAC权限管理模型
RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用 户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。
RBAC 模型作为目前最为广泛接受的权限模型。 NIST (The National Institute of Standards and Technology,美国国家标准与技术研究院)标准RBAC模型由4个部件模型组成,这4个部件模型分别是基本模型RBAC0(Core RBAC)、角色分级模型RBAC1(Hierarchal RBAC)、角色限制模型RBAC2(Constraint RBAC)和统一模型RBAC3(Combines RBAC)[1]。RBAC0模型如图1所示。
![JSF项目中实现基于RBAC模型的权限管理设计(一) - laomaowww - laomaowww的博客](http://img5.ph.126.net/8-PxQUA4cS0ZbCuaYAu-FA==/2565081462781694355.jpg)
与另一种常用的权限管理模型ACL比较,RBAC模型对权限控制的颗粒度更细,更容易扩展和富有弹性。
3 基于RBAC0的数据库模型设计
3.1 基本的数据库模型
![JSF项目中实现基于RBAC模型的权限管理设计(一) - laomaowww - laomaowww的博客](http://img0.ph.126.net/c-JpPG1-7sjE_cwjb0KFgA==/1089308159887528080.jpg)
这个模型用五张表来描述权限管理的模型。适用于角色和资源并不是太多的情况。
3.2 对资源分组后的数据库模型
![JSF项目中实现基于RBAC模型的权限管理设计(一) - laomaowww - laomaowww的博客](http://img2.ph.126.net/hVtfQWsRro4RDHG-4Lpgyg==/2585347661104870702.jpg)
对于复杂的系统,角色众多,资源众多,给角色分配资源的工作就比较繁重,授权的管理页面篇幅就会比较大。因此,这个模型增加了Permission表,用于给资源进行分组(分配)处理后再给映射到角色。
4 JSF中的RBAC的实现
4.1 资源分类和管理
4.1.1 资源分类
对于WEB项目,权限管理的资源基本上可以分为两种类型,一种是文件、目录类型,比如页面文件、多媒体文件等,以及目录。这一类型的资源往往表现为一种路径的形式,我们称这种类型的资源为URL类型。
另一种是页面上的菜单、按钮等具体操作资源,笔者将这种类型称为ACTION类型。
4.1.2 资源管理和初始化
对于一个信息管理型的项目来说,需要鉴权的资源是非常多的,因此对资源的管理也是后台管理的一个重要的组成部分。但我们的难题是这些资源在项目部署的时候如何轻松地导入到数据库中去呢。写SQL脚本,或者让后台管理员一条一条输入?
对于软件的使用者或者维护者来说,这样的方式都是一场噩梦。所以,作为软件开发者,必须为用户考虑到这一点。明智的做法是:预先定义好绝大部分的资源,写成xml形式的文件,在系统部署的时候导入到数据库中。而后台的资源管理模块(页面)仅仅把它当作一个查漏补缺的功能吧。
很多WEB项目往往会做一个install页面,这个页面要求用户输入一个超级管理员账号和信息。在用户提交时在后台完成数据库建库、初始化数据(给某些需要的表插入预定义记录)和其它一些初始化配置的工作。这是一个很好的习惯,我们对资源的初始化也可以在Install中完成,包括预定义的角色、以及预定义角色的资源分配。
4.2 鉴权服务模块
根据用户、角色、资源这个三者的关系,设计一个鉴权服务类以供调用是一个明智的想法。
具体的代码就不在此罗列了。
4.3 权限验证模块设计
一个好的权限管理机制在项目中应用时,最好不要让程序员在具体业务代码的方法中来判断用户权限。因为这意味着大量重复的代码。同时,也会导致权限机制的修改造成所有业务代码都需要修改一遍。
最好办法是实现与具体业务代码无关的独立的权限验证模块。这个模块可以拦截用户对资源的访问请求,并且在该请求被实施前做出权限判断,将权限不符的访问导向警告或提示页面。
在普通的JSP项目中,我们往往会利用Tomcat的Servlet filter机制来实现这样的功能,但filter机制的颗粒度不够,无法做到ACTION级别。另一个问题是,在JSF项目中,页面的跳转默认使用的是Forward形式,而不是redirect形式的URL重定向,因此Filter往往无法截获页面的转向。
因此,在JSF项目中我们首先需要解决如何截获到用户对资源的访问的问题。
4.3 权限验证模块设计
一个好的权限管理机制在项目中应用时,最好不要让程序员在具体业务代码的方法中来判断用户权限。因为这意味着大量重复的代码。同时,也会导致权限机制的修改造成所有业务代码都需要修改一遍。
最好办法是实现与具体业务代码无关的独立的权限验证模块。这个模块可以拦截用户对资源的访问请求,并且在该请求被实施前做出权限判断,将权限不符的访问导向警告或提示页面。
在普通的JSP项目中,我们往往会利用Tomcat的Servlet filter机制来实现这样的功能,但filter机制的颗粒度不够,无法做到ACTION级别。另一个问题是,在JSF项目中,页面的跳转默认使用的是Forward形式,而不是redirect形式的URL重定向,因此Filter往往无法截获页面的转向。
因此,在JSF项目中我们首先需要解决如何截获到用户对资源的访问的问题。
4.3.1 捕获URL级别的资源访问
在JSF中,我们有时候会使用PhaseListener观察JSF生命周期,这给我们一个思路,通过PhaseListener 可以在一个点上(JSF生命周期的Restore View phase阶段)来获取请求和输出路径信息(这个路径实际上就是我们定义的URL类型的资源)。路径获取后我们就可以据此进行鉴权,从而避免在每个页面或者每个backing bean中判断用户访问权限,造成过多的冗余代码和管理上的混乱。
步骤一:在faces-config.xml中注册 自定义的PhaseListener。
<lifecycle>
<phase-listener>com.laomao.view.listener.PermissionPhaseListener</phase-listener>
</lifecycle>
步骤二:实现我们自己的PhaseListener接口
package com.laomao.view.listener;
import javax.el.MethodExpression;
import javax.faces.FacesException;
import javax.faces.application.Application;
import javax.faces.application.NavigationHandler;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class PermissionPhaseListener implements PhaseListener {
private static Log log = LogFactory.getLog(PermissionPhaseListener.class);
@Override
public void afterPhase(PhaseEvent event) {
PhaseId phaseId = event.getPhaseId();
if (phaseId == PhaseId.RESTORE_VIEW || phaseId == PhaseId.INVOKE_APPLICATION) {
FacesContext currentContext = FacesContext.getCurrentInstance();
String viewId= currentContext.getViewRoot().getViewId();
//ViewId 就是我们需要的路径,类似URL,如/noPermission.xhtml
System.out.println("afterPhase: " + viewId);
//判断用户的操作权限
boolean isEnabled = true;
//TODO: 鉴权代码
//…
if(!isEnabled){
gotoNoPermissionPage();
}
}
}
/**
* 转入无权限声明页面
* @param expression
*/
private void gotoNoPermissionPage() {
FacesContext context = FacesContext.getCurrentInstance();
Application application = context.getApplication();
NavigationHandler navHandler = application.getNavigationHandler();
navHandler.handleNavigation(context, null, Constants.NAVIGATION_NO_PERMISSION);
context.renderResponse();
}
}
PhaseListener可以捕获到URL的变化,对于页面内部的操作则无能为力了。例如CommandButton的操作,可能删除了一条记录,但URL却没有变化,仅仅是页面局部进行了刷新。因此,我们还需要一个可以捕获到用户对ACTION类型的资源的访问的利器。这个利器就是ActionListener。
4.3.2 捕获ACTION级别的资源访问
当一个按钮被按下,或者命令联结被点击时,JSF会在invork application阶段的broadcast
event时通过ActionListener调用事先绑定的事件处理方法。如果有用户自定义的ActionListener时,JSF将使用用户自定义ActionListener来代替默认的ActionListener。
用户自定义ActionListener必须实现javax.faces.event.ActionListener接口,并实现其接口方法:public void processAction(ActionEvent actionEvent)。
注意,在这个实现方法里,必须调用控件的事件处理方法(JSF Backing Bean中的事件处理方法),并对其返回值做出画面迁移处理和其他你自己的处理等。
步骤一:在faces-config.xml中注册 自定义的ActionListener。
<application>
<action-listener>com.laomao.view.listener.ActionListenerImpl</action-listener>
</application>
步骤二:实现我们自己的ActionListener接口
package com.laomao.view.listener;
import javax.el.ELContext;
import javax.el.MethodExpression;
import javax.faces.FacesException;
import javax.faces.application.Application;
import javax.faces.application.NavigationHandler;
import javax.faces.component.ActionSource2;
import javax.faces.context.FacesContext;
import javax.faces.el.EvaluationException;
import javax.faces.el.MethodBinding;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;
import javax.faces.event.ActionListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class ActionListenerImpl implements ActionListener {
private static Log log = LogFactory.getLog(PermissionPhaseListener.class);
@Override
public void processAction(ActionEvent actionEvent) throws AbortProcessingException {
FacesContext facesContext = FacesContext.getCurrentInstance();
Application application = facesContext.getApplication();
ActionSource2 actionSource = (ActionSource2)actionEvent.getComponent();
MethodBinding methodBinding = actionSource.getAction();
String fromAction;
String outcome;
if (methodBinding == null)
{
fromAction = null;
outcome = null;
}
else
{
fromAction = methodBinding.getExpressionString();
try
{
outcome = (String) methodBinding.invoke(facesContext, null);
//fromAction与faces-config.xml中登记的是一致的
//fromAction: #{loginController.loginAction}, outcome: success
System.out.println("fromAction: " + fromAction + ", outcome: " + outcome);
boolean isEnabled = true;
//TODO: 鉴权代码
//…
if(!isEnabled){
//如果无此权限,转到权限不足页面
gotoNoPermissionPage(actionSource.getActionExpression());
}
}
catch (EvaluationException e)
{
Throwable cause = e.getCause();
if (cause != null && cause instanceof AbortProcessingException)
{
throw (AbortProcessingException)cause;
}
else
{
throw new FacesException("Error calling action method of component with id " + actionEvent.getComponent().getClientId(facesContext), e);
}
}
catch (RuntimeException e)
{
throw new FacesException("Error calling action method of component with id " + actionEvent.getComponent().getClientId(facesContext), e);
}
}
NavigationHandler navigationHandler = application.getNavigationHandler();
navigationHandler.handleNavigation(facesContext,fromAction,outcome);
//Render Response if needed
facesContext.renderResponse();
}
/**
* 转入无权限声明页面
* @param expression
*/
private void gotoNoPermissionPage(MethodExpression expression) {
FacesContext context = FacesContext.getCurrentInstance();
Application application = context.getApplication();
NavigationHandler navHandler = application.getNavigationHandler();
navHandler.handleNavigation(context, null == expression ? null : expression.getExpressionString(), Constants.NAVIGATION_NO_PERMISSION);
context.renderResponse();
}
}