使用Spring Aop實現許可權攔截功能
最近在做的一個系統需要實現許可權攔截功能,主要是防止一些惡意的使用者直接輸入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的配置放在同一個配置檔案中。