1. 程式人生 > >Spring AOP 實現功能許可權校驗功能

Spring AOP 實現功能許可權校驗功能

實現功能許可權校驗的功能有多種方法,其一使用攔截器攔截請求,其二是使用AOP拋異常。
首先用攔截器實現未登入時跳轉到登入介面的功能。注意這裡沒有使用AOP切入,而是用攔截器攔截,因為AOP一般切入的是service層方法,而攔截器是攔截控制器層的請求,它本身也是一個處理器,可以直接中斷請求的傳遞並返回檢視,而AOP則不可以。

1.使用攔截器實現未登入時跳轉到登入介面的功能

1.1 攔截器SecurityInterceptor

package com.jykj.demo.filter;

import java.io.PrintWriter;

import javax.servlet
.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import com.alibaba.fastjson.JSON; import com.jykj.demo.util.Helper; import com
.jykj.demo.util.Result; public class SecurityInterceptor implements HandlerInterceptor{ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("SecurityInterceptor:"+request.getContextPath
()+","+request.getRequestURI()+","+request.getMethod()); HttpSession session = request.getSession(); if (session.getAttribute(Helper.SESSION_USER) == null) { System.out.println("AuthorizationException:未登入!"+request.getMethod()); if("POST".equalsIgnoreCase(request.getMethod())){ response.setContentType("text/html; charset=utf-8"); PrintWriter out = response.getWriter(); out.write(JSON.toJSONString(new Result(false,"未登入!"))); out.flush(); out.close(); }else{ response.sendRedirect(request.getContextPath()+"/login"); } return false; } else { return true; } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // TODO Auto-generated method stub } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // TODO Auto-generated method stub } }

1.2.spring-mvc.xml(攔截器配置部分)

<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
    <mvc:resources mapping="/resources/**" location="/resources/" />
<mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/*"/>  <!-- 攔截/  /test  /login  等等單層結構的請求  --> 
            <mvc:mapping path="/**/*.aspx"/><!-- 攔截字尾為.aspx的請求 -->
            <mvc:mapping path="/**/*.do"/><!-- 攔截字尾為 .do的請求 -->
            <mvc:exclude-mapping path="/login"/>
            <mvc:exclude-mapping path="/signIn"/>
            <mvc:exclude-mapping path="/register"/>
            <bean class="com.jykj.demo.filter.SecurityInterceptor">
            </bean>
        </mvc:interceptor>
    </mvc:interceptors>

這裡需要特別說明:攔截器攔截的路徑最好是帶有後綴名的,否則一些靜態的資原始檔不好控制,也就是說請求最好有一個統一的格式如 .do 等等,這樣匹配與過濾速度會非常快。如果不這樣,例如 用 /** 來攔截所有的請求,則頁面渲染速度會非常慢,因為資原始檔也被攔截了。

2.使用AOP實現功能許可權校驗

對於功能許可權校驗也可以類似地用攔截器來實現,只不過會攔截所有的請求,對不需要許可權校驗的請求沒有很好的過濾功能,所以採用AOP指定攔截需要校驗的方法的方式來實現之。

2.1 切面類 PermissionAspect

package com.jykj.demo.filter;

import java.io.IOException;
import java.lang.reflect.Method;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;

import com.jykj.demo.annotation.ValidatePermission;
import com.jykj.demo.exception.AccessDeniedException;
import com.jykj.demo.service.SysUserRolePermService;

/**
 * 事件日誌 切面,凡是帶有 @ValidatePermission 以及@ResponseBody註解 控制器 都要進行 功能許可權檢查,
 * 若無許可權,則丟擲AccessDeniedException 異常,該異常將請求轉發至一個控制器,然後將異常結果返回
 * @author Administrator
 *
 */
public class PermissionAspect {
    @Autowired
    SysUserRolePermService sysUserRolePermService;

    public void doBefore(JoinPoint jp) throws IOException{
        System.out.println(
                "log PermissionAspect Before method: " + jp.getTarget().getClass().getName() + "." + jp.getSignature().getName());
        Method soruceMethod = getSourceMethod(jp);
        if(soruceMethod!=null){
            ValidatePermission oper = soruceMethod.getAnnotation(ValidatePermission.class);
            if (oper != null) {
                int fIdx = oper.idx();
                Object[] args = jp.getArgs();
                if (fIdx>= 0 &&fIdx<args.length){
                    int functionId = (Integer) args[fIdx];
                    String rs = sysUserRolePermService.permissionValidate(functionId);
                    System.out.println("permissionValidate:"+rs);
                    if(rs.trim().isEmpty()){
                        return ;//正常
                    }
                }
            }
        }
        throw new AccessDeniedException("您無權操作!");
    }
    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;
    }
}

2.2自定義註解ValidatePermission

package com.jykj.demo.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Descrption該註解是標籤型註解,被此註解標註的方法需要進行許可權校驗
 */
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface ValidatePermission {
    /**
     * @Description功能Id的引數索引位置  預設為0,表示功能id在第一個引數的位置上,-1則表示未提供,無法進行校驗
     */
    int idx() default 0;
}

說明: AOP切入的是方法,不是某個控制器請求,所以不能直接返回檢視來中斷該方法的請求,但可以通過拋異常的方式達到中斷方法執行的目的,所以在before通知中,如果通過驗證直接return返回繼續執行連線點方法,否則丟擲一個自定義異常AccessDeniedException來中斷連線點方法的執行。該異常的捕獲可以通過系統的異常處理器(可以看做控制器)來捕獲並跳轉到一個檢視或者一個請求。這樣就達到攔截請求的目的。所以需要配置異常處理器。

2.3 spring-mvc.xml(異常處理器配置,以及aop配置)

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <!-- <property name="defaultErrorView" value="rediret:/error"></property>   -->
        <property name="exceptionMappings">
            <props>
                <!--<prop key="com.jykj.demo.exception.AuthorizationException">redirect:/login</prop>-->
                <prop key="com.jykj.demo.exception.AccessDeniedException">forward:/accessDenied</prop>
            </props>
        </property>
    </bean>
<bean id="aspectPermission" class="com.jykj.demo.filter.PermissionAspect" />
    <!-- 對帶有@ValidatePermission和ResponseBody註解的controller包及其子包所有方法執行功能許可權校驗  --> 
    <aop:config proxy-target-class="true">  
        <aop:aspect ref="aspectPermission">  
            <aop:pointcut id="pc"  
                expression="@annotation(com.jykj.demo.annotation.ValidatePermission) 
                and @annotation(org.springframework.web.bind.annotation.ResponseBody) 
                and execution(* com.jykj.demo.controller..*.*(..)) " />  
            <aop:before pointcut-ref="pc" method="doBefore"/>  
        </aop:aspect>  
    </aop:config>

2.4 註解需要進行功能校驗的控制器請求

@RequestMapping(value = "/moduleAccess.do", method = RequestMethod.POST, produces="text/html;charset=utf-8")
    @ResponseBody
    @ValidatePermission
    public String moduleAccess(int fid,String action,FrmModule module) {
        System.out.println("fid:"+fid+",action:"+action);
        int rs = -1;
        try{
            if(Helper.F_ACTION_CREATE.equals(action)){
                rs = moduleService.access(module,Helper.DB_ACTION_INSERT);
                //module.setModuleid(rs);
                module = moduleService.selectByPrimaryKey(rs);
            }else if(Helper.F_ACTION_EDIT.equals(action)){
                rs = moduleService.access(module,Helper.DB_ACTION_UPDATE);
                module = moduleService.selectByPrimaryKey(module.getModuleid());
            }else if(Helper.F_ACTION_REMOVE.equals(action)){
                rs = moduleService.access(module,Helper.DB_ACTION_DELETE);
            }else{
                return JSON.toJSONString(new Result(false,"請求引數錯誤:action"));
            }
        }catch(Exception e){
            e.printStackTrace();
            return JSON.toJSONString(new Result(false,"操作失敗,出現異常,請聯絡管理員!"));
        }
        if(rs<0){
            return JSON.toJSONString(new Result(false,"操作失敗,請聯絡管理員!"));
        }
        return  JSON.toJSONString(new Result(true,module));
    }

2.5 異常處理器將請求轉發到的控制器請求 forward:/accessDenied

@RequestMapping(value = "/accessDenied",produces = "text/html;charset=UTF-8")
@ResponseBody
public String accessDenied(){
    return JSON.toJSONString(new Result(false,"您沒有許可權對此進行操作!"));
}

2.6 請求校驗不通過時 由上述的控制器返回 結果本身

如下所示:

{"info":"您沒有許可權對此進行操作!","success":false}

2.7 功能校驗service 示例

/**
     * 校驗當前使用者在某個模組的某個功能的許可權
     * @param functionId
     * @return 空字串表示 有許可權 ,否則是錯誤資訊
     * @throws Exception 
     */
    public String permissionValidate(int functionId){
        Object o =  request.getSession().getAttribute(Helper.SESSION_USER);
        //if(o==null)  throw new AuthorizationException(); 
        SysUser loginUser= (SysUser)o;
        if(loginUser.getUserid() == 1) return "";
        try{
            return mapper.permissionValidate(loginUser.getUserid(),functionId);
        }catch(Exception ex){
            ex.printStackTrace();
            return "資料庫操作出現異常!";
        }
    }

說明: 這裡僅僅是對帶有@ValidatePermission和@ResponseBody註解的controller包及其子包所有方法進行切入,這樣肯定是不夠通用的,應該是對帶有@ValidatePermission的方法進行切入,在切面類中通過判斷該方法是否有@ResponseBody註解來丟擲不一樣的異常,若帶有@ResponseBody註解則丟擲上述的異常返回json字串,
否則,應該丟擲另一個自定義異常然後將請求重定向到一個合法的檢視如error.jsp .

通過客戶端傳送 /moduleAccess.do 請求,該請求對應的方法同時具有@ValidatePermission和@ResponseBody,並且有功能Id引數fid,這樣AOP可以切入該方法,執行doBefore通知,通過功能引數fid,對它結合使用者id進行許可權校驗,若校驗通過直接返回,程式繼續執行,否則丟擲自定義異常AccessDeniedException,該異常由系統捕獲(需要配置異常處理器)併發出請求 forward:/accessDenied ,然後對應的控制器 /accessDenied 處理該請求返回一個包含校驗失敗資訊的json給客戶端。這樣傳送 /moduleAccess.do 請求,如果校驗失敗,轉發到了/accessDenied請求,否則正常執行。繞了這麼一個大圈子才實現它。