后台管理系统的通用权限解决方案(十五)基于注解和切面实现操作日志记录
基于注解和切面实现操作日志记录
- 1)创建一个新的模块
opt-log
,其结构如下
- 2)创建注解
@OptLog
,当请求标注了该注解的Controller方法时,则表示需要记录操作日志
package com.itweid.opt.annotation;/*** 操作日志注解* @author: itweid* @since: 2024-11-13 13:05:05*/
import java.lang.annotation.*;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OptLog {/*** 描述*/String value();/*** 记录执行参数*/boolean recordRequestParam() default true;/*** 记录返回参数*/boolean recordResponseParam() default true;}
- 3)定义操作日志事件类
OptLogEvent
,用于封装操作日志AuthOptLog
package com.itweid.opt.event;import com.itweid.common.entity.AuthOptLog;
import org.springframework.context.ApplicationEvent;/*** 定义操作日志事件*/
public class OptLogEvent extends ApplicationEvent {public OptLogEvent(AuthOptLog authOptLog) {super(authOptLog);}
}
- 4)创建切面类
OptLogAspect
,配置切入点拦截规则,拦截使用@OptLog
注解标注的方法
package com.itweid.opt.aspect;import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONUtil;
import com.itweid.common.entity.AuthOptLog;
import com.itweid.common.exception.BaseException;
import com.itweid.common.pojo.BaseResult;
import com.itweid.jwt.pojo.JwtUserInfo;
import com.itweid.jwt.utils.AuthTokenUtils;
import com.itweid.opt.annotation.OptLog;
import com.itweid.opt.event.OptLogEvent;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Objects;/*** @author: itweid* @since: 2024-10-25 09:23:57*/
@Slf4j
@Aspect
public class OptLogAspect {@Autowiredprivate ApplicationContext applicationContext;@Autowiredprivate AuthTokenUtils authTokenUtils;/*** 用于保存线程中的操作日志对象*/private static final ThreadLocal<AuthOptLog> THREAD_LOCAL = new ThreadLocal<>();/*** 定义Controller切入点拦截规则,拦截 @OptLog 注解的方法*/@Pointcut("@annotation(com.itweid.opt.annotation.OptLog)")public void optLogAspect() {}/*** 从ThreadLocal中获取操作日志对象,没有则创建一个*/private AuthOptLog getAuthOptLog() {AuthOptLog authOptLog = THREAD_LOCAL.get();if (authOptLog == null) {return new AuthOptLog();}return authOptLog;}/*** 前置通知,收集操作相关信息封装为Audit对象并保存到ThreadLocal中*/@Before(value = "optLogAspect()")public void doBefore(JoinPoint joinPoint) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();LocalDateTime now = LocalDateTime.now();AuthOptLog authOptLog = getAuthOptLog();// 开始时间authOptLog.setStartTime(now);// 创建时间authOptLog.setCreateTime(now);// 请求IPauthOptLog.setRequestIp(ServletUtil.getClientIP(request));// 浏览器authOptLog.setUa(StrUtil.sub(request.getHeader("user-agent"), 0, 500));// 类名authOptLog.setClassPath(joinPoint.getTarget().getClass().getName());// 方法名authOptLog.setActionMethod(joinPoint.getSignature().getName());// 请求地址authOptLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));// 请求类型authOptLog.setHttpMethod(request.getMethod());// 请求参数Object[] args = joinPoint.getArgs();String strArgs = "";try {if (!request.getContentType().contains("multipart/form-data")) {strArgs = JSONUtil.toJsonStr(args);}} catch (Exception e) {try {strArgs = Arrays.toString(args);} catch (Exception ex) {log.warn("解析参数异常", ex);}}authOptLog.setParams(StrUtil.sub(strArgs, 0, 65535));// 创建时间authOptLog.setCreateTime(now);// 获取 @OptLog 注解的信息MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();Method method = methodSignature.getMethod();OptLog ann = method.getAnnotation(OptLog.class);if (ann != null) {// 获取 @Api 注解的信息Api api = joinPoint.getTarget().getClass().getAnnotation(Api.class);if (api != null) {String[] tags = api.tags();if (tags != null && tags.length > 0) {// 操作描述authOptLog.setDescription(tags[0] + "-" + ann.value());}} else {// 操作描述authOptLog.setDescription(ann.value());}}// 操作员信息// 程序执行到这里时,已经在过滤器或拦截器中完成了权限的校验,所以这里不需要再进行校验try {String token = request.getHeader("X-token");if(token != null) {JwtUserInfo userFromToken = authTokenUtils.getUserFromToken(token);if(userFromToken != null) {authOptLog.setUserId(userFromToken.getUserId());authOptLog.setUserName(userFromToken.getAccount());}}} catch (BaseException e) {log.error("解析token时发生错误 {} {}", e.getCode(), e.getMessage());}log.info(authOptLog.showReqMsg());// 保存到线程容器THREAD_LOCAL.set(authOptLog);}/*** 成功返回通知*/@AfterReturning(returning = "ret", pointcut = "optLogAspect()")public void doAfterReturning(Object ret) {// 根据返回对象 ret 再做一些操作AuthOptLog authOptLog = getAuthOptLog();BaseResult baseResult = Convert.convert(BaseResult.class, ret);if (baseResult == null) {authOptLog.setType("OPT");} else {authOptLog.setResultCode(StrUtil.toString(baseResult.getCode()));authOptLog.setResultMsg(baseResult.getMessage());if (baseResult.isSuccess()) {authOptLog.setType("OPT");} else {authOptLog.setType("EX");}}authOptLog.setFinishTime(LocalDateTime.now());authOptLog.setConsumingTime(authOptLog.getStartTime().until(authOptLog.getFinishTime(), ChronoUnit.MILLIS));log.info(authOptLog.toResMsg());// 发布事件applicationContext.publishEvent(new OptLogEvent(authOptLog));THREAD_LOCAL.remove();}/*** 异常返回通知*/@AfterThrowing(throwing = "e", pointcut = "optLogAspect()")public void doAfterThrowable(Throwable e) {// 根据异常返回对象 e 再做一些操作AuthOptLog authOptLog = getAuthOptLog();authOptLog.setType("EX");authOptLog.setExDetail(getStackTrace(e));authOptLog.setFinishTime(LocalDateTime.now());authOptLog.setConsumingTime(authOptLog.getStartTime().until(authOptLog.getFinishTime(), ChronoUnit.MILLIS));log.info(authOptLog.toResMsg());// 发布事件applicationContext.publishEvent(new OptLogEvent(authOptLog));THREAD_LOCAL.remove();}public static String getStackTrace(Throwable throwable) {StringWriter sw = new StringWriter();try (PrintWriter pw = new PrintWriter(sw)) {throwable.printStackTrace(pw);return sw.toString();}}}
在这个切面类中,主要完成了以下几件事情:
在切面类中定义切点,拦截Controller中添加
@OptLog
注解的方法在切面类中定义前置通知,在前置通知方法
doBefore()
中收集操作相关信息封装为AuthOptLog
对象并保存到线程容器ThreadLocal
中在切面类中定义成功返回通知,在成功返回通知方法
doAfterReturning
中通过ThreadLocal
获取AuthOptLog
对象并继续设置其他的成功操作信息,随后发布事件在切面类中定义异常返回通知,在异常返回通知方法
doAfterThrowable
中通过ThreadLocal
获取AuthOptLog
对象并继续设置其他的异常操作信息,随后发布事件另外,在
doBefore()
方法中,会根据token信息解析得到本次操作的操作员信息,解析的方法参考上一节:后台管理系统的通用权限解决方案(十四)基于JWT实现登录功能
- 5)创建操作日志事件监听类,监听
OptLogEvent
时间的发布,并将操作日志写入数据库
package com.itweid.auth.listener;import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.itweid.common.entity.AuthOptLog;
import com.itweid.opt.event.OptLogEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;/*** 操作日志监听器*/
@Slf4j
@Component
public class OptLogListener {// 异步监听OptLogEvent事件@Async@EventListener(OptLogEvent.class)public void saveAuthOptLog(OptLogEvent optLogEvent) {AuthOptLog authOptLog = (AuthOptLog) optLogEvent.getSource();// 将日志信息保存到数据库Db.save(authOptLog);log.info("监听到操作日志事件,已处理完毕...");}
}
- 6)创建配置类
OptLogConfiguration
,用于注册切面类OptLogAspect
package com.itweid.opt.config;import com.itweid.opt.aspect.OptLogAspect;
import org.springframework.context.annotation.Bean;/*** @author: itweid* @since: 2024-11-08 09:27:44*/
public class OptLogConfiguration {@Beanpublic OptLogAspect optLogAspect() {return new OptLogAspect();}
}
- 7)创建注解
@EnableOptLog
,导入配置类OptLogConfiguration
,用于标识是否启用操作日志监听
package com.itweid.opt.annotation;import com.itweid.opt.config.OptLogConfiguration;
import org.springframework.context.annotation.Import;import java.lang.annotation.*;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(OptLogConfiguration.class)
@Documented
@Inherited
public @interface EnableOptLog {
}
- 8)在启动类中标注
@EnableOptLog
、@EnableAsync
注解
@SpringBootApplication
@EnableAuthToken
@EnableAsync
@EnableOptLog
public class AuthServiceApp {public static void main(String[] args) {SpringApplication.run(AuthServiceApp.class, args);}
}
- 9)创建一个测试用的Controller方法
@RestController
@RequestMapping("/auth-user")
@Api(value = "AuthUserController", tags = "用户操作")
public class AuthUserController {@ApiOperation("新增用户")@PostMapping("/save")@OptLog("新增用户")public BaseResult save(@RequestBody AuthUser authUser) {return BaseResult.setOk();}
}
- 10)启动项目,访问
http://127.0.0.1:8081/doc.html
查看接口文档
- 11)调试新增用户接口
日志输出如下:
数据库记录:
可见,前端发起请求时,后端都会生成一条操作日志信息记录下来,以方便后期的溯源等。
…
本节完,更多内容查阅:后台管理系统的通用权限解决方案