最近在做的一个系统需要实现权限拦截功能,主要是防止一些恶意的用户直接输入URL来对我们的系统造成破坏。
下面来说以下具体的实现:
首先看一下我们定义的Aspect类
package com.hhoj.judger.aspect;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import com.hhoj.judger.annotation.ValidatePermission;
import com.hhoj.judger.entity.User;
import com.hhoj.judger.util.HttpObjectHolder;
@Aspect
public class PermissionAspect {
@Pointcut(value="@annotation(com.hhoj.judger.annotation.ValidatePermission)")
public void validate() {}
@Around(value="validate()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Method method=getSourceMethod(pjp);
if(method!=null) {
ValidatePermission vp=method.getAnnotation(ValidatePermission.class);
int role=vp.role();
HttpServletRequest request=HttpObjectHolder.getCurrentRequest();
HttpServletResponse response=HttpObjectHolder.getCurrentResponse();
User user=(User)request.getSession().getAttribute("currentUser");
if(user==null||user.getRole()<role) {
/**
* 权限不足直接跳转到提醒页面
*/
String path=request.getContextPath();
response.sendRedirect(path+"/authenticationFailure");
return null;
}
}
return pjp.proceed();
}
/**
* 获取被拦截的方法
* @param jp
* @return
*/
private Method getSourceMethod(JoinPoint jp){
Method proxyMethod = ((MethodSignature) jp.getSignature()).getMethod();
try {
return jp.getTarget().getClass().getMethod(proxyMethod.getName(), proxyMethod.getParameterTypes());
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
return null;
}
}
该Aspect主要是对被 ValidatePermission 注解的方法实行拦截,然后通过反射获取注解中的参数值,再获取当前用户的角色,通过判断当前用户是否有权限执行该方法,如果权限不够则跳转到提示错误页面,否则继续执行。该过程中唯一不好解决的问题就是Request,Response等内置对象的获取。
我们可以采取两种方案:
1. 在Controller控制类中,每个需要被拦截的方法上都添加 HttpServletRequest,HttpServletResponse参数,Spring Mvc在处理请求的时候会自动为我们注入参数值,然后我们在Aspect中通过 ProceedingJoinPoint的getArgs 获取被拦截方法的参数,这样就可以取到我们需要的request和reponse,这个方法的弊端就是我们需要在每个被拦截的方法上都添加上HttpServletRequest,HttpServletResponse参数,在需要拦截的方法数目较多的情况下这也是个不小的工作量,这还不是主要的问题,我们大部分Controller中的处理方法都不需要用到HttpServletRequest或者HttpServletResponse,在一个方法上添加它永不到的参数,这种实现方法实在不太雅观。
2. 通过一个ThreadLocal对我们的request,或response进行绑定。需要用到的时候就在ThreadLocal中获取,通过该方法我们不仅仅可以在Aspect中使用这些内置对象,只要是我们想用,在任何地方都能使用。那么还有一个问题,这些对象应该在什么地方绑定?答案就是我们可以在拦截器中进行绑定,不管是Servlet的拦截器,还是Spring Mvc的拦截器都可以,这里我采用的是Spring Mvc的拦截器。下面是我的实现过程:
package com.hhoj.judger.filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.hhoj.judger.util.HttpObjectHolder;
/**
* 将Request,和Response绑定到当前线程
* 用户AOP拦截时使用
* @author zhu
*
*/
public class SaveHttpObjectFilter implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HttpObjectHolder.setCurrentRequest((HttpServletRequest)request);
HttpObjectHolder.setCurrentResponse((HttpServletResponse)response);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
}
package com.hhoj.judger.util;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 用于保存每个线程的request 和Response
* @author zhu
*
*/
public final class HttpObjectHolder {
private static ThreadLocal<HttpServletRequest>requestThreadLocal=new ThreadLocal<>();
private static ThreadLocal<HttpServletResponse>responseThreadLocal=new ThreadLocal<>();
public static HttpServletRequest getCurrentRequest() {
return requestThreadLocal.get();
}
public static void setCurrentRequest(HttpServletRequest request) {
requestThreadLocal.set(request);
}
public static HttpServletResponse getCurrentResponse() {
return responseThreadLocal.get();
}
public static void setCurrentResponse(HttpServletResponse response) {
responseThreadLocal.set(response);
}
}
在来看看 ValidatePermission注解的定义
package com.hhoj.judger.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 被此注解标注的方法需要进行权限校验
* @author zhu
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidatePermission {
int role () ;
}
然后在我们的Spring配置文件中进行配置,由于我们采用的是注解方式的Aop 所以只要在配置文件中加入
<aop:aspectj-autoproxy />
这里有个特别需要注意的地方!!由于我采用的是Spring +Spring MVC的开发模式,所以会产生两个Bean容器,Spring会创建一个,Spring MVC也会创建一个。Spring MVC的Bean 容器会把Spring容器当作是它的父容器, 这一点从Spring MVC的源码中就可以看出来,这是简化后的Spring MVC的Bean容器初始化过程。
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
return wac;
}
就是Spring的Bean容器,而cawc就是Spring MVC的Bean 容器。在我们查找Bean的时候子容器是可以查到到父容器中的Bean的,但是反过来就不行了。一般我们配置的时候都喜欢把Service等其他的Bean配置到Spring配置文件中,而把Controller配置到Spring MVC的配置文件中,那么这就就会产生一个问题,Spring MVC中的Controller Bean对Spring中的Bean 是不可见的,也就是说我们把AOP配置在Spring配置文件中,在运行的时候它是找不到我们的Contrller的。所以就会产生AOP配置不起作用的现象。解决方案:将Controller的Bean配置与Spring AOP的配置放在同一个配置文件中。