SpringBoot2 - AOP - 實現鑑權 [管理員 | 前端 | 匿名用戶]

基於SpringBoot2 - AOP - 實現鑑權 [管理員 | 前端 | 匿名用戶]

設計RESTFUL API的接口權限鑑別問題,能處理的方式有不少種,你能夠直接在controller中鑑權,在調用service以及其它代碼,也可使用interceptor配置哪部分用戶能訪問哪部分接口,也可使用shiro以及Spring Security等框架來實現權限,最後還能夠採用AOP的方式來實現。前端

如下描述可能不許,但通俗易懂java

  • AOP全稱 Aspect Oriented Program,意思是面向切面變成。我來小小地解釋一下: web

    咱們把一個對象考慮成一個棒棒饃,把饃饃切一刀,那麼被切的這個地方咱們叫切入點(饃饃上每個能夠被切的地方都叫作鏈接點),這個刀面就叫作切面,切面嵌入了饃饃裏面。饃饃裏面有隻蛆要從饃饃的一端到另外一端,就要通過刀面,蛆在到達刀面時,想辦法爬上刀,在經過刀(咬個洞鑽過去)。redis

    對象中的代碼執行分前後,對象執行過程當中裏面有方法執行,某一句代碼執行等,他們都分順序執行。這個時候若是有個方法要執行了,咱們在這兒切一刀,那這塊對象就被切爛了。 spring

    可能描述的有問題,簡而言之,咱們在要插入其它代碼的地方用註解標記一下,那麼就能夠經過AOP的一系列機制在此處插入一段代碼了。詳細看看代碼就明白了。數據庫

那麼問題來了!爲何要選擇AOP呢??


爲何不選擇直接在controller中鑑權?

這一次項目接口爆發面積廣,影響做用大,若是在每一個controller中寫大量相似的鑑權代碼將會付出很大的代價,一樣採用BaseController的方式把這部分代碼抽出以下降複雜度仍然是一件比較麻煩的事情,所以該方案直接跳過。api

爲何不選擇interceptor呢?

這是以前作一個小練手項目時使用SpringBoot配置的攔截器: 緩存

interceptor寫起來不算複雜,可是接口過多的時候,要添加好多沒有權限的內容放行,這個配置類就會顯得很是複雜。相對於AOP來講,AOP實現更加方便快捷。因此當跟AOP對比時我選擇了AOP。restful

爲何不選擇shiro和security?

框架通常爲重型應用或者較爲複雜的認證和權限場景提供解決方案,個人系統中只有管理員,前端,匿名三類角色,使用框架費時費力,相對比於框架,本身實現一個權限控制流程就更加簡單了。session


綜合以上,AOP實現權限是一種更加便捷的方案。


如下就是使用AOP來實現權限的具體內容

功能介紹

前端請求後臺時,須要判斷是管理員仍是普通用戶仍是匿名用戶,後臺是restful api,controller自己不做權限驗證。在每個controller方法前面添加一個註解,經過AOP來判斷用戶header中帶過來的token字段是否存在,以及token在redis的session緩存中userId究竟是管理員仍是普通用戶,來鑑別權利。

  • 鑑權經過,則執行controller對應的方法並返回
  • 鑑權失敗,返回permission denied錯誤

東西介紹

  • 首先利用註解在controller上註解一下,表示我將在此處插入一段權限鑑別代碼,這段代碼判斷若是請求者沒有權限,就不會執行被標記的這個方法直接返回給用戶權限錯誤了。如圖:

  • 而後你得知道,這個註解是怎麼實現的,裏面有兩個參數,一個表示是否開啓鑑權,一個表示要執行controller中的方法你須要什麼權限。

  • 固然註解自己只能起標記的做用,不能有業務實現。業務實現須要由切面類來實現:

代碼浮現:

權限枚舉類

package xyz.ruankun.machinemother.util.constant;
public enum  AuthAopConstant {
    /**
     * 管理員 普通用戶 匿名者
     */
    ADMIN,
    USER,
    ANON
}

用於在controller上標記的註解

其中pass字段說明該註解是否生效,role字段表名要執行被切的那個controller中的方法須要什麼權限。

package xyz.ruankun.machinemother.annotation;
import xyz.ruankun.machinemother.util.constant.AuthAopConstant;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Authentication {
    /**
     *  true爲啓用驗證
     *  false爲跳過驗證
     * @return
     */
    boolean pass() default true;

    AuthAopConstant role() default AuthAopConstant.ANON;
}

切面類

該類就是一個切面,裏面定義了一個切入點,就是插入的這個面該從哪裏開始執行,以及執行的邏輯等。權限代碼看@Around標記的那個方法就好了

package xyz.ruankun.machinemother.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import xyz.ruankun.machinemother.annotation.Authentication;
import xyz.ruankun.machinemother.repository.AdminRepository;
import xyz.ruankun.machinemother.repository.UserRepository;
import xyz.ruankun.machinemother.service.AdminService;
import xyz.ruankun.machinemother.service.UserInfoService;
import xyz.ruankun.machinemother.util.Constant;
import xyz.ruankun.machinemother.util.constant.AuthAopConstant;
import xyz.ruankun.machinemother.vo.ResponseEntity;

import javax.servlet.http.HttpServletRequest;

/**
 * 使用aop完成API請求時的認證和權限
 * made by Jason. Completed by mrruan
 */
@Aspect
@Component
public class AuthenticationAspect {

    public final static Logger logger = LoggerFactory.getLogger(AuthenticationAspect.class);

    @Autowired
    private UserInfoService userInfoService;

    @Autowired
    private AdminService adminService;

    //去數據庫查詢權限
    @Autowired
    AdminRepository adminRepository;

    @Autowired
    UserRepository userRepository;

    @Pointcut(value = "@annotation(xyz.ruankun.machinemother.annotation.Authentication)")
    public void pointcut() {}

    /**
     * 與被註釋方法正確返回以後執行
     * @param joinPoint 方法執行前的參數
     * @param result 方法返回值 後續觀察,是否保存
     */
    @AfterReturning(returning = "result", value = "@annotation(xyz.ruankun.machinemother.annotation.Authentication)")
    public void after(JoinPoint joinPoint, Object result) {
        logger.info("refreshing token");
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof HttpServletRequest) {
                HttpServletRequest request = (HttpServletRequest) arg;
                String token = request.getParameter("token");
                if (token != null) {
                    //經過token獲取id值更新token有效期
                    int userId = Integer.valueOf(userInfoService.readDataFromRedis(token));
                    String sessionKey = userInfoService.readDataFromRedis("session_key" + userId);
                    if (null == sessionKey){
                        //管理員是沒有sessionkey的喲
                        adminService.updateSession(String.valueOf(userId),token,15);
                    }else
                        userInfoService.updateSession(userId,sessionKey, token,15);
                    logger.info("refreshed token");
                }else{
                    logger.info("not refreshed token");
                }
            }
        }

    }
    @Around("pointcut() && @annotation(authentication)")
    public  Object interceptor(ProceedingJoinPoint proceedingJoinPoint, Authentication authentication){

        boolean pass = authentication.pass();
        //要驗證權限
        AuthAopConstant role = authentication.role();
        if(pass && role != AuthAopConstant.ANON){
            //經過拿到的role,咱們能夠知道能處理這個請求的角色是什麼
            //若是是匿名者,直接放行,若是是用戶,就須要用戶的權限才行,管理員則須要管理員的角色才行
            //規定一致,token放在header中
            HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder
                    .getRequestAttributes()).getRequest();
            String token = request.getHeader("token");
            AuthAopConstant realRole = authenticate(token);
            if (realRole == role) {
                //權限正確,去訪問吧
                try {
                    return proceedingJoinPoint.proceed();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                    ResponseEntity responseEntity = new ResponseEntity();
                    responseEntity.success(Constant.AOP_SERVER_ERROR, "", null);
                    return responseEntity;
                }
            }else{
                //權限錯誤,返回錯誤
                ResponseEntity responseEntity = new ResponseEntity();
                responseEntity.success(Constant.AUTH_ERROR, "permission denied,forbidden access", null);
                return responseEntity;
            }
        }else{
            //不驗證權限
            try {
                return proceedingJoinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                ResponseEntity responseEntity = new ResponseEntity();
                responseEntity.success(Constant.AOP_SERVER_ERROR, "", null);
                return responseEntity;
            }
        }
    }

    /**
     * 這個方法用於判斷該token所屬的究竟是誰(管理員? 用戶? 匿名?)
     * @param token
     * @return
     */
    private AuthAopConstant authenticate(String token){
        String userId = null;
        try {
            userId = userInfoService.readDataFromRedis(token);
        } catch (Exception e) {
            e.printStackTrace();
            //讀取userId錯誤(最大的多是請求的header中沒有token),直接返回匿名錯誤
            return  AuthAopConstant.ANON;
        }
        if(userId == null){
            //匿名的或者說用戶過時的,沒有找到session
            return AuthAopConstant.ANON;
        }else{
            Integer id;
            try {
                id = Integer.parseInt(userId);
                logger.info("userId:" + id);
            } catch (Exception e) {
                e.printStackTrace();
                //都拋出了異常了,這個userId是假的,直接匿名者
                return AuthAopConstant.ANON;
            }
            if(adminRepository.findById(id).isPresent()){
                //是管理員
                return AuthAopConstant.ADMIN;
            }else{
                if (userRepository.findById(id).isPresent()){
                    //是用戶
                    return AuthAopConstant.USER;
                }else{
                    //沒有發現它是用戶,假的
                   return AuthAopConstant.ANON;
                }
            }
        }
    }
}

controller長什麼樣

攔截了一下登陸方法,用來測試的,起做用了。注意一下,個人邏輯中是把token放在header中了的。

/**
     * 只有管理員能幹
     * @param img 圖片文件
     * @return 返回上傳成功
     */
    @PutMapping("/template")
    @Authentication(role = AuthAopConstant.ADMIN)
    public ResponseEntity putOneTemplate(@RequestParam MultipartFile img){
        ResponseEntity responseEntity = new ResponseEntity();
        boolean rs = qrCodeService.putTemplate(img);
        if (rs)
            responseEntity.success(null);
        else
            responseEntity.serverError();
        return  responseEntity;
    }
相關文章
相關標籤/搜索