定时任务白名单机制防止代码注入

作者:忆笙智云官方 | 发布时间:2026-05-06 10:15 | 更新时间:2026-07-06 10:15

定时任务白名单机制防止代码注入

引言

定时任务是后端系统中常见的功能模块,用于执行数据同步、报表生成、缓存刷新等周期性操作。在低代码平台中,用户可能通过管理界面动态创建和管理定时任务,这带来了一个严重的安全隐患:代码注入攻击

如果定时任务框架允许执行任意类和方法,攻击者可以通过创建恶意任务来执行系统命令、读取敏感文件、甚至获取服务器控制权。本文分享一种基于白名单机制的定时任务安全防护方案,从数据库配置、创建校验到运行时检查,全链路保障定时任务的执行安全。

核心内容

一、安全威胁分析

flowchart TB
    A[定时任务安全威胁] --> B[任意类执行]
    A --> C[系统命令注入]
    A --> D[敏感信息泄露]
    A --> E[权限提升攻击]

    B --> B1["Runtime.exec('rm -rf /')"]
    C --> C1["ProcessBuilder 执行 Shell"]
    D --> D1["读取 /etc/passwd"]
    E --> E1["通过反射调用特权方法"]

    style A fill:#ff6b6b,color:#fff
    style B fill:#ffa07a
    style C fill:#ffa07a
    style D fill:#ffa07a
    style E fill:#ffa07a

常见的攻击场景:

// 攻击示例:通过定时任务执行系统命令
// 任务配置:beanName=java.lang.Runtime, methodName=exec, params=rm -rf /
// 如果没有白名单限制,这种任务会被直接执行

// 攻击示例:通过反射获取敏感信息
// 任务配置:beanName=java.lang.Class, methodName=getRuntime

二、白名单机制整体设计

flowchart TB
    subgraph 任务创建阶段
        A[用户提交任务配置] --> B{beanName+method 在白名单中?}
        B -->|否| C[拒绝创建,返回错误提示]
        B -->|是| D[保存任务到数据库]
    end

    subgraph 任务执行阶段
        E[调度器触发任务] --> F{运行时校验白名单}
        F -->|不在白名单| G[拒绝执行,记录安全日志]
        F -->|在白名单| H[反射调用目标方法]
        H --> I[执行结果记录]
    end

    subgraph 白名单管理
        J[数据库配置白名单] --> K[应用启动时加载缓存]
        K --> L[管理界面维护]
        L --> M[动态刷新缓存]
    end

    style C fill:#ff6b6b,color:#fff
    style G fill:#ff6b6b,color:#fff

三、白名单数据模型

3.1 数据库表设计

-- 定时任务白名单表
CREATE TABLE sys_job_whitelist (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    bean_name       VARCHAR(200) NOT NULL COMMENT '允许执行的Bean名称(全限定类名或Spring Bean名)',
    method_name     VARCHAR(100) NOT NULL COMMENT '允许执行的方法名',
    description     VARCHAR(500) COMMENT '方法功能描述',
    params_schema   JSON COMMENT '参数格式说明(JSON Schema)',
    status          TINYINT DEFAULT 1 COMMENT '状态:0-禁用 1-启用',
    create_by       VARCHAR(64) COMMENT '创建人',
    create_time     DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_by       VARCHAR(64) COMMENT '更新人',
    update_time     DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    UNIQUE KEY uk_bean_method (bean_name, method_name)
) COMMENT '定时任务白名单配置表';

3.2 白名单初始数据

-- 预置常用任务白名单
INSERT INTO sys_job_whitelist (bean_name, method_name, description) VALUES
('dataSyncTask', 'syncUserData', '同步用户数据'),
('dataSyncTask', 'syncDeptData', '同步部门数据'),
('reportTask', 'generateDailyReport', '生成日报表'),
('reportTask', 'generateMonthlyReport', '生成月报表'),
('cacheTask', 'refreshDictCache', '刷新字典缓存'),
('cacheTask', 'refreshConfigCache', '刷新配置缓存'),
('logTask', 'cleanExpiredLogs', '清理过期日志'),
('logTask', 'archiveOldLogs', '归档历史日志');

四、白名单校验实现

4.1 白名单实体与缓存

/**
 * 定时任务白名单实体
 */
@Data
@TableName("sys_job_whitelist")
public class JobWhitelist {

    @TableId(type = IdType.AUTO)
    private Long id;

    /** 允许执行的Bean名称 */
    private String beanName;

    /** 允许执行的方法名 */
    private String methodName;

    /** 方法功能描述 */
    private String description;

    /** 参数格式说明 */
    private String paramsSchema;

    /** 状态:0-禁用 1-启用 */
    private Integer status;
}
/**
 * 白名单缓存服务
 * 应用启动时加载到内存,变更时动态刷新
 */
@Component
public class JobWhitelistCache {

    /** 白名单缓存 key: beanName#methodName */
    private volatile Set<String> whitelistSet = new HashSet<>();

    @Autowired
    private JobWhitelistMapper whitelistMapper;

    /**
     * 初始化加载白名单
     */
    @PostConstruct
    public void init() {
        refreshCache();
    }

    /**
     * 刷新缓存
     */
    public void refreshCache() {
        List<JobWhitelist> list = whitelistMapper.selectList(
            new LambdaQueryWrapper<JobWhitelist>().eq(JobWhitelist::getStatus, 1)
        );
        Set<String> newSet = list.stream()
            .map(w -> w.getBeanName() + "#" + w.getMethodName())
            .collect(Collectors.toSet());
        this.whitelistSet = newSet;
    }

    /**
     * 校验是否在白名单中
     * @param beanName   Bean名称
     * @param methodName 方法名
     * @return true-在白名单中 false-不在白名单
     */
    public boolean isAllowed(String beanName, String methodName) {
        String key = beanName + "#" + methodName;
        return whitelistSet.contains(key);
    }
}

4.2 任务创建时校验

/**
 * 定时任务服务
 */
@Service
public class JobService {

    @Autowired
    private JobWhitelistCache whitelistCache;

    @Autowired
    private JobMapper jobMapper;

    /**
     * 创建定时任务
     * 创建前校验 beanName + methodName 是否在白名单中
     */
    public void createJob(JobDTO jobDTO) {
        // 1. 白名单校验
        if (!whitelistCache.isAllowed(jobDTO.getBeanName(), jobDTO.getMethodName())) {
            throw new BusinessException(
                "不允许执行该任务:[%s.%s] 不在白名单中,请联系管理员添加",
                jobDTO.getBeanName(), jobDTO.getMethodName()
            );
        }

        // 2. 参数格式校验(如果白名单配置了参数Schema)
        validateParams(jobDTO);

        // 3. Cron 表达式校验
        validateCron(jobDTO.getCronExpression());

        // 4. 保存任务
        Job job = new Job();
        BeanUtils.copyProperties(jobDTO, job);
        jobMapper.insert(job);
    }
}

4.3 任务执行时校验

即使数据库中的任务被篡改(如直接修改数据库),执行时仍需二次校验:

/**
 * 定时任务执行器
 * 运行时二次校验白名单,防止数据库被直接篡改
 */
@Component
public class JobExecutor {

    @Autowired
    private JobWhitelistCache whitelistCache;

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 执行定时任务
     */
    public void execute(Job job) {
        String beanName = job.getBeanName();
        String methodName = job.getMethodName();

        // 运行时白名单校验(双重保障)
        if (!whitelistCache.isAllowed(beanName, methodName)) {
            logSecurityEvent(job, "任务不在白名单中,拒绝执行");
            throw new SecurityException("非法任务执行被拦截");
        }

        try {
            // 通过 Spring 容器获取 Bean 实例
            Object target = applicationContext.getBean(beanName);

            // 反射调用方法(仅允许无参或简单参数方法)
            Method method = findMethod(target.getClass(), methodName, job.getParams());
            method.setAccessible(true);
            Object result = method.invoke(target, parseParams(job.getParams()));

            log.info("任务执行成功: {}.{}", beanName, methodName);
        } catch (Exception e) {
            log.error("任务执行失败: {}.{}", beanName, methodName, e);
            throw new RuntimeException("任务执行异常", e);
        }
    }

    /**
     * 记录安全事件
     */
    private void logSecurityEvent(Job job, String reason) {
        log.warn("【安全告警】定时任务被拦截 - 任务ID:{}, Bean:{}, Method:{}, 原因:{}",
            job.getId(), job.getBeanName(), job.getMethodName(), reason);
        // 可接入安全审计系统
    }
}

五、白名单管理界面

flowchart TB
    subgraph 白名单管理
        A[管理员进入白名单管理页] --> B[查看当前白名单列表]
        B --> C{操作}
        C -->|新增| D[填写Bean名+方法名+描述]
        D --> E[保存并刷新缓存]
        C -->|编辑| F[修改描述/参数Schema]
        F --> E
        C -->|禁用| G[设置status=0]
        G --> E
        C -->|删除| H[确认后删除记录]
        H --> E
    end

    subgraph 缓存同步
        E --> I[更新数据库]
        I --> J[刷新内存缓存]
        J --> K[集群节点同步通知]
    end

5.1 白名单管理接口

/**
 * 白名单管理控制器
 */
@RestController
@RequestMapping("/api/job/whitelist")
public class JobWhitelistController {

    @Autowired
    private JobWhitelistService whitelistService;

    /** 查询白名单列表 */
    @GetMapping("/list")
    public Result<List<JobWhitelist>> list() {
        return Result.success(whitelistService.list());
    }

    /** 新增白名单 */
    @PostMapping
    public Result<Void> add(@RequestBody JobWhitelistDTO dto) {
        // 校验 Bean 是否存在于 Spring 容器中
        whitelistService.validateBeanExists(dto.getBeanName(), dto.getMethodName());
        whitelistService.add(dto);
        return Result.success();
    }

    /** 刷新缓存 */
    @PostMapping("/refresh")
    public Result<Void> refreshCache() {
        whitelistService.refreshCache();
        return Result.success();
    }
}

六、集群环境缓存同步

在多节点部署时,白名单缓存需要保持一致:

/**
 * 基于Redis Pub/Sub的缓存同步
 */
@Component
public class WhitelistCacheSync {

    private static final String CHANNEL = "job:whitelist:refresh";

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private JobWhitelistCache whitelistCache;

    /**
     * 发布缓存刷新通知
     */
    public void publishRefresh() {
        redisTemplate.convertAndSend(CHANNEL, "refresh");
    }

    /**
     * 订阅缓存刷新通知
     */
    @RedisListener(channel = CHANNEL)
    public void onRefreshMessage(String message) {
        if ("refresh".equals(message)) {
            whitelistCache.refreshCache();
            log.info("白名单缓存已刷新");
        }
    }
}

七、安全纵深防护

白名单是核心防线,但还需配合其他安全措施:

flowchart LR
    A[安全防护体系] --> B[白名单校验]
    A --> C[参数格式校验]
    A --> D[执行超时控制]
    A --> E[安全审计日志]
    A --> F[数据库防篡改]

    B --> B1[创建时校验]
    B --> B2[执行时校验]
    C --> C1[参数Schema验证]
    D --> D1[超时自动终止]
    E --> E1[操作全量记录]
    F --> F1[关键字段签名校验]

7.1 任务参数格式校验

/**
 * 参数格式校验
 * 防止通过参数注入恶意内容
 */
private void validateParams(JobDTO jobDTO) {
    String params = jobDTO.getParams();
    if (StringUtils.isBlank(params)) return;

    // 1. 检查参数长度
    if (params.length() > 1000) {
        throw new BusinessException("任务参数长度不能超过1000字符");
    }

    // 2. 检查是否包含危险字符
    String[] dangerousPatterns = {
        "Runtime", "ProcessBuilder", "exec",
        "getClass", "forName", "invoke",
        "../", "::", "$$"
    };
    for (String pattern : dangerousPatterns) {
        if (params.contains(pattern)) {
            throw new BusinessException("任务参数包含非法内容");
        }
    }

    // 3. 检查参数是否为合法JSON
    try {
        JsonParser.parseString(params);
    } catch (Exception e) {
        throw new BusinessException("任务参数格式错误,必须为合法JSON");
    }
}

结论与建议

核心要点

  1. 白名单是定时任务安全的核心防线:只允许执行预先审核通过的类和方法,从根本上杜绝代码注入
  2. 双重校验机制:创建时校验 + 执行时校验,防止绕过管理界面直接篡改数据库
  3. 动态缓存管理:白名单变更后实时刷新,集群环境下通过消息通知同步
  4. 纵深防护:白名单 + 参数校验 + 超时控制 + 审计日志,多层安全防护

最佳实践建议

建议 说明
最小权限原则 白名单只添加确实需要定时执行的任务,宁少勿多
定期审计 定期审查白名单,移除不再使用的任务
安全审计日志 所有任务创建、修改、执行操作全量记录
参数白名单 不仅限制类和方法,参数格式也要严格校验
集群同步 多节点部署时确保白名单缓存一致性
数据库权限 限制直接修改任务表的数据库账号权限

相关资源