基于 Annotation 拦截的 Spring AOP 权限验证方法

时间:2021-09-07 22:04:01

基于 Annotation 拦截的 Spring AOP 权限验证方法

转自:http://www.ibm.com/developerworks/cn/java/j-lo-springaopfilter/

使用 Annotation 可以非常方便的根据用户的不同角色,分配访问 Java 方法的权限。在 Java Web 开发中,使用这种方法,可以提高系统的松耦合度,方便维护。

在 Web 开发过程中,一个非常理想的开发过程是,开发人员在开发中并不需要关心权限问题,不需要在 Java 方法中写很多逻辑判断去判断用户是否具有合适的角色和权限,这样开发会花费非常多的人力成本,因为所有的开发人员都需要了解关于权限的详细内容,也非常不容易进行后期维护。我们希望有专门的很少数量的开发人员了解权限内容,并且可以随时方便的修改和配置。于是,我们使用 Annotation,在 Java 方法之前使用 Annotation 可以非常方便的添加,修改和删除对于权限的管理功能。

本文描述了在开发过程中经常遇到的关于权限验证问题的一个典型应用案例,这个案例描述如下:系统要求只有登录用户才可以下定单。通过这个简单的例子,我们将看到如何完成整个系统的权限控制。

本文的开发环境如下:

  • Struts2
  • Spring 3.0
  • JDK1.6
  • AspectJ 6.9

本文将分为以下几个章节,详细描述提出的权限验证方法:

  1. AOP 的基本概念
  2. 权限验证系统架构详细讲解

AOP 的基本概念

AOP 是 Aspect Oriented Programming 的缩写,意思是面向方面的编程。我们在系统开发中可以提取出很多共性的东西作为一个 Aspect,可以理解为在系统中,我们需要很多次重复实现的功能。比如计算某个方法运行了多少毫秒,判断用户是不是具有访问权限,用户是否已登录,数据的事务处理,日志记录等等。

一般我们描述一个故事,都会说什么时间什么地点发生了什么事情,那 Join Point 的意思是,发生的地点,Advice 就是发生了什么事,Aspect 就是这个故事的整体,包含了 Join Point 和 Advice。PointCut 又把地点进行了规律性的总结,比如使用正则表达式 (com.example.service.*,即所有在 service 包下面的方法),把所有 Advice 发生的地点进行描述。

读者现在应该已经大概了解了 AOP 的基本概念,下面我们再来详细介绍一下:

Join Point:表示在程序中明确定义的执行点,典型的 Join Point 包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 Join Point。

PointCut:表示一组 Join Point,这些 Join Point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。

Advice:Advice 定义了在 PointCut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 Join Point 之前、之后还是代替执行的代码。

回页首

基于 Annotation 的 Spring AOP 权限验证方法的实现

Spring AOP 目前只支持基于 method 的 Join Points,而不支持基于 fileds 的 Join Points,也可以使用 AspectJ 去实现基于 fields 的 AOP,这并不会破坏 Spring 的核心 API。 Spring AOP 更倾向于配合 Spring IoC 去解决在企业级系统中更为普遍的问题。

在这个具体的例子中,我们实现了这样一个场景,在用户下订单的时候,先判断用户是否已经登录,如果用户没有登录,系统将转到登录页面,要求用户登录。

1. 配置 applicationContext

在 Spring 中支持 AOP 的配置非常的简单,只需要在 Spring 配置文件 applicationContext.xml 中添加:

<aop:aspectj-autoproxy/>

同时在 applicationContext.xml 的 schema 中配置:

清单 1
 <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd">

在配置时,我们需要将引用的 jar 包放置在 WEB-INF/lib 目录下面:

需要的 jar 包有

基于 Annotation 拦截的 Spring AOP 权限验证方法

配置这些之后 Spring AOP 就可以开始工作了。

2. 定义 Annotation

首先,我们定义一个常量来表示用户是否登录:

清单 2
package com.example.myenum;
public enum ISLOGIN {
YES,
LOGOUT,
NO
}

这里也可以选择不使用 enum,UserAccessAnnotation 中的 isLogin() 方法也可以返回整数或 String 类型,返回类型并没有限制。常量定义之后,我们再定义 Annotation,在 UserAccessAnnotation 中定义 isLogin(),表示用户是否已经登录:

清单 3
package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import com.example.myenum.ISLOGIN; @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UserAccessAnnotation {
/**
* User has been login or not.
*
*/
ISLOGIN isLogin();
}

定义好之后,这个 Annoatation 将可以被放置在需要验证用户是否登录的方法前面,就像下面这样:

清单 4
package com.example.aspect;
public class OrderAction extends BaseAction{
……
@UserAccessAnnotation(>isLogin=ISLOGIN.YES)
public String Order(){
try{
Boolean result = orderService.order(Quote quote);
if(result) return SUCCESS;
}catch(Exception e) {
logger.debug(e);
this.addActionError(getText("user_no_permission_error"));
}
return INPUT;
}
……
}

在这里我们使用 UserAccessAnnotation 来表示需要在 Order 方法执行之前判断用户是否已经登录,如果没有登录,在 struts2 中,通过下面定义的 Exception 的捕获机制,将页面转到登录页面。

3. 在 applicationContext.xml 中定义 Aspect

清单 5
<bean id="permission" class="com.example.aspect.PermissionAspect" scope="prototype">
<property name="authService" ref="AuthService" />
</bean>

我们要在 Spring 中定义 PermissionAspect。在 Struts+Spring 架构中可以把 Aspect 看作是一个 Action,只不过 Aspect 是其他 Action 的前提条件或者结束动作。Aspect 定义中的 Service 属性和 Action 中的 Service 属性没有任何区别。这里我们用 AuthService 类来实现判断用户是否已经登录的逻辑。

4. 定义 PointCut

清单 6
@Aspect
public class SystemArchitecture {
/**
* A Join Point is defined in the action layer where the method needs
* a permission check.
*/
@Pointcut("@annotation(com.example.annotation.UserAccessAnnotation)")
public void userAccess() {} }

PointCut 即切入点,就是定义方法执行的点,before、after 或者 around。 一般情况下,我们把 PointCut 全部集中定义在 SystemArchitecture 类中,以方便修改和管理。

当实现 Aspect 时可以很方便的使用我们在 SystemArchitecture 中定义的 PointCut。

5. 实现 Aspect

清单 7
 package com.example.aspect; 

 import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before; import com.example.annotation.UserAccessAnnotation;
import com.example.base.action.BaseAction;
import com.example.myenum.USERTYPE;
import com.example.service.AuthService; @Aspect
public class PermissionAspect extends BaseAction{
……
AuthService authService = null; @Before(value="com.example.aspect.SystemArchitecture.userAccess()&&"+
"@annotation(userAccessAnnotation)",argNames="userAccessAnnotation") public void checkPermission(UserAccessAnnotation userAccessAnnotation)
throws Exception{
IsLogin isLogin = userAccessAnnotation.isLogin (); if(!authService.userLogin(user).equals(isLogin.toString())){
throw new NoPermissionException(getText("user_no_permission_error"));
}
}
……
}

在 checkPermission 方法前,我们首先定义 PointCut:

@Before(value="com.example.aspect.SystemArchitecture.userAccess()&&"+
"@annotation(userAccessAnnotation)",argNames="userAccessAnnotation").

argNames="userAccessAnnotation" 的意思是把 Annotation 当做参数传递进来,并判断用户登录状态是否与 Annotation 中的定义一致。如果不一致,就要抛出 NoPermissionException,通知系统该用户没有权限。

6. 在 Struts action 配置文件中定义 Global Exception

在 Struts.xml 中配置:

清单 8
 <global-results>
<result name="loginerror">/WEB-INF/jsp/login.jsp</result>
</global-results>
<global-exception-mappings>
<exception-mapping exception="com.example.exceptions.NoPermissionException"
result="loginerror"/>
</global-exception-mappings>

经过上面的配置,在 NoPermissionException 抛出之后,Struts2 会 catch 这个 exception,并转到 login.jsp 页面。

Annotation 的放置位置时非常灵活的,并不局限于放置在 Struts2 的 Action 之前,若您没有使用 struts,也可以放置在 Service 类的实现方法之前,让调用方法捕捉 exception。Aspect 如何处理用户没有登录的情况也可以根据实际需要去实现,同样不局限于抛出 exception 这种方式。总之,处理方法是非常灵活的,根据读者的需要可以随机应变。

回页首

总结

综上所述,我们利用在 Struts Action 之前增加 Annotation 的方式非常方便的验证用户在系统中的访问权限。需要验证登录与否的方法之前,只要简单的添加 Annotation,就可以进行登录判断。可见,通过这种方式,只需要很少的人力就可以管理整个系统的权限控制。可以很好的控制项目开发的成本,增加系统的灵活性。