使用Spring AOP做接口權限校驗和日志記錄
一、AOP 介紹
AOP: 翻譯為面向切面編程(Aspect Oriented Programming),它是一種編程思想,是面向對象編程(OOP)的一種補充。它的目的是在不修改源代碼的情況下給程序動態(tài)添加額外功能。
1.1 AOP 應用場景
AOP 的使用場景一般有:數(shù)據(jù)源切換、事務管理、權限控制、日志記錄等。根據(jù)它的名字我們不難理解,它的實現(xiàn)很像是將我們要實現(xiàn)的代碼切入業(yè)務邏輯中。
它有以下特點:
- 侵入性小,幾乎可以不改動原先代碼的情況下把新的功能加入到業(yè)務中。
- 實現(xiàn)方便,使用幾個注解就可以實現(xiàn),使系統(tǒng)更容易擴展。
- 更好的復用代碼,比如事務日志打印,簡單邏輯適合所有情況。
1.2 AOP 中的注解
- @Aspect:切面,表示一個橫切進業(yè)務的一個對象,一般定義為切面類,它里面包含切入點和通知。
- @Pointcut:切入點, 表示需要切入的位置。比如某些類或者某些方法,也就是先定一個范圍。
- @Before:前置通知,切入點的方法體執(zhí)行之前執(zhí)行。
- @Around:環(huán)繞通知,環(huán)繞切入點執(zhí)行也就是把切入點包裹起來執(zhí)行。
- @After:后置通知,在切入點正常運行結束后執(zhí)行。
- @AfterReturning:后置通知,在切入點正常運行結束后執(zhí)行,異常則不執(zhí)行。
- @AfterThrowing:后置通知,在切入點運行異常時執(zhí)行。
二、權限校驗
需求介紹:開發(fā)一個接口用于根據(jù)學生 id 獲取學生身份證號,接口上需要做權限校驗,只有系統(tǒng)管理員或者是機構管理員組織類型的賬號才能執(zhí)行此接口,其他組織類別及普通成員執(zhí)行此接口,系統(tǒng)提示:沒有權限。
2.1 定義權限注解
/**
* 權限要求
* 此注解用于標注于接口方法上, 根據(jù)屬性group和role確定接口準入要求的組織和角色,
* 標注此注解的方法會被切面{@link com.test.cloud.ig.vision.data.aspect.AuthorityAspect}攔截
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Auth {
/**
* 需要滿足的組織類型
* 默認值:{@link GroupType#SYSTEM}
*
* @return
*/
GroupType[] group() default GroupType.SYSTEM;
/**
* 需要滿足的角色類型
* 默認值:{@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");
// 校驗權限
authorityService.checkGroupAuthority(Integer.valueOf(accountId), auth.group(), auth.role());
}
}2.3 權限驗證服務
@Service
public class AuthorityService {
@Autowired
AccountService accountService;
/**
* 判斷賬號是否有對應的組織操作權限
*
* @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 織入切點
/**
* 學生接口
*
* @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;
/**
* 通過學生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);
//校驗是否有該學校的數(shù)據(jù)權限
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 訪問接口則提示沒有權限。

將 MEMBER 改為 ADMIN,重新發(fā)送請求,能夠返回學生身份證號信息。

三、日志記錄
3.1 日志切面類
/**
* Controller日志切面, 用于打印請求相關日志
*
* @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);
/**
* 標注有@RequestMapping、@PostMapping、@DeleteMapping、@PutMapping、@Override注解的方法
* 考慮到Feign繼承的情況, 可能實現(xiàn)類里未必會有以上注解, 所以對于標有@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() {
}
/**
* 標注有@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ù)構建請求連接
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();
// 結束時間
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ù)構建請求連接
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構建請求參數(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)一處理
/**
* 默認的Controller切面
* 主要對Controller異常進行轉換, 轉換為相應的Result進行返回
*
* @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();
// 判斷異常中是否有錯誤信息,如果存在就使用異常中的消息,否則使用默認消息
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();
// 判斷異常中是否有錯誤信息,如果存在就使用異常中的消息,否則使用默認消息
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(面向切面編程)的實現(xiàn)原理主要基于動態(tài)代理技術,它提供了對業(yè)務邏輯各個方面的關注點分離和模塊化,使得非功能性需求(如日志記錄、事務管理、安全檢查、權限控制等)可以集中管理和維護,而不是分散在各個業(yè)務模塊中。
Spring AOP 實現(xiàn)原理的關鍵要點如下:
- JDK 動態(tài)代理:對于實現(xiàn)了接口的目標類,Spring AOP 默認使用 JDK 的 java.lang.reflect.Proxy 類來創(chuàng)建代理對象。代理對象會在運行時實現(xiàn)代理接口,并覆蓋其中的方法,在方法調用前后執(zhí)行切面邏輯(即通知 advice)。
- CGLIB 動態(tài)代理:對于未實現(xiàn)接口的類,Spring AOP 會選擇使用 CGLIB 庫來生成代理對象。CGLIB 通過字節(jié)碼技術創(chuàng)建目標類的子類,在子類中重寫目標方法并在方法調用前后插入切面邏輯。
到此這篇關于使用Spring AOP做接口權限校驗和日志記錄的文章就介紹到這了,更多相關Spring AOP接口權限校驗和日志記錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
java中實現(xiàn)創(chuàng)建目錄與創(chuàng)建文件的操作實例
用Java創(chuàng)建文件或目錄非常簡單,下面這篇文章主要給大家介紹了關于java中實現(xiàn)創(chuàng)建目錄與創(chuàng)建文件的操作實例,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2023-01-01
Mybatis-Plus中getOne方法獲取最新一條數(shù)據(jù)的示例代碼
這篇文章主要介紹了Mybatis-Plus中getOne方法獲取最新一條數(shù)據(jù),本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-05-05
Spring Boot 集成 Kafkad的實現(xiàn)示例
這篇文章主要介紹了Spring Boot 集成 Kafkad的示例,幫助大家更好的理解和學習使用Spring Boot框架,感興趣的朋友可以了解下2021-04-04

