設計RESTFUL API的接口權限鑑別問題,能處理的方式有不少種,你能夠直接在controller中鑑權,在調用service以及其它代碼,也可使用interceptor配置哪部分用戶能訪問哪部分接口,也可使用shiro以及Spring Security等框架來實現權限,最後還能夠採用AOP的方式來實現。前端
如下描述可能不許,但通俗易懂java
AOP全稱 Aspect Oriented Program,意思是面向切面變成。我來小小地解釋一下: web
咱們把一個對象考慮成一個棒棒饃,把饃饃切一刀,那麼被切的這個地方咱們叫切入點(饃饃上每個能夠被切的地方都叫作鏈接點),這個刀面就叫作切面,切面嵌入了饃饃裏面。饃饃裏面有隻蛆要從饃饃的一端到另外一端,就要通過刀面,蛆在到達刀面時,想辦法爬上刀,在經過刀(咬個洞鑽過去)。redis
對象中的代碼執行分前後,對象執行過程當中裏面有方法執行,某一句代碼執行等,他們都分順序執行。這個時候若是有個方法要執行了,咱們在這兒切一刀,那這塊對象就被切爛了。 spring
可能描述的有問題,簡而言之,咱們在要插入其它代碼的地方用註解標記一下,那麼就能夠經過AOP的一系列機制在此處插入一段代碼了。詳細看看代碼就明白了。數據庫
這一次項目接口爆發面積廣,影響做用大,若是在每一個controller中寫大量相似的鑑權代碼將會付出很大的代價,一樣採用BaseController的方式把這部分代碼抽出以下降複雜度仍然是一件比較麻煩的事情,所以該方案直接跳過。api
這是以前作一個小練手項目時使用SpringBoot配置的攔截器: 緩存
interceptor寫起來不算複雜,可是接口過多的時候,要添加好多沒有權限的內容放行,這個配置類就會顯得很是複雜。相對於AOP來講,AOP實現更加方便快捷。因此當跟AOP對比時我選擇了AOP。restful
框架通常爲重型應用或者較爲複雜的認證和權限場景提供解決方案,個人系統中只有管理員,前端,匿名三類角色,使用框架費時費力,相對比於框架,本身實現一個權限控制流程就更加簡單了。session
綜合以上,AOP實現權限是一種更加便捷的方案。
前端請求後臺時,須要判斷是管理員仍是普通用戶仍是匿名用戶,後臺是restful api,controller自己不做權限驗證。在每個controller方法前面添加一個註解,經過AOP來判斷用戶header中帶過來的token字段是否存在,以及token在redis的session緩存中userId究竟是管理員仍是普通用戶,來鑑別權利。
package xyz.ruankun.machinemother.util.constant; public enum AuthAopConstant { /** * 管理員 普通用戶 匿名者 */ ADMIN, USER, ANON }
其中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; } } } } }
攔截了一下登陸方法,用來測試的,起做用了。注意一下,個人邏輯中是把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; }