使用Spring AOP做接口权限校验和日志记录
一、AOP 介绍
AOP: 翻译为面向切面编程(Aspect Oriented Programming),它是一种编程思想,是面向对象编程(OOP)的一种补充。它的目的是在不修改源代码的情况下给程序动态添加额外功能。
1.1 AOP 应用场景
AOP 的使用场景一般有:数据源切换、事务管理、权限控制、日志记录等。根据它的名字我们不难理解,它的实现很像是将我们要实现的代码切入业务逻辑中。
它有以下特点:
- 侵入性小,几乎可以不改动原先代码的情况下把新的功能加入到业务中。
- 实现方便,使用几个注解就可以实现,使系统更容易扩展。
- 更好的复用代码,比如事务日志打印,简单逻辑适合所有情况。
1.2 AOP 中的注解
- @Aspect:切面,表示一个横切进业务的一个对象,一般定义为切面类,它里面包含切入点和通知。
- @Pointcut:切入点, 表示需要切入的位置。比如某些类或者某些方法,也就是先定一个范围。
- @Before:前置通知,切入点的方法体执行之前执行。
- @Around:环绕通知,环绕切入点执行也就是把切入点包裹起来执行。
- @After:后置通知,在切入点正常运行结束后执行。
- @AfterReturning:后置通知,在切入点正常运行结束后执行,异常则不执行。
- @AfterThrowing:后置通知,在切入点运行异常时执行。
二、权限校验
需求介绍:开发一个接口用于根据学生 id 获取学生身份证号,接口上需要做权限校验,只有系统管理员或者是机构管理员组织类型的账号才能执行此接口,其他组织类别及普通成员执行此接口,系统提示:没有权限。
2.1 定义权限注解
/** * 权限要求 * 此注解用于标注于接口方法上, 根据属性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) { // 根据账号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); //校验是否有该学校的数据权限 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,重新发送请求,能够返回学生身份证号信息。
三、日志记录
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继承的情况, 可能实现类里未必会有以上注解, 所以对于标有@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(); // 请求参数 Map<String, String> parameterMap = getParameterMap(request); String parameterStr = buildParamStr(parameterMap); // 根据请求路径和参数构建请求连接 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(); // 请求参数 Map<String, String> parameterMap = getParameterMap(request); String parameterStr = buildParamStr(parameterMap); // 根据请求路径和参数构建请求连接 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("执行切面afterThrowing方法异常", ex); } } /** * 获取HTTP请求中的参数 * * @param request * @return 参数键值对 */ 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; } /** * 根据请求参数map构建请求参数字符串, 参数间采用&分隔 */ 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 异常统一处理
/** * 默认的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异常的统一处理 * * @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(), "参数" + 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()) { // 这里列出了全部错误参数,按正常逻辑,只需要第一条错误即可 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()) { // 这里列出了全部错误参数,按正常逻辑,只需要第一条错误即可 FieldError fieldError = (FieldError) allErrors.get(0); return Result.fail(ResultCodeEnum.PARAMETER_ERROR.getCode(), fieldError.getDefaultMessage()); } } return Result.fail(ResultCodeEnum.PARAMETER_ERROR); } /** * Exception异常的统一处理 * * @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(面向切面编程)的实现原理主要基于动态代理技术,它提供了对业务逻辑各个方面的关注点分离和模块化,使得非功能性需求(如日志记录、事务管理、安全检查、权限控制等)可以集中管理和维护,而不是分散在各个业务模块中。
Spring AOP 实现原理的关键要点如下:
- JDK 动态代理:对于实现了接口的目标类,Spring AOP 默认使用 JDK 的 java.lang.reflect.Proxy 类来创建代理对象。代理对象会在运行时实现代理接口,并覆盖其中的方法,在方法调用前后执行切面逻辑(即通知 advice)。
- CGLIB 动态代理:对于未实现接口的类,Spring AOP 会选择使用 CGLIB 库来生成代理对象。CGLIB 通过字节码技术创建目标类的子类,在子类中重写目标方法并在方法调用前后插入切面逻辑。
到此这篇关于使用Spring AOP做接口权限校验和日志记录的文章就介绍到这了,更多相关Spring AOP接口权限校验和日志记录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
IDEA中已配置阿里镜像但maven无法下载jar包的问题及解决方法
这篇文章主要介绍了IDEA中已配置阿里镜像但maven无法下载jar包的问题,本文给大家分享解决方法,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2020-08-08简单了解Spring beanfactory循环依赖命名重复属性
这篇文章主要介绍了简单了解Spring beanfactory循环依赖命名重复2大属性,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下2020-06-06SpringBoot AOP导致service注入后是null的问题
本文主要讲述了如何利用SpringAOP实现用户操作日志的记录,首先,引入SpringBoot的AOP依赖,然后,选择基于注解的形式来实现日志操作,以避免污染原有代码和逻辑,在理解了SpringBootAOP的一些注解后,需要记录用户的正常请求以及异常请求的信息2024-10-10spring security中Authority、Role的区别及说明
这篇文章主要介绍了spring security中Authority、Role的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2023-09-09
最新评论