最近在做的一个系统需要实现权限拦截功能,主要是防止一些恶意的用户直接输入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的配置放在同一个配置文件中。