Skip to content

Commit 08696b5

Browse files
committed
feat(admin): 新增任务审计日志功能
- 在JobCodeController和XxlJobServiceImpl中添加操作日志记录 - 记录任务新建、更新、删除、启停、触发及GLUE代码更新等敏感操作 - 日志包含操作人、操作类型和操作内容,便于安全审计和问题追溯 - 优化字符串判空逻辑,使用StringTool工具类替代手动判断 - 更新文档,增加审计日志特性说明和接入公司名单 - 调整HTTP任务参数示例展示方式,提升可读性 - 重构部分校验逻辑,提高代码健壮性和可维护性
1 parent 41354cf commit 08696b5

File tree

4 files changed

+105
-35
lines changed

4 files changed

+105
-35
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ XXL-JOB 是一个开源且免费项目,其正在进行的开发完全得益于
113113
- 35、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色;
114114
- 36、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作;
115115
- 37、AI任务:原生提供AI执行器,并内置多个AI任务Handler,与spring-ai、ollama、dify等集成打通,支持快速开发AI类任务。
116+
- 38、审计日志:记录任务操作敏感信息,用于系统监控、审计和安全分析,可快速追溯异常行为以及定位排查问题。
116117

117118
## Development
118119
于2015年中,我在github上创建XXL-JOB项目仓库并提交第一个commit,随之进行系统结构设计,UI选型,交互设计……
@@ -845,6 +846,20 @@ XXL-JOB 是一个开源且免费项目,其正在进行的开发完全得益于
845846
- 691、联通云
846847
- 692、北京爱话本科技有限公司
847848
- 693、北京起创科技有限公司
849+
- 694、平安证券【平安证券】
850+
- 695、合肥中科类脑智能技术有限公司
851+
- 696、南京同仁堂健康产业有限公司【同仁堂】
852+
- 697、铜仁市碧江区智惠加油站
853+
- 698、惟客数据
854+
- 699、凤凰新闻【凤凰新闻】
855+
- 700、深圳王力智能
856+
- 701、返利网数字科技股份有限公司
857+
- 702、上海阜能信息科技有限公司
858+
- 703、深圳市极能超电数字科技有限公司
859+
- 704、海目星激光科技集团股份有限公司
860+
- 705、深圳市极能超电数字科技有限公司
861+
- 706、安克创新科技股份有限公司【安克】
862+
- 707、大庆点神科技有限公司
848863
- ……
849864

850865
> 更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。

doc/XXL-JOB官方文档.md

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅
5858
- 35、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色;
5959
- 36、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作;
6060
- 37、AI任务:原生提供AI执行器,并内置多个AI任务Handler,与spring-ai、ollama、dify等集成打通,支持快速开发AI类任务。
61+
- 38、审计日志:记录任务操作敏感信息,用于系统监控、审计和安全分析,可快速追溯异常行为以及定位排查问题。
62+
6163

6264
### 1.4 发展
6365
于2015年中,我在github上创建XXL-JOB项目仓库并提交第一个commit,随之进行系统结构设计,UI选型,交互设计……
@@ -788,6 +790,20 @@ XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅
788790
- 691、联通云
789791
- 692、北京爱话本科技有限公司
790792
- 693、北京起创科技有限公司
793+
- 694、平安证券【平安证券】
794+
- 695、合肥中科类脑智能技术有限公司
795+
- 696、南京同仁堂健康产业有限公司【同仁堂】
796+
- 697、铜仁市碧江区智惠加油站
797+
- 698、惟客数据
798+
- 699、凤凰新闻【凤凰新闻】
799+
- 700、深圳王力智能
800+
- 701、返利网数字科技股份有限公司
801+
- 702、上海阜能信息科技有限公司
802+
- 703、深圳市极能超电数字科技有限公司
803+
- 704、海目星激光科技集团股份有限公司
804+
- 705、深圳市极能超电数字科技有限公司
805+
- 706、安克创新科技股份有限公司【安克】
806+
- 707、大庆点神科技有限公司
791807
- ……
792808

793809
> 更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。
@@ -2606,11 +2622,11 @@ public void execute() {
26062622
- 10、【修复】脚本任务process销毁逻辑优化,解决风险情况下脚本进程无法终止问题;
26072623
- 11、【修复】调度预读任务数量调整,改为调度线程池大小x10,降低事务颗粒度,提升性能及稳定性;
26082624
- 12、【修复】合并PR-2369,修复脚本任务参数取值问题;
2609-
- 13、【强化】通用HTTP任务(httpJobHandler)强化,支持更丰富请求参数设置,完整参数示例如下:
2625+
- 13、【强化】通用HTTP任务(httpJobHandler)强化,支持更丰富请求参数设置,完整参数示例如下:
26102626
26112627
<details>
2612-
<summary>完整参数示例参考:</summary>
2613-
2628+
<summary>完整参数示例参考:</summary>
2629+
26142630
```
26152631
{
26162632
"url": "http://www.baidu.com",
@@ -2636,15 +2652,19 @@ public void execute() {
26362652
- 16、【重构】规范API交互协议,通用响应结构体调整为Response,调度中心API统一为Response封装数据;
26372653
(注意:响应结构体从ReturnT升级为Response,其中属性值“content”会调整为“data”,取值逻辑需注意)
26382654
- 17、【升级】Http通讯组件升级,基于接口代理方式重构通讯组件,提升组件性能及扩展性;
2639-
- 18、【ING】UI框架重构升级,提升交互体验;
2640-
- 19、【ING】调整资源加载逻辑,移除不必要的拦截器逻辑,提升页面加载效率;
2655+
- 18、【新增】任务审计日志,记录任务操作敏感日志信息,如任务新建/更新/删除/启停/触发以及GLUE代码更新等,用于系统监控、审计和安全分析,可快速追溯异常行为以及定位排查问题等。
2656+
(当前任务审计日志以Info级别输出在系统日志中,可通过关键词 "xxl-job operation log:" 检索过滤)
2657+
- 19、【ING】UI框架重构升级,提升交互体验;
2658+
- 20、【ING】调整资源加载逻辑,移除不必要的拦截器逻辑,提升页面加载效率;
2659+
26412660
26422661
26432662
### TODO LIST
26442663
- 1、调度隔离:调度中心针对不同执行器,各自维护不同的调度和远程触发组件。
26452664
- 2、任务优先级:调度与执行阶段按照优先级分配资源。
26462665
- 3、多数据库支持,DAO层通过JPA实现,不限制数据库类型。
2647-
- 4、执行器Log清理功能:调度中心Log删除时同步删除执行器中的Log文件;
2666+
- 4、OpenApi:
2667+
- 执行器Log文件清理:支持调度中心远程删除执行器中指定任务的Log文件;
26482668
- 5、性能优化:任务、执行器数据全量本地缓存;新增消息表广播通知;
26492669
- 6、DAG流程任务
26502670
- 子任务:废弃
@@ -2653,24 +2673,30 @@ public void execute() {
26532673
- 分片任务:全部完成后才会出发后置节点;
26542674
- 配置并列的"a-b、b-c"路径列表,构成串行、并行、dag任务流程,"dagre-d3"绘图;任务依赖,流程图,子任务+会签任务,各节点日志;支持根据成功、失败选择分支;
26552675
- 7、任务标签:方便搜索;
2656-
- 8、告警增强:
2657-
- 邮件告警:支持自定义标题、模板格式;
2658-
- webhook告警:支持自定义告警URL、请求体格式;
2659-
- 9、安全强化:AccessToken动态生成、动态启停;控制调度、回调;
2676+
- 8、GLUE 模式 Web Ide 版本对比功能;
2677+
- 9、自定义失败重试时间间隔;
26602678
- 10、任务导入导出工具,灵活支持版本升级、迁移等场景。
26612679
- 11、任务日志重构:一次调度只记录一条主任务,维护起止时间和状态。
26622680
- 普通任务:只记录一条主任务;
26632681
- 广播任务:记录一条主任务,每个分片任务记录一条次任务,关联在主任务上;
26642682
- 重试任务:失败时,新增主任务。所有调度记录,包括入口调度和重试调度,均挂载主任务上。
26652683
- 12、分片任务:全部完成后才会出发后置节点;
26662684
- 13、日期过滤:支持多个时间段排除;
2667-
- 13、GLUE 模式 Web Ide 版本对比功能;
26682685
- 14、提供执行器Docker镜像;
26692686
- 15、脚本任务,支持数据参数,新版本仅支持单参数不支持需要兼容;
26702687
- 17、批量调度:调度请求入queue,调度线程批量获取调度请求并发起远程调度;提高线程效率;
26712688
- 18、执行器端口复用,复用容器端口提供通讯服务;
2672-
- 19、自定义失败重试时间间隔;
2673-
- 20、安全功能增强,通讯加密参数改用加密数据避免AccessToken明文, 降低token泄漏风险;
2689+
- 19、安全功能增强,通讯加密参数改用加密数据避免AccessToken明文, 降低token泄漏风险;
2690+
- 20、告警增强:
2691+
- 邮件告警:支持自定义标题、模板格式;
2692+
- webhook告警:支持自定义告警URL、请求体格式;
2693+
- 21、公共告警策略:执行器维度设置多告警策略,任务勾选启用;待评估任务或执行器维度;
2694+
- 20、日志策略:
2695+
- 调度日志:全局配置:废弃; 新增“调度日志策略”:任务维度自定义,保留3天、7天、1个月、3个月、一年、永久;
2696+
- 执行日志:新增“执行RollingLog开关”:任务维度自定义,支持:RollingLog、普通日志(slf4j输出)、关闭(不输出);
2697+
- 21、AccessToken:废弃全局配置;支持在线管理,动态生成、动态启停;
2698+
2699+
26742700
26752701
## 八、其他
26762702

xxl-job-admin/src/main/java/com/xxl/job/admin/controller/biz/JobCodeController.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77
import com.xxl.job.admin.util.I18nUtil;
88
import com.xxl.job.admin.util.JobGroupPermissionUtil;
99
import com.xxl.job.core.glue.GlueTypeEnum;
10+
import com.xxl.sso.core.model.LoginInfo;
1011
import com.xxl.tool.core.StringTool;
12+
import com.xxl.tool.gson.GsonTool;
1113
import com.xxl.tool.response.Response;
1214
import jakarta.annotation.Resource;
1315
import jakarta.servlet.http.HttpServletRequest;
16+
import org.slf4j.Logger;
17+
import org.slf4j.LoggerFactory;
1418
import org.springframework.stereotype.Controller;
1519
import org.springframework.ui.Model;
1620
import org.springframework.web.bind.annotation.RequestMapping;
@@ -27,6 +31,7 @@
2731
@Controller
2832
@RequestMapping("/jobcode")
2933
public class JobCodeController {
34+
private static final Logger logger = LoggerFactory.getLogger(JobCodeController.class);
3035

3136
@Resource
3237
private XxlJobInfoMapper xxlJobInfoMapper;
@@ -79,7 +84,7 @@ public Response<String> save(HttpServletRequest request,
7984
}
8085

8186
// valid jobGroup permission
82-
JobGroupPermissionUtil.validJobGroupPermission(request, existsJobInfo.getJobGroup());
87+
LoginInfo loginInfo = JobGroupPermissionUtil.validJobGroupPermission(request, existsJobInfo.getJobGroup());
8388

8489
// update new code
8590
existsJobInfo.setGlueSource(glueSource);
@@ -103,7 +108,10 @@ public Response<String> save(HttpServletRequest request,
103108
// remove code backup more than 30
104109
xxlJobLogGlueMapper.removeOld(existsJobInfo.getId(), 30);
105110

111+
// write operation log
112+
logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}",
113+
loginInfo.getUserName(), "jobcode-update", GsonTool.toJson(xxlJobLogGlue));
106114
return Response.ofSuccess();
107115
}
108-
116+
109117
}

xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import com.xxl.job.core.glue.GlueTypeEnum;
1919
import com.xxl.sso.core.model.LoginInfo;
2020
import com.xxl.tool.core.DateTool;
21+
import com.xxl.tool.core.StringTool;
22+
import com.xxl.tool.gson.GsonTool;
2123
import com.xxl.tool.response.PageModel;
2224
import com.xxl.tool.response.Response;
2325
import jakarta.annotation.Resource;
@@ -70,10 +72,10 @@ public Response<String> add(XxlJobInfo jobInfo, LoginInfo loginInfo) {
7072
if (group == null) {
7173
return Response.ofFail (I18nUtil.getString("system_please_choose")+I18nUtil.getString("jobinfo_field_jobgroup"));
7274
}
73-
if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) {
75+
if (StringTool.isBlank(jobInfo.getJobDesc())) {
7476
return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) );
7577
}
76-
if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) {
78+
if (StringTool.isBlank(jobInfo.getAuthor())) {
7779
return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) );
7880
}
7981

@@ -91,7 +93,7 @@ public Response<String> add(XxlJobInfo jobInfo, LoginInfo loginInfo) {
9193
return Response.ofFail ( (I18nUtil.getString("schedule_type")) );
9294
}
9395
try {
94-
int fixSecond = Integer.valueOf(jobInfo.getScheduleConf());
96+
int fixSecond = Integer.parseInt(jobInfo.getScheduleConf());
9597
if (fixSecond < 1) {
9698
return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
9799
}
@@ -104,7 +106,7 @@ public Response<String> add(XxlJobInfo jobInfo, LoginInfo loginInfo) {
104106
if (GlueTypeEnum.match(jobInfo.getGlueType()) == null) {
105107
return Response.ofFail ( (I18nUtil.getString("jobinfo_field_gluetype")+I18nUtil.getString("system_unvalid")) );
106108
}
107-
if (GlueTypeEnum.BEAN==GlueTypeEnum.match(jobInfo.getGlueType()) && (jobInfo.getExecutorHandler()==null || jobInfo.getExecutorHandler().trim().length()==0) ) {
109+
if (GlueTypeEnum.BEAN==GlueTypeEnum.match(jobInfo.getGlueType()) && StringTool.isBlank(jobInfo.getExecutorHandler()) ) {
108110
return Response.ofFail ( (I18nUtil.getString("system_please_input")+"JobHandler") );
109111
}
110112
// 》fix "\r" in shell
@@ -124,10 +126,10 @@ public Response<String> add(XxlJobInfo jobInfo, LoginInfo loginInfo) {
124126
}
125127

126128
// 》ChildJobId valid
127-
if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) {
129+
if (StringTool.isNotBlank(jobInfo.getChildJobId())) {
128130
String[] childJobIds = jobInfo.getChildJobId().split(",");
129131
for (String childJobIdItem: childJobIds) {
130-
if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) {
132+
if (StringTool.isNotBlank(childJobIdItem) && StringTool.isNumeric(childJobIdItem)) {
131133
XxlJobInfo childJobInfo = xxlJobInfoMapper.loadById(Integer.parseInt(childJobIdItem));
132134
if (childJobInfo==null) {
133135
return Response.ofFail (
@@ -165,26 +167,21 @@ public Response<String> add(XxlJobInfo jobInfo, LoginInfo loginInfo) {
165167
return Response.ofFail ( (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail")) );
166168
}
167169

168-
return Response.ofSuccess(String.valueOf(jobInfo.getId()));
169-
}
170+
// write operation log
171+
logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}",
172+
loginInfo.getUserName(), "jobinfo-save", GsonTool.toJson(jobInfo));
170173

171-
private boolean isNumeric(String str){
172-
try {
173-
int result = Integer.valueOf(str);
174-
return true;
175-
} catch (NumberFormatException e) {
176-
return false;
177-
}
174+
return Response.ofSuccess(String.valueOf(jobInfo.getId()));
178175
}
179176

180177
@Override
181178
public Response<String> update(XxlJobInfo jobInfo, LoginInfo loginInfo) {
182179

183180
// valid base
184-
if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) {
181+
if (StringTool.isBlank(jobInfo.getJobDesc())) {
185182
return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) );
186183
}
187-
if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) {
184+
if (StringTool.isBlank(jobInfo.getAuthor())) {
188185
return Response.ofFail ( (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) );
189186
}
190187

@@ -202,7 +199,7 @@ public Response<String> update(XxlJobInfo jobInfo, LoginInfo loginInfo) {
202199
return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
203200
}
204201
try {
205-
int fixSecond = Integer.valueOf(jobInfo.getScheduleConf());
202+
int fixSecond = Integer.parseInt(jobInfo.getScheduleConf());
206203
if (fixSecond < 1) {
207204
return Response.ofFail ( (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) );
208205
}
@@ -223,10 +220,10 @@ public Response<String> update(XxlJobInfo jobInfo, LoginInfo loginInfo) {
223220
}
224221

225222
// 》ChildJobId valid
226-
if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) {
223+
if (StringTool.isNotBlank(jobInfo.getChildJobId())) {
227224
String[] childJobIds = jobInfo.getChildJobId().split(",");
228225
for (String childJobIdItem: childJobIds) {
229-
if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) {
226+
if (StringTool.isNotBlank(childJobIdItem) && StringTool.isNumeric(childJobIdItem)) {
230227
// parse child
231228
int childJobId = Integer.parseInt(childJobIdItem);
232229
if (childJobId == jobInfo.getId()) {
@@ -310,6 +307,10 @@ public Response<String> update(XxlJobInfo jobInfo, LoginInfo loginInfo) {
310307
exists_jobInfo.setUpdateTime(new Date());
311308
xxlJobInfoMapper.update(exists_jobInfo);
312309

310+
// write operation log
311+
logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}",
312+
loginInfo.getUserName(), "jobinfo-update", GsonTool.toJson(exists_jobInfo));
313+
313314
return Response.ofSuccess();
314315
}
315316

@@ -329,6 +330,11 @@ public Response<String> remove(int id, LoginInfo loginInfo) {
329330
xxlJobInfoMapper.delete(id);
330331
xxlJobLogMapper.delete(id);
331332
xxlJobLogGlueMapper.deleteByJobId(id);
333+
334+
// write operation log
335+
logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}",
336+
loginInfo.getUserName(), "jobinfo-remove", id);
337+
332338
return Response.ofSuccess();
333339
}
334340

@@ -372,6 +378,11 @@ public Response<String> start(int id, LoginInfo loginInfo) {
372378

373379
xxlJobInfo.setUpdateTime(new Date());
374380
xxlJobInfoMapper.update(xxlJobInfo);
381+
382+
// write operation log
383+
logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}",
384+
loginInfo.getUserName(), "jobinfo-start", id);
385+
375386
return Response.ofSuccess();
376387
}
377388

@@ -395,6 +406,11 @@ public Response<String> stop(int id, LoginInfo loginInfo) {
395406

396407
xxlJobInfo.setUpdateTime(new Date());
397408
xxlJobInfoMapper.update(xxlJobInfo);
409+
410+
// write operation log
411+
logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}",
412+
loginInfo.getUserName(), "jobinfo-stop", id);
413+
398414
return Response.ofSuccess();
399415
}
400416

@@ -417,6 +433,11 @@ public Response<String> trigger(LoginInfo loginInfo, int jobId, String executorP
417433
}
418434

419435
XxlJobAdminBootstrap.getInstance().getJobTriggerPoolHelper().trigger(jobId, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList);
436+
437+
// write operation log
438+
logger.info(">>>>>>>>>>> xxl-job operation log: operator = {}, type = {}, content = {}",
439+
loginInfo.getUserName(), "jobinfo-trigger", jobId);
440+
420441
return Response.ofSuccess();
421442
}
422443

0 commit comments

Comments
 (0)