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