Spring security後臺使用自定義註解進行權限控制

最近在使用spring security進行編碼,在實際使用的過程中,遇到的問題記錄一下。

背景:在一個項目中,我使用spring security進行權限控制。不僅前臺控制頁面和按鈕的顯示,還在後臺對沒有權限的請求進行過濾。因爲每個需要進行權限控制的後臺請求,都需要寫相同的代碼,如果一個兩個還好,寫多了就開始想能不能減少代碼量。

先看看沒有使用自定義註解的時候是怎麼在後臺進行權限控制的

上面的例子是一個上傳文件後臺請求,我們需要判斷這個用戶是否有上傳文件的權限。在這裏我定義了一個工具類AuthenticationUtil,用來判斷用戶有沒有登錄(authen),和判斷用戶是否具有某個權限(authenRole),並把判斷結果放到Map中。如果沒有權限,就不再執行後面的語句。假如我每個請求都需要判斷權限,那麼每個請求的開頭都需要加這樣5行代碼,這樣代碼重用性就低。而且controller裏面不能專注關心業務,還混入了權限的代碼。

爲了解決這個問題,使用自定義註解,插入一個AOP,可以很好的解決這個問題。

看一下使用AOP之後的代碼

一行代碼就完成之前5行代碼做的事情,是不是很簡潔,controller中也不用再關心權限的問題了。

這裏我使用Springboot中的Aop技術,定義一個切面,使用around進行注入。

1.自定義註解AuthenticRequire

package demo.config.aop;

import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthenticRequire {
    //權限的名稱
    String value() default "";

    //沒有權限的時候,通過這個接口得到返回值
    Class<? extends IReturnHandle> handle() default DefaultReturnHandle.class;
}

value是權限的名稱,而另一個變量handle,是用來進行類型轉換的,因爲有時候url請求的返回值不是Map,而是其他類型,這時候就需要自己實現一個類型轉換類來完成返回值的轉換。

2.定義類型轉換接口IReturnHandle

package demo.config.aop;

public interface IReturnHandle<T,K> {
    //完成類型轉換,從類型T轉成類型K,和java8的內置函數apply一樣的作用
    K handle(T t);
}

3.定義一個默認的類型轉換實現類

package demo.config.aop;

import java.util.Map;

public class DefaultReturnHandle implements IReturnHandle<Map<String, Object>, Map<String, Object>> {

    @Override
    public Map<String, Object> handle(Map<String, Object> stringStringMap) {
        return stringStringMap;
    }
}

4.針對AuthenticRequire註解,定義一個AOP切面,完成權限控制

package demo.config.aop;

import demo.util.AuthenticationUtil;
import lombok.extern.log4j.Log4j2;
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 org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Aspect
@Component
@Log4j2
public class AuthenticAop {
    @Pointcut("@annotation(demo.config.aop.AuthenticRequire)")
    public void operationLog(){}

    @Around("operationLog()")
    public Object around(ProceedingJoinPoint pjp){
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        AuthenticRequire authenticRequire = method.getAnnotation(AuthenticRequire.class);

        Map<String, Object> result = new HashMap<>();
        boolean authen = false;
        Object[] args = pjp.getArgs();
        if(args!=null){
            //從方法的參數中,過濾出HttpServletRequest參數
            Optional<Object> objectOptional = Arrays.stream(args).filter(arg->arg instanceof HttpServletRequest).findFirst();
            if(objectOptional.isPresent()) {
                HttpServletRequest request = (HttpServletRequest) objectOptional.get();
                //判斷用戶有沒有登錄,如果有登錄,再判斷有沒有權限
                authen = AuthenticationUtil.authen((AbstractAuthenticationToken) request.getUserPrincipal(), result);
                if(authen) {
                    String roleName = authenticRequire.value();
                    authen = AuthenticationUtil.authenRole((AbstractAuthenticationToken) request.getUserPrincipal(), roleName, result);
                }
            }
        }
        //如果沒有權限,直接返回
        if(!authen){
            if(authenticRequire.handle()!=null){
                try {
                    IReturnHandle<Map<String, Object>,?> handle = authenticRequire.handle().newInstance();
                    return handle.handle(result);
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            }
            return result;
        }

        //有相應的權限,執行方法,並返回結果
        Object methodReturn = null;
        try {
            methodReturn = pjp.proceed();
        } catch (Throwable throwable) {
            log.error(throwable.getMessage(), throwable);
        }
        return methodReturn;
    }
}

5.最後直接使用就可以了,在有需要的地方注入註解,完成權限控制。假如用戶沒有對應權限,則請求不會進入到controller中。

如果方法返回值是Map<String,Object>,那就這樣就行,如果不是,則需要多傳遞一個變量,如下圖

因爲下載文件的請求的返回值是ResponseEntity,所以需要自己實現一個IReturnHandle實現類進行類型轉換。

注意:註解的方法需要有一個參數HttpServletRequest參數,用來獲取security權限信息。如果沒有這個參數,則認爲沒有權限。

最後再付上權限判斷的工具類

package demo.util;

import org.springframework.security.authentication.AbstractAuthenticationToken;

import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
import java.util.Map;

/**
 * 權限認證操作
 */
public class AuthenticationUtil {

    /**
     * 判斷用戶是否登錄
     * @param principal
     * @param result
     * @return
     */
    public static boolean authen(AbstractAuthenticationToken principal, Map<String, Object> result){
        if(principal==null) {
            result.put("error", "用戶未登陸");
            return false;
        }
        return true;
    }

    /**
     * 判斷用戶是否具備某個權限
     * @param principal
     * @param roleName
     * @param result
     * @return
     */
    public static boolean authenRole(AbstractAuthenticationToken principal, String roleName, Map<String, Object> result){
        if(roleName==null || roleName.isEmpty())
            return true;
        boolean isRole = principal.getAuthorities().stream()
                .map(o->o.getAuthority()).filter(o->o.equalsIgnoreCase(roleName)).count()>0;
        if(!isRole){
            if(result!=null)
                result.put("error", "當前用戶沒有操作權限");
            return false;
        }
        return true;
    }
}

因爲項目涉及其他代碼,就不上傳了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章