使用Spring AOP做接口權(quán)限校驗(yàn)和日志記錄
一、AOP 介紹
AOP: 翻譯為面向切面編程(Aspect Oriented Programming),它是一種編程思想,是面向?qū)ο缶幊蹋∣OP)的一種補(bǔ)充。它的目的是在不修改源代碼的情況下給程序動態(tài)添加額外功能。
1.1 AOP 應(yīng)用場景
AOP 的使用場景一般有:數(shù)據(jù)源切換、事務(wù)管理、權(quán)限控制、日志記錄等。根據(jù)它的名字我們不難理解,它的實(shí)現(xiàn)很像是將我們要實(shí)現(xiàn)的代碼切入業(yè)務(wù)邏輯中。
它有以下特點(diǎn):
- 侵入性小,幾乎可以不改動原先代碼的情況下把新的功能加入到業(yè)務(wù)中。
- 實(shí)現(xiàn)方便,使用幾個注解就可以實(shí)現(xiàn),使系統(tǒng)更容易擴(kuò)展。
- 更好的復(fù)用代碼,比如事務(wù)日志打印,簡單邏輯適合所有情況。
1.2 AOP 中的注解
- @Aspect:切面,表示一個橫切進(jìn)業(yè)務(wù)的一個對象,一般定義為切面類,它里面包含切入點(diǎn)和通知。
- @Pointcut:切入點(diǎn), 表示需要切入的位置。比如某些類或者某些方法,也就是先定一個范圍。
- @Before:前置通知,切入點(diǎn)的方法體執(zhí)行之前執(zhí)行。
- @Around:環(huán)繞通知,環(huán)繞切入點(diǎn)執(zhí)行也就是把切入點(diǎn)包裹起來執(zhí)行。
- @After:后置通知,在切入點(diǎn)正常運(yùn)行結(jié)束后執(zhí)行。
- @AfterReturning:后置通知,在切入點(diǎn)正常運(yùn)行結(jié)束后執(zhí)行,異常則不執(zhí)行。
- @AfterThrowing:后置通知,在切入點(diǎn)運(yùn)行異常時執(zhí)行。
二、權(quán)限校驗(yàn)
需求介紹:開發(fā)一個接口用于根據(jù)學(xué)生 id 獲取學(xué)生身份證號,接口上需要做權(quán)限校驗(yàn),只有系統(tǒng)管理員或者是機(jī)構(gòu)管理員組織類型的賬號才能執(zhí)行此接口,其他組織類別及普通成員執(zhí)行此接口,系統(tǒng)提示:沒有權(quán)限。
2.1 定義權(quán)限注解
/** * 權(quán)限要求 * 此注解用于標(biāo)注于接口方法上, 根據(jù)屬性group和role確定接口準(zhǔn)入要求的組織和角色, * 標(biāo)注此注解的方法會被切面{@link com.test.cloud.ig.vision.data.aspect.AuthorityAspect}攔截 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Auth { /** * 需要滿足的組織類型 * 默認(rèn)值:{@link GroupType#SYSTEM} * * @return */ GroupType[] group() default GroupType.SYSTEM; /** * 需要滿足的角色類型 * 默認(rèn)值:{@link RoleType#ADMIN} * * @return */ RoleType[] role() default RoleType.ADMIN; }
2.2 定義切面類
@Aspect @Order(1) @Component public class AuthorityAspect { @Autowired AuthorityService authorityService; @Pointcut(value = "@annotation(com.coe.cloud.ig.vision.data.annotation.Auth)") public void auth() { } @Before("auth()&&@annotation(auth)") public void before(Auth auth) { RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); // 從請求頭中獲取賬號id String accountId = request.getHeader("accountId"); // 校驗(yàn)權(quán)限 authorityService.checkGroupAuthority(Integer.valueOf(accountId), auth.group(), auth.role()); } }
2.3 權(quán)限驗(yàn)證服務(wù)
@Service public class AuthorityService { @Autowired AccountService accountService; /** * 判斷賬號是否有對應(yīng)的組織操作權(quán)限 * * @param accountId * @param groups 滿足條件的組織級別 * @param roles 滿足條件的角色 */ public void checkGroupAuthority(Integer accountId, GroupType[] groups, RoleType[] roles) { // 根據(jù)賬號ID獲取賬號信息 TAccount account = accountService.findById(accountId); // 判斷賬號是否能操作此組織級別 List<GroupType> controlGroups = GroupUtil.getControlGroups(GroupType.getByCode(account.getBizType())); controlGroups.retainAll(Arrays.asList(groups)); AssertUtil.isFalse(controlGroups.isEmpty(), ResultCodes.NO_AUTHORITY); // 判斷賬號是否滿足角色要求 RoleType role = RoleType.getByCode(account.getRole()); AssertUtil.isTrue(Arrays.asList(roles).contains(role), ResultCodes.NO_AUTHORITY); } }
2.4 織入切點(diǎn)
/** * 學(xué)生接口 * * @author: huangBX * @date: 2023/5/24 15:16 * @description: * @version: 1.0 */ @RestController @RequestMapping("/student") public class StudentController { @Autowired StudentService studentService; @Autowired AccountService accountService; @Autowired AuthorityService authorityService; /** * 通過學(xué)生Id查詢身份證號 * * @param accountId * @param studentId * @return */ @GetMapping ("/selectByStudentId") @Auth(group = {GroupType.SYSTEM, GroupType.ORGAN}, role = {RoleType.ADMIN}) public Result<String> idCard(@RequestHeader("accountId") Integer accountId, @RequestParam("studentId") Integer studentId) { TAccount account = accountService.findById(accountId); AssertUtil.isNotNull(account, ResultCodes.ACCOUNT_NOT_FOUND); //校驗(yàn)是否有該學(xué)校的數(shù)據(jù)權(quán)限 authorityService.checkDataAuthority(accountId, account.getBizId(), GroupType.ORGAN); TStudent student = studentService.findById(studentId); AssertUtil.isNotNull(student, ResultCodes.STUDENT_NOT_FOUND); return Result.success(student.getIdCard()); } }
2.5 測試
賬號信息表
role 角色字段若為 MEMBER 訪問接口則提示沒有權(quán)限。
將 MEMBER 改為 ADMIN,重新發(fā)送請求,能夠返回學(xué)生身份證號信息。
三、日志記錄
3.1 日志切面類
/** * Controller日志切面, 用于打印請求相關(guān)日志 * * @author: huangbx * @date: 2022/9/30 09:05 * @description: * @version: 1.0 */ @Aspect @Component public class ControllerLogAspect { private static final Logger LOGGER = LoggerFactory.getLogger(ControllerLogAspect.class); /** * 標(biāo)注有@RequestMapping、@PostMapping、@DeleteMapping、@PutMapping、@Override注解的方法 * 考慮到Feign繼承的情況, 可能實(shí)現(xiàn)類里未必會有以上注解, 所以對于標(biāo)有@Override注解的方法, 也納入范圍 */ @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping) " + "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" + "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" + "|| @annotation(org.springframework.web.bind.annotation.PutMapping)" + "|| @annotation(java.lang.Override)") public void requestMapping() { } /** * 標(biāo)注有@Controller或@RestController的類的所有方法 */ @Pointcut("@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController)") public void controller() { } @Around("controller()") public Object around(ProceedingJoinPoint point) throws Throwable { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 請求方法 String method = request.getMethod(); // 請求相對路徑 String requestURI = request.getRequestURI(); // 請求參數(shù) Map<String, String> parameterMap = getParameterMap(request); String parameterStr = buildParamStr(parameterMap); // 根據(jù)請求路徑和參數(shù)構(gòu)建請求連接 String requestURL = parameterStr != null && !parameterStr.isEmpty() ? requestURI + "?" + parameterStr : requestURI; // 請求體 Object[] body = point.getArgs(); if ("GET".equalsIgnoreCase(method)) { LOGGER.info("{} {}", method, requestURL); } else { LOGGER.info("{} {}, body:{}", method, requestURL, body); } // 請求處理開始時間 long startTime = System.currentTimeMillis(); Object result = point.proceed(); // 結(jié)束時間 long endTime = System.currentTimeMillis(); if ("GET".equalsIgnoreCase(method)) { LOGGER.info("{} {}, result:{}, cost:{}ms", method, requestURL, result, endTime - startTime); } else { LOGGER.info("{} {}, body:{}, result:{}, cost:{}ms", method, requestURL, body, result, endTime - startTime); } return result; } @AfterThrowing(pointcut = "controller()", throwing = "e") public void afterThrowing(JoinPoint point, Throwable e) { try { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 請求方法 String method = request.getMethod(); // 請求相對路徑 String requestURI = request.getRequestURI(); // 請求參數(shù) Map<String, String> parameterMap = getParameterMap(request); String parameterStr = buildParamStr(parameterMap); // 根據(jù)請求路徑和參數(shù)構(gòu)建請求連接 String requestURL = parameterStr != null && !parameterStr.isEmpty() ? requestURI + "?" + parameterStr : requestURI; // 請求體 Object[] body = point.getArgs(); if (e instanceof BusinessException) { BusinessException exception = (BusinessException) e; if ("GET".equalsIgnoreCase(method)) { LOGGER.warn("{} {}, code:{}, msg:{}", method, requestURL, exception.getExceptionCode(), exception.getMessage()); } else { LOGGER.warn("{} {}, body:{}, code:{}, msg:{}", method, requestURL, body, exception.getExceptionCode(), exception.getMessage()); } } else { if ("GET".equalsIgnoreCase(method)) { LOGGER.error("{} {}", method, requestURL, e); } else { LOGGER.error("{} {}, body:{}", method, requestURL, body, e); } } } catch (Exception ex) { LOGGER.error("執(zhí)行切面afterThrowing方法異常", ex); } } /** * 獲取HTTP請求中的參數(shù) * * @param request * @return 參數(shù)鍵值對 */ private Map<String, String> getParameterMap(HttpServletRequest request) { Map<String, String> parameterMap = new HashMap<>(); if (request != null && request.getParameterNames() != null) { Enumeration<String> parameterNames = request.getParameterNames(); while (parameterNames.hasMoreElements()) { String parameterName = parameterNames.nextElement(); String parameterValue = request.getParameter(parameterName); parameterMap.put(parameterName, parameterValue); } } return parameterMap; } /** * 根據(jù)請求參數(shù)map構(gòu)建請求參數(shù)字符串, 參數(shù)間采用&分隔 */ private String buildParamStr(Map<String, String> parameterMap) { if (parameterMap == null || parameterMap.isEmpty()) { return null; } StringBuilder paramBuilder = new StringBuilder(); parameterMap.forEach((key, value) -> paramBuilder.append(key).append("=").append(value).append("&")); return paramBuilder.substring(0, paramBuilder.length() - 1); } }
3.2 異常統(tǒng)一處理
/** * 默認(rèn)的Controller切面 * 主要對Controller異常進(jìn)行轉(zhuǎn)換, 轉(zhuǎn)換為相應(yīng)的Result進(jìn)行返回 * * @author: huangbx * @date: 2022/9/23 16:41 * @description: * @version: 1.0 */ @RestControllerAdvice public class DefaultControllerAdvice { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultControllerAdvice.class); /** * BusinessException異常的統(tǒng)一處理 * * @param e * @return */ @ExceptionHandler(BusinessException.class) @ResponseBody public Result handleBizException(BusinessException e) { return Result.fail(e.getExceptionCode(), e.getMessage()); } @ExceptionHandler(MissingServletRequestParameterException.class) @ResponseBody public Result handMissingServletRequestParameterException(MissingServletRequestParameterException e) { return Result.fail(ResultCodeEnum.PARAMETER_ERROR.getCode(), "參數(shù)" + e.getParameterName() + "不能為空"); } @ExceptionHandler(MissingRequestHeaderException.class) @ResponseBody public Result handlMissingRequestHeaderException(MissingRequestHeaderException e) { return Result.fail(ResultCodeEnum.PARAMETER_ERROR.getCode(), "請求頭" + e.getHeaderName() + "不能為空"); } @ExceptionHandler({MethodArgumentNotValidException.class}) public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { BindingResult bindingResult = e.getBindingResult(); // 判斷異常中是否有錯誤信息,如果存在就使用異常中的消息,否則使用默認(rèn)消息 if (bindingResult.hasErrors()) { List<ObjectError> allErrors = bindingResult.getAllErrors(); if (!allErrors.isEmpty()) { // 這里列出了全部錯誤參數(shù),按正常邏輯,只需要第一條錯誤即可 FieldError fieldError = (FieldError) allErrors.get(0); return Result.fail(ResultCodeEnum.PARAMETER_ERROR.getCode(), fieldError.getDefaultMessage()); } } return Result.fail(ResultCodeEnum.PARAMETER_ERROR); } @ExceptionHandler({BindException.class}) public Result handleBindException(BindException e) { BindingResult bindingResult = e.getBindingResult(); // 判斷異常中是否有錯誤信息,如果存在就使用異常中的消息,否則使用默認(rèn)消息 if (bindingResult.hasErrors()) { List<ObjectError> allErrors = bindingResult.getAllErrors(); if (!allErrors.isEmpty()) { // 這里列出了全部錯誤參數(shù),按正常邏輯,只需要第一條錯誤即可 FieldError fieldError = (FieldError) allErrors.get(0); return Result.fail(ResultCodeEnum.PARAMETER_ERROR.getCode(), fieldError.getDefaultMessage()); } } return Result.fail(ResultCodeEnum.PARAMETER_ERROR); } /** * Exception異常的統(tǒng)一處理 * * @param e * @return */ @ExceptionHandler(Exception.class) @ResponseBody public Result handleOtherException(Exception e) { LOGGER.error("unexpected exception", e); return Result.fail(ResultCodeEnum.SYSTEM_ERROR); } }
四、AOP 底層原理
Spring AOP(面向切面編程)的實(shí)現(xiàn)原理主要基于動態(tài)代理技術(shù),它提供了對業(yè)務(wù)邏輯各個方面的關(guān)注點(diǎn)分離和模塊化,使得非功能性需求(如日志記錄、事務(wù)管理、安全檢查、權(quán)限控制等)可以集中管理和維護(hù),而不是分散在各個業(yè)務(wù)模塊中。
Spring AOP 實(shí)現(xiàn)原理的關(guān)鍵要點(diǎn)如下:
- JDK 動態(tài)代理:對于實(shí)現(xiàn)了接口的目標(biāo)類,Spring AOP 默認(rèn)使用 JDK 的 java.lang.reflect.Proxy 類來創(chuàng)建代理對象。代理對象會在運(yùn)行時實(shí)現(xiàn)代理接口,并覆蓋其中的方法,在方法調(diào)用前后執(zhí)行切面邏輯(即通知 advice)。
- CGLIB 動態(tài)代理:對于未實(shí)現(xiàn)接口的類,Spring AOP 會選擇使用 CGLIB 庫來生成代理對象。CGLIB 通過字節(jié)碼技術(shù)創(chuàng)建目標(biāo)類的子類,在子類中重寫目標(biāo)方法并在方法調(diào)用前后插入切面邏輯。
到此這篇關(guān)于使用Spring AOP做接口權(quán)限校驗(yàn)和日志記錄的文章就介紹到這了,更多相關(guān)Spring AOP接口權(quán)限校驗(yàn)和日志記錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot使用AOP實(shí)現(xiàn)統(tǒng)一角色權(quán)限校驗(yàn)
- Spring AOP實(shí)現(xiàn)功能權(quán)限校驗(yàn)功能的示例代碼
- SpringBoot中使用AOP實(shí)現(xiàn)日志記錄功能
- SpringBoot使用AOP實(shí)現(xiàn)日志記錄功能詳解
- Spring AOP如何自定義注解實(shí)現(xiàn)審計或日志記錄(完整代碼)
- 在springboot中使用AOP進(jìn)行全局日志記錄
- Spring AOP實(shí)現(xiàn)復(fù)雜的日志記錄操作(自定義注解)
- SpringAop實(shí)現(xiàn)操作日志記錄
- springMVC自定義注解,用AOP來實(shí)現(xiàn)日志記錄的方法
相關(guān)文章
java中實(shí)現(xiàn)創(chuàng)建目錄與創(chuàng)建文件的操作實(shí)例
用Java創(chuàng)建文件或目錄非常簡單,下面這篇文章主要給大家介紹了關(guān)于java中實(shí)現(xiàn)創(chuàng)建目錄與創(chuàng)建文件的操作實(shí)例,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-01-01Mybatis-Plus中g(shù)etOne方法獲取最新一條數(shù)據(jù)的示例代碼
這篇文章主要介紹了Mybatis-Plus中g(shù)etOne方法獲取最新一條數(shù)據(jù),本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-05-05Spring Boot 集成 Kafkad的實(shí)現(xiàn)示例
這篇文章主要介紹了Spring Boot 集成 Kafkad的示例,幫助大家更好的理解和學(xué)習(xí)使用Spring Boot框架,感興趣的朋友可以了解下2021-04-04繼承jpa?Repository?寫自定義方法查詢實(shí)例
這篇文章主要介紹了繼承jpa?Repository?寫自定義方法查詢實(shí)例,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12