0%

SpringBoot-Mybatis

数据审计

部分业务需要记录用户对操作行为,如果给每张表都做一个记录表,感觉冗余数据太多了。所以采用审计表存储相关日志信息。

实现方案

基于Spring Aop 织入方式对关键方法进行拦截,也可以通过方法进行拦截。

  • 基于注解,在方法上织入。
  • 基于普通方法,在业务逻辑中进行调用。
  • 批量写入、延时写入数据库可能会出现短时间部分数据的丢失,但是频繁写入会导致Mysql资源占用过多。
    • 使用java.util.concurrent.ConcurrentLinkedQueue 作为消费列队。每次最多消费消费10条日志记录批量写入数据库。
  • 在审计信息中添加业务执行链路标识、执行用户标识。方便排查问题。
    • 基于MDC记录链路标识。

演示效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 无参数
* @return
*/
@MonitorAnnotation(audit = true, type = "查看", title = "首页")
@RequestMapping(value = "")
public Object index() {
...
}

/**
* url占位符
* @param key
* @return
*/
@RequestMapping(value = "/add{key}")
public Object add(@PathVariable(required = false) String key) {
if (Objects.isNull(key)) {
key = Strings.EMPTY;
}
auditService.test(key);
...
}

/**
* url参数
* @param apiParam
* @return
*/
@MonitorAnnotation(audit = true, type = "查看", title = "api", descriptionExpression = "解析参数 #{[0].id}")
@RequestMapping(value = "/path")
public Object path(ApiParam apiParam) {
...
}

/**
* json参数
* @param apiParam
* @return
*/
@MonitorAnnotation(audit = true, type = "查看", title = "api", descriptionExpression = "解析json #{[0].id}")
@RequestMapping(value = "/api")
public Object api(@RequestBody ApiParam apiParam) {
...
}
  • Console
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 无参数
code-example git:(main) ✗ curl http://127.0.0.1:9041
{"code":"200"}%

[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 58] [fffffffffe71f0b8ffffffffaed06e501644984299461010] [] HTTP URL Method : http://127.0.0.1:9041/#GET
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 68] [fffffffffe71f0b8ffffffffaed06e501644984299461010] [] Class Method : cn.z201.audit.AppApplicationController#index
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 79] [fffffffffe71f0b8ffffffffaed06e501644984299461010] [] Args : []
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 64] [fffffffffe71f0b8ffffffffaed06e501644984299461010] [] Time-Consuming : 10 ms
[XNIO-1 task-1] [AuditRepository.java : 50] [fffffffffe71f0b8ffffffffaed06e501644984299461010] [] {"eventType":"查看","eventTitle":"首页","eventDescription":"","eventTime":1644984299487,"opTraceId":"fffffffffe71f0b8ffffffffaed06e501644984299461010","userId":1}
# url参数 方法调用
code-example git:(main) ✗ curl http://127.0.0.1:9041/add1
{"code":"200","data":"1"}%

[XNIO-1 task-1] [AuditRepository.java : 50] [000000001aeb91faffffffff9f9ad4b01644984411302010] [] {"eventType":"查看","eventTitle":"测试方法写入","eventDescription":"1","opTraceId":"000000001aeb91faffffffff9f9ad4b01644984411302010","userId":1}
#url参数
code-example git:(main) ✗ curl http://127.0.0.1:9041/path\?id\=1
{"code":"200","data":{"id":1}}%
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 58] [ffffffffbc9cb15500000000603b0e6a1644984489674100] [] HTTP URL Method : http://127.0.0.1:9041/path#GET
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 68] [ffffffffbc9cb15500000000603b0e6a1644984489674100] [] Class Method : cn.z201.audit.AppApplicationController#path
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 79] [ffffffffbc9cb15500000000603b0e6a1644984489674100] [] Args : [{"id":1}]
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 64] [ffffffffbc9cb15500000000603b0e6a1644984489674100] [] Time-Consuming : 3 ms
[XNIO-1 task-1] [AuditRepository.java : 50] [ffffffffbc9cb15500000000603b0e6a1644984489674100] [] {"eventType":"查看","eventTitle":"api","eventDescription":"解析参数 1","opTraceId":"ffffffffbc9cb15500000000603b0e6a1644984489674100","userId":1}

# json 参数
code-example git:(main) ✗ curl -X POST http://localhost:9041/api -H 'Content-Type: application/json' -d '{"id":"1"}'
{"code":"200","data":{"id":1}}%
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 58] [000000001fbbccdf000000006a218db41644985048604100] [] HTTP URL Method : http://localhost:9041/api#POST
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 68] [000000001fbbccdf000000006a218db41644985048604100] [] Class Method : cn.z201.audit.AppApplicationController#api
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 79] [000000001fbbccdf000000006a218db41644985048604100] [] Args : [{"id":1}]
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 64] [000000001fbbccdf000000006a218db41644985048604100] [] Time-Consuming : 1 ms
[XNIO-1 task-1] [AuditRepository.java : 50] [000000001fbbccdf000000006a218db41644985048604100] [] {"eventType":"查看","eventTitle":"api","eventDescription":"解析json 1","opTraceId":"000000001fbbccdf000000006a218db41644985048604100","userId":1}

演示代码

SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 创建数据库
CREATE DATABASE IF NOT EXISTS `docker_mybatis_audit` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS docker_mybatis_audit.`biz_audit_log`;
CREATE TABLE IF NOT EXISTS docker_mybatis_audit.`biz_audit_log`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`is_enable` bit(1) NOT NULL DEFAULT b'1' COMMENT '数据是否有效 1 有效 0 无效',
`create_time` bigint(20) unsigned NOT NULL COMMENT '创建时间',
`update_time` bigint(20) unsigned NOT NULL COMMENT '更新时间',
`event_type` varchar(100) COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件类型 insert update delete select 等等',
`event_title` varchar(100) COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件标题 xxx查看了什么记录 xx修改了什么记录',
`event_description` varchar(100) COLLATE utf8mb4_bin DEFAULT '' COMMENT '事件描述',
`event_time` bigint(20) unsigned NOT NULL COMMENT '事件记录时间',
`op_trace_id` varchar(100) COLLATE utf8mb4_bin DEFAULT '' COMMENT '操作标志',
`user_id` BIGINT(20) unsigned NOT NULL DEFAULT '0' COMMENT '操作用户id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据审计日志';

AuditRepository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* @author z201.coding@gmail.com
**/
@Repository
@Slf4j
public class AuditRepository {

@Autowired
@Qualifier("auditLogExecutor")
private MdcThreadPoolTaskExecutor mdcThreadPoolTaskExecutor;

@Resource
private BizAuditLogDao bizAuditLogDao;

private static ConcurrentLinkedQueue<BizAuditLog> concurrentLinkedQueue = new ConcurrentLinkedQueue();

private void add(BizAuditLog bizAuditLog) {
if (Objects.isNull(bizAuditLog)) {
return;
}
if (Objects.isNull(bizAuditLog.getUserId())) {
// 演示场景,应该是从其他地方插入当前用户信息。
bizAuditLog.setUserId(1L);
}
log.info("{}", JsonTool.toString(bizAuditLog));
concurrentLinkedQueue.add(bizAuditLog);
}

/**
* 普通参数写入方式
* @param type
* @param title
* @param descriptionExpression
* @param args
*/
public void add(String type,String title,String descriptionExpression, Object args) {
//格式化描述表达式得到易读的描述
String description = parseDescriptionExpression(new Object[]{args}, descriptionExpression);
BizAuditLog bizAuditLog = new BizAuditLog();
bizAuditLog.setEventType(type);
bizAuditLog.setEventTitle(title);
bizAuditLog.setEventDescription(description);
bizAuditLog.setEventTime(System.currentTimeMillis());
bizAuditLog.setOpTraceId(MdcTool.getInstance().get());
add(bizAuditLog);
}

/**
* 注解拦截方式
* @param monitorAnnotation
* @param args
*/
public void add(MonitorAnnotation monitorAnnotation, Object[] args) {
if (monitorAnnotation.audit()) {
String type = monitorAnnotation.type();
String title = monitorAnnotation.title();
String descriptionExpression = monitorAnnotation.descriptionExpression();
//格式化描述表达式得到易读的描述
String description = parseDescriptionExpression(args, descriptionExpression);
BizAuditLog bizAuditLog = new BizAuditLog();
bizAuditLog.setEventType(type);
bizAuditLog.setEventTitle(title);
bizAuditLog.setEventDescription(description);
bizAuditLog.setEventTime(System.currentTimeMillis());
bizAuditLog.setOpTraceId(MdcTool.getInstance().get());
add(bizAuditLog);
}
}

@PostConstruct
public void init() {
mdcThreadPoolTaskExecutor.execute(() -> {
for (; ; ) {
try {
if (concurrentLinkedQueue.isEmpty()) {
Thread.sleep(500);
} else {
Iterator<BizAuditLog> iterator = concurrentLinkedQueue.iterator();
List<BizAuditLog> bizAuditLogList = new ArrayList<>();
while (iterator.hasNext()) {
if (bizAuditLogList.size() >= 10) {
break;
}
bizAuditLogList.add(iterator.next());
iterator.remove();
}
bizAuditLogDao.batchInsert(bizAuditLogList);
Thread.sleep(500);
}
} catch (Exception e) {
log.error("审计日志存储失败 :" + e);
}
}
});
}

private String parseDescriptionExpression(Object[] args, String descriptionExpression) {
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression expression = spelExpressionParser.parseExpression(descriptionExpression, new TemplateParserContext());
return expression.getValue(new StandardEvaluationContext(args), String.class);
}

}

  • 采用spring spel作为默认的参数解析。

END

  • 本文作者: 庆峰的博客
  • 本文链接: https://z201.cn/posts/5961172/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!