java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring AOP接口权限校验和日志记录

使用Spring AOP做接口权限校验和日志记录

作者:南波塞文

本文介绍了面向切面编程(AOP)的基本概念、应用场景及其在Spring中的实现原理,通过AOP,可以方便地在不修改原有代码的情况下,实现日志记录、权限校验等功能,以学生身份证号查询接口为例,展示了如何定义权限注解、切面类以及权限验证服务,感兴趣的朋友一起看看吧

一、AOP 介绍

AOP: 翻译为面向切面编程(Aspect Oriented Programming),它是一种编程思想,是面向对象编程(OOP)的一种补充。它的目的是在不修改源代码的情况下给程序动态添加额外功能。

1.1 AOP 应用场景

AOP 的使用场景一般有:数据源切换、事务管理、权限控制、日志记录等。根据它的名字我们不难理解,它的实现很像是将我们要实现的代码切入业务逻辑中。

它有以下特点:

1.2 AOP 中的注解

二、权限校验

需求介绍:开发一个接口用于根据学生 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 实现原理的关键要点如下:

到此这篇关于使用Spring AOP做接口权限校验和日志记录的文章就介绍到这了,更多相关Spring AOP接口权限校验和日志记录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文