对于全局日志的记录,大多数人都能立马想到使用aop环绕通知进行记录,如果现在有需求需要对用户的操作记录留痕呢?此文章就是在全局日志的基础上实现操作留痕
信息安全的基本原则之一是“可追溯性”,即任何操作都应有迹可循。通过记录员工的操作行为,可以确保在发生数据泄露或其他安全事件时,能够迅速定位问题源头,采取相应措施。
行为分析是通过对员工日常操作行为的记录和分析,识别异常行为,从而预防潜在风险。管理层面则需要根据这些数据制定相应的管理策略,确保员工行为符合公司规范。
法律法规对数据保护和信息安全有明确要求。员工操作留痕不仅是企业内部管理的需要,也是满足外部合规要求的必要手段。
实现的方式多种多样,常见的有2种方式:
本文就是基于后端实现一套【高性能的操作日志留痕】,基于后端去实现操作留痕都会不可避免地对原代码有侵入性,但是我们要想出入侵性最低的方案 - 基于注解去实现。
对于不熟悉 AOP 的伙伴,可以先到官方文档上看下基本的使用 - Spring AOP 官方文档
1、通过注解声明功能模块
、操作
,由后端controller接口上声明
优点:便于后端问题排查
缺点:存在代码冗余,由于接口的复用性太多,定义功能模块与操作(新增编辑删除)等无法说明页面操作,例如app与erp都调用同一个接口查询列表,或者新增提交,一个接口就无法区分,只能重写的controller接口进行区分。
2、注解标记记录留痕,由前端请求头中声明功能模块
、操作
,后端进行采集 (⭐️ 推荐)
优点:操作留痕更精确
缺点:多负担一些网络IO的传输
相关信息
UTF-8编码中一个汉字也就是3-4个字节,一次接口调用一般在10个汉字左右,按照多的来算,平均每次调用40字节,对于消耗的那点网络IO可以忽略不计,推荐采用第二种方案进行数据采集
既然是无侵入式的,那注解少不了,先定义好注解。
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface PassLog { }
操作留痕的表,可自行进行扩展,此处 browser
对于app的操作存储手机的唯一标识
sqlcreate table sys_log
(
id bigint auto_increment
primary key,
operate_user_id int not null comment '操作人ID',
operate_user_name varchar(64) not null comment '操作人Name',
operate_module varchar(64) null comment '操作模块',
operate_item varchar(128) null comment '操作项',
interface_path varchar(512) not null comment '接口路径',
method varchar(64) null comment '请求方式(get、post等)',
ip varchar(128) not null comment 'ip',
browser varchar(512) null comment '浏览器',
method_name varchar(512) not null comment '方法名',
result varchar(512) not null comment '响应结果',
result_time bigint null comment '响应时间(单位:ms)',
create_time datetime not null comment '创建时间'
)
comment '系统日志';
实体类
java@Data
@Accessors(chain = true)
@TableName("sys_log")
public class SysLog {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 操作人ID
*/
@TableField(value = "operate_user_id")
private Long operateUserId;
/**
* 操作人Name
*/
@TableField(value = "operate_user_name")
private String operateUserName;
/**
* 操作模块
*/
@TableField(value = "operate_module")
private String operateModule;
/**
* 操作项
*/
@TableField(value = "operate_item")
private String operateItem;
/**
* 接口路径
*/
@TableField(value = "interface_path")
private String interfacePath;
/**
* 请求方式
*/
@TableField(value = "method")
private String method;
/**
* ip
*/
@TableField(value = "ip")
private String ip;
/**
* 浏览器
*/
@TableField(value = "browser")
private String browser;
/**
* 方法名
*/
@TableField(value = "method_name")
private String methodName;
/**
* 响应结果
*/
@TableField(value = "result")
private String result;
/**
* 响应时间
*/
@TableField(value = "result_time")
private Long resultTime;
/**
* 创建时间
*/
@TableField(value = "create_time")
private LocalDateTime createTime;
}
具体的代码实现逻辑如下:
@Around
与 AfterThrowing
方法PassLog
注解,存在则进行操作留痕User-Agent
、Operate_Module
、Operate_Item
记录java@Aspect
@Slf4j
@Component
public class LogAspect implements Ordered, DisposableBean {
@Resource
private SysLogService sysLogService;
/**
* Get the order value of this object.
* <p>Higher values are interpreted as lower priority. As a consequence,
* the object with the lowest value has the highest priority (somewhat
* analogous to Servlet {@code load-on-startup} values).
* <p>Same order values will result in arbitrary sort positions for the
* affected objects.
*
* @return the order value
* @see #HIGHEST_PRECEDENCE
* @see #LOWEST_PRECEDENCE
*/
@Override
public int getOrder() {
return LOWEST_PRECEDENCE;
}
@Pointcut("execution(* cn.liushigong.echoim.controller.*.*(..))")
private void pointcutToLog() {}
/**
* 日志切面
*/
@Around("pointcutToLog()")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
// 方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 类名+方法名
String methodName = signature.getDeclaringTypeName() + "#" + method.getName();
log.info("{}, params = {}", methodName, JSONUtil.toJsonStr(joinPoint.getArgs()));
// 计时器
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object result = joinPoint.proceed();
stopWatch.stop();
// 构建日志
buildLog(method, stopWatch.getTotalTimeMillis(), methodName, JSONUtil.toJsonStr(result), null);
return result;
}
@AfterThrowing(pointcut="pointcutToLog()", throwing="ex")
public void doRecoveryActions(JoinPoint joinPoint, Exception ex) throws Throwable {
// 方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 类名+方法名
String methodName = signature.getDeclaringTypeName() + "#" + method.getName();
// 构建日志
buildLog(method, -1, methodName, null, ex);
}
public void buildLog(Method method, long takeTime, String methodName, String result, Throwable ex){
// 记录系统日志
boolean hasAnnotation = AnnotationUtil.hasAnnotation(method, PassLog.class);
if (hasAnnotation) {
SysLog sysLog = new SysLog();
sysLog.setResultTime(takeTime);
sysLog.setResult(ex != null ? ex.getMessage() : "success");
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
sysLog.setInterfacePath(request.getRequestURI());
sysLog.setMethod(request.getMethod());
sysLog.setIp(request.getRemoteAddr());
sysLog.setBrowser(request.getHeader("User-Agent"));
sysLog.setOperateModule(request.getHeader("Operate_Module"));
sysLog.setOperateItem(request.getHeader("Operate_Item"));
sysLog.setMethodName(methodName);
sysLog.setOperateUserId(0L);
sysLog.setOperateUserName("");
sysLog.setCreateTime(LocalDateTime.now());
// 异步记录日志
LogThreadPool.execute(() -> {
sysLogService.save(sysLog);
});
}
// 记录异常日志
if (ex != null) {
log.info("{}, error = {}", methodName, ex.getMessage(), ex);
}
else {
log.info("{}, result = {}, takeTime [{}] ms", methodName, result, takeTime);
}
}
@Override
public void destroy() throws Exception {
LogThreadPool.shutdown(false);
}
/**
* 日志线程池
*/
static class LogThreadPool {
/**
* 执行器
*/
private static ThreadPoolExecutor executor;
/**
* 私有构造器 拒绝new
*/
private LogThreadPool() {
}
static {
init();
}
/**
* 初始化
*/
public static synchronized void init() {
//线程池已存在则立即关闭
if (null != executor) {
executor.shutdown();
}
//创建线程池
executor = createThreadPool();
}
/**
* 初始化线程池
*/
private static ThreadPoolExecutor createThreadPool() {
// 原子类
AtomicInteger threadNumber = new AtomicInteger(1);
// CPU核心数
int coreNum = Runtime.getRuntime().availableProcessors();
// 创建线程池
return new ThreadPoolExecutor(
coreNum + 1,
coreNum * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000),
r -> new Thread(r, "Log-thread-" + threadNumber.getAndIncrement()),
new ThreadPoolExecutor.CallerRunsPolicy());
}
/**
* 关闭线程池
*
* @param isNow 是否立即关闭
*/
public static synchronized void shutdown(boolean isNow) {
if (null != executor) {
if (isNow) {
executor.shutdownNow();
} else {
executor.shutdown();
}
}
}
/**
* 执行线程
*
* @param runnable runnable
*/
public static void execute(Runnable runnable) {
executor.execute(runnable);
}
/**
* 执行线程
*
* @param task task
* @param <T> T
* @return Future
*/
public static <T> Future<T> submit(Callable<T> task) {
return executor.submit(task);
}
/**
* 执行线程
*
* @param runnable runnable
* @return Future
*/
public static Future<?> submit(Runnable runnable) {
return executor.submit(runnable);
}
}
}
全局日志在结合TraceId,可以参考全链路日志TraceId穿透异步线程与MQ消息,这才是完整的一套高性能日志的解决方案,最终效果如下
本文作者:柳始恭
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!