基于组件方法级别授权访问

板桥里人 http://www.jdon.com 2005/10/29(转载请保留)

  当用户登陆进入系统后,面临着其访问权限的检查,目前实际应用中,授权访问(ACL)一般有以下几种:

  • 1. Web资源,主要是以Web URL为特征,对Web资源的Jsp/Servlet 图片等目录下全部资源实现授权访问。
  • 2. 组件资源,同一个组件的不同方法实现对资源进行不同性质的操作,每种操作方法都需要实现授权访问,例如:A角色可以实现帖子创建,但不能实现帖子删除。
  • 3. 数据资源,保存在数据库中的某些数据只能让特定角色一些访问。比如某个帖子只能让积分超过多少的用户访问。

  其中Web资源的ACL实现有很多种,使用基于Web容器的安全验证或使用开源项目JGuard。Web资源的ACL的有一个特点,它只能对Web资源实现授权访问,如果你设置/auth目录下的资源只能被某些角色访问,也就是说网址http://localhost/myweb/auth/下包括图片等任何资源只能被指定角色访问,这样,对Web资源形成了有效的保护。

  假设你有一个组件服务,如TestService如下:

public interface TestService {
  public UserTest initUser();

  public void createUser(EventModel em);

  public void updateUser(EventModel em);

  public void deleteUser(EventModel em);

  public UserTest getUser(String userId);

  public PageIterator getAllUsers(int start, int count);
}

  如果你授权目录和未授权目录都有对TestService访问,如下面struts-config.xml

<action name="userActionForm" path="/userSaveAction"   type="com.jdon.strutsutil.ModelSaveAction"
  scope="request" validate="true" input="/user.jsp">
  <forward name="success" path="/result.jsp" />
  <forward name="failure" path="/result.jsp" />
</action>



<action name="userActionForm" path="/auth/userSaveAction"   type="com.jdon.strutsutil.ModelSaveAction"
  scope="request" validate="true" input="/auth/user.jsp">
  <forward name="success" path="/auth/result.jsp" />
  <forward name="failure" path="/auth/result.jsp" />
</action>

  也就是说,网址http://localhost/myweb/userSaveAction.do与http://localhost/myweb/auth/userSaveAction.do

  都是实现用户资料的保存,都将分别调用TestService的createUser和updateUser等方法。如下:

  如果你的设计目的是:用户的创建可以开放给任何人,但是用户资料的修改或删除只能由超级管理员Admin实施,其他任何人员都没有权限进行修改或删除。

  为了实施这个设计目的,如果只使用 Web资源的ACL,使用如上图设计,将存在安全隐患,因为http://localhost/myweb/userSaveAction.do是完全开放给任何人,任何人对将从这个入口实现对TestService的访问,这样系统容易留有安全漏洞。

  一个解决方案是将用户资料的修改或删除功能从TestService接口中分离出来,这是破坏了对象的封装性。

  根本解决之道是使用基于组件方法的ACL,也就是前面总结的第2条,下面我们谈谈在Jdon框架下如何实现这个功能。

  实现组件TestService的第一步思路是在其createUser和updateUser等方法中加入权限判断,这种方式缺点是将权限和业务逻辑(如数据库操作)混淆的在一起,授权在发生变化时,修改createUser方法有可能更改业务逻辑代码,造成很大的不安全性。很显然,ACL和业务逻辑必须分开。

  分离的方法首先使用设计模式,这里可以选用Decorator模式或Proxy模式,所以我们需要实现TestService接口的一个子类TestServiceProxy,在TestServiceProxy中的各个方法中进行权限检查,而原来的TestService子类实现TestServiceImp则完全封装的是是业务逻辑。jive就是采取这个实现原理的。

  但是这样做哟一个缺点,每个接口都要实现一个Proxy类,如果有很多服务接口,需要实现很多Proxy类,这些Proxy类数量多而庞杂,系统不简练。

  比较好的解决办法是使用拦截器概念来实现,也就是我们只要做一个通用的Proxy类,系统能够在运行时自动检查。

  使用Jdon框架可以很方便地实现这个目的,因为Jdon框架的组件都是可插拔,可配置的,因此只要我们编制一个权限拦截器插入Jdon框架,然后使用XML配置文件描述一下哪些方法只能被哪些角色访问,权限拦截器根据这个配置规则进行运行检验,这样既做到分离,又达到简洁的效果。

  首先,我们使用XML定义ACL的访问规则,这个规则XML可以由我们任意指定,因为是我们自己写XML解析器解析它,我把它设定如下格式:

<permission>

  <service ref="testService">
    <method name="createUser">
      <role>Admin</role>
    </method>
    <method name="updateUser">
      <role>Admin</role>
      <role>User</role>
    </method>
    <method name="deleteUser">
      <role>Admin</role>
    </method>
  </service>

</permission>

  在这个配置中,我们定义了TestService的createUser方法只能被Admin访问,而updateUser的方法只能被Admin和user访问。

  然后,我们自己做一个XML解析器解析它,使用Jdon框架辅助的Jdom读取XML很方便,拷贝框架内其他读取XML代码即可,这个类在本文附件源码包中的PermissionXmlParser。

  最后,我们编制一个拦截器,主要代码如下:

public class PermissionInterceptor implements MethodInterceptor {
....

  public Object invoke(MethodInvocation invocation) throws Throwable {
    logger.debug("enter PermissionInterceptor");
    ....

    boolean hasPerm = false;

    Method method = invocation.getMethod();
    String methodNameNow = method.getName(); //获取当前被访问服务的方法名
    String serviceName = targetMetaDef.getName();//获取当前被访问服务的名称

    if (!isAuth(serviceName, methodNameNow)){ //如果在XML中没有定义,放行
      return invocation.proceed();
    }
    try {
      TargetMetaRequest targetMetaRequest =           proxyMethodInvocation.getTargetMetaRequest();

      //获得Jdon框架的SessionContext,这个类似HttpSession
      //通过SessionContext可以在微容器框架内部使用Session类数据
      SessionContext sessionContext = targetMetaRequest.getSessionContext();

      //获得登陆用户名,下行类似request.getPrincipleName()方法
      //通过下行,可以在微容器框架内部获得基于Web容器登陆用户的Principle Name
      String principleName =             userPrincipalSetup.getPrincipalName(sessionContext);
      logger.debug("principleName=" + principleName);

      //使用SessionContext作为缓存,不必每次根据Principle Name查询角色时,
      //都会访问数据库
       String roleName = (String)sessionContext.getArrtibute("roleName");
      if (roleName == null){
            roleName = operatorDao.getOperator(principleName);
            sessionContext.setArrtibute("roleName", roleName);
      }

      //从XML配置中检查当前服务和方法以及角色是否在配置中,也就是是否有权限。
      hasPerm = isUserInRole(serviceName, methodNameNow, roleName);
    } catch (Exception e) {
      logger.error(e);
    }

    if (hasPerm)
      return invocation.proceed();
    else {
      logger.error("no permission operate method: " + methodNameNow + " for "
             + targetMetaDef.getClassName());
    throw new Throwable("no permission");//这里可以抛出一个无权限的Exception!
  }
}

.....

}

  当然为了使这个拦截器能够被Jdon框架装载,有两个方法:一个是更改jdonframework.jar包中META-INF目录下的aspect.xml,将PermissionInterceptor放入;或者在自己项目下建立一个文件myaspect.xml,在其中配置如下:

<aspect>

  <interceptor name="test_permissionInterceptor"       class="com.jdon.framework.test.service.PermissionInterceptor"       pointcut="pojoServices" />

</aspect>

  只要确保myaspect.xml在WEB-INF/classes下,Jdon框架就会自动装载,当然因为PermissionInterceptor使用到XML解析,还需要在jdonframework.xml配置解析器PermissionXmlParser如下:

<pojoService name="testService"   class="com.jdon.framework.test.service.TestServicePOJOImp"/>


<pojoService name="operatorDao" class="com.jdon.framework.test.dao.OperatorDao" >
  <constructor value="java:/NewsDS"/>
</pojoService>

<pojoService name="permissionXmlParser"   class="com.jdon.framework.test.service.PermissionXmlParser" >
  <constructor value="test_permission.xml"/>
</pojoService>

  其中test_permission.xml就是前面的权限定义XML配置文件。

  注意,上述使用Jdon框架上述基于组件方法的ACL前提是,必须首先获得验证合格的登陆用户信息,本例子中登陆系统是使用基于Web容器的登陆方法。

使用Shiro实现基于服务的多域身份验证和授权