钉钉远程一键执行服务器启动脚本

一、方案概述

本方案实现钉钉群机器人 + 后端服务 远程控制多台服务器执行启动/停止脚本。

核心能力:

  • 钉钉群发送可点击按钮卡片
  • 点击按钮自动回调后端接口
  • 后端验签、IP白名单校验
  • 远程SSH登录服务器执行脚本
  • 安全、可控、可追溯

二、整体架构流程

  1. 运行Java程序 → 钉钉群发送按钮卡片
  2. 员工点击钉钉按钮 → 请求后端回调接口
  3. 后端校验:IP白名单 + 签名防篡改
  4. 校验通过 → SSH远程登录目标服务器
  5. 执行指定启动脚本 → 返回执行结果

三、环境依赖

  • JDK 8+
  • SpringBoot 2.x
  • 钉钉群机器人(支持加签)
  • 服务器开启SSH端口(默认20)
  • 服务器账号密码/密钥

四、实现步骤

步骤1:创建钉钉机器人并获取密钥

  1. 钉钉群 → 群设置 → 机器人 → 添加机器人 → 选择自定义机器人
  2. 安全设置:加签 ,复制 Webhook地址密钥(SECRET)
  3. 记住:必须开启加签,否则无法使用

步骤2:配置 application.yml

yaml 复制代码
dingtalk:
  url: 加签xxx
  trusted:
    enable: true
    ips: 白名单
  script: 执行脚本如启动脚本

remote:
  server:
    port: 6220
    username: 远程服务器用户
    password: 远程服务器密码

步骤3:编写钉钉消息发送工具类

DingTalkService.java

作用:生成带签名的钉钉卡片,发送到群里

java 复制代码
import com.alibaba.fastjson.JSONObject;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class DingTalkService {

    private static final String WEBHOOK_URL = "WEBHOOK_URLx'x'x ";
    private static final String SECRET = "你的SEC密钥";
    private static final String CALLBACK_DOMAIN = "https://你的域名:7777/task/dingtalk";

    public void sendActionCardMessage() throws Exception {
        String timestamp = String.valueOf(System.currentTimeMillis());
        String sign = sign(timestamp, SECRET);
        String webhookWithSign = WEBHOOK_URL + "&timestamp=" + timestamp + "&sign=" + sign;

        JSONObject cardData = new JSONObject();
        cardData.put("msgtype", "actionCard");

        JSONObject actionCard = new JSONObject();
        actionCard.put("title", "价签服务远程控制面板");
        actionCard.put("text", "### 价签服务一键启动\n支持服务器:242、245、003\n点击按钮执行启动脚本");
        actionCard.put("btnOrientation", "0");

        List<JSONObject> btnList = new ArrayList<>();
        btnList.add(createBtn("242-server", "18.2.0.24.242"));
        btnList.add(createBtn("245-server", "18.2.0.24.245"));
        btnList.add(createBtn("003-server", "18.2.0.24.3"));
        actionCard.put("btns", btnList);
        cardData.put("actionCard", actionCard);

        String response = sendHttpPost(webhookWithSign, cardData.toJSONString());
        System.out.println("发送结果:" + response);
    }

    private JSONObject createBtn(String title, String serverId) {
        JSONObject btn = new JSONObject();
        btn.put("title", title);
        try {
            String timestamp = String.valueOf(System.currentTimeMillis());
            String nonce = UUID.randomUUID().toString().replace("-", "");
            String signStr = timestamp + nonce + serverId;
            String sign = generateCallbackSign(signStr, SECRET);

            String callbackUrl = String.format(
                    "%s/callback?serverId=%s&timestamp=%s&nonce=%s&sign=%s",
                    CALLBACK_DOMAIN,
                    URLEncoder.encode(serverId, "UTF-8"),
                    URLEncoder.encode(timestamp, "UTF-8"),
                    URLEncoder.encode(nonce, "UTF-8"),
                    URLEncoder.encode(sign, "UTF-8")
            );
            btn.put("actionURL", callbackUrl);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return btn;
    }

    private String generateCallbackSign(String signStr, String secret) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signData = mac.doFinal(signStr.getBytes(StandardCharsets.UTF_8));
        return new String(java.util.Base64.getEncoder().encode(signData));
    }

    private String sign(String timestamp, String secret) throws Exception {
        String stringToSign = timestamp + "\n" + secret;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
        return URLEncoder.encode(new String(java.util.Base64.getEncoder().encode(signData)), "UTF-8");
    }

    private String sendHttpPost(String url, String body) throws Exception {
        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            HttpPost post = new HttpPost(url);
            post.setHeader("Content-Type", "application/json;charset=utf-8");
            post.setEntity(new StringEntity(body, StandardCharsets.UTF_8));
            return EntityUtils.toString(httpClient.execute(post).getEntity(), StandardCharsets.UTF_8);
        }
    }

    public static void main(String[] args) {
        try {
            new DingTalkService().sendActionCardMessage();
            System.out.println("发送成功!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

步骤4:编写回调接口(核心)

DingTalkCallbackController.java

作用:接收钉钉回调、验签、IP校验、远程执行脚本

java 复制代码
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/dingtalk")
public class DingTalkCallbackController {
    private static final Logger log = LoggerFactory.getLogger(DingTalkCallbackController.class);

    @Value("${dingtalk.url}")
    private String CALLBACK_SECRET;
    @Value("${dingtalk.trusted.enable:false}")
    private Boolean enable;
    @Value("${dingtalk.trusted.ips:}")
    private String trustedIpsConfig;
    @Value("${dingtalk.script}")
    private String script;
    @Value("${remote.server.port:22}")
    private int remotePort;
    @Value("${remote.server.username}")
    private String remoteUsername;
    @Value("${remote.server.password}")
    private String remotePassword;

    @GetMapping("/callback")
    public String callback(@RequestParam Map<String, String> params, HttpServletRequest request) {
        try {
            String clientIp = getClientIp(request);
            if (enable && !isIpAllowed(clientIp)) {
                return "error: ip not allowed";
            }

            String serverId = params.get("serverId");
            String timestamp = params.get("timestamp");
            String nonce = params.get("nonce");
            String sign = params.get("sign");

            String signStr = timestamp + nonce + serverId;
            String calcSign = generateCallbackSign(signStr, CALLBACK_SECRET);
            if (!calcSign.equals(sign)) {
                return "error: sign invalid";
            }

            log.info("验签通过,执行服务器:{}", serverId);
            return executeScript(serverId);
        } catch (Exception e) {
            log.error("执行异常", e);
            return "error: " + e.getMessage();
        }
    }

    private String executeScript(String serverIp) throws Exception {
        Session session = null;
        ChannelExec channel = null;
        try {
            JSch jsch = new JSch();
            session = jsch.getSession(remoteUsername, serverIp, remotePort);
            session.setPassword(remotePassword);
            session.setConfig("StrictHostKeyChecking", "no");
            session.connect(30000);

            channel = (ChannelExec) session.openChannel("exec");
            channel.setCommand(script);
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(channel.getInputStream(), StandardCharsets.UTF_8));
            channel.connect();

            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line).append("\n");
            }
            return sb.toString();
        } finally {
            if (channel != null) channel.disconnect();
            if (session != null) session.disconnect();
        }
    }

    private String generateCallbackSign(String signStr, String secret) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signData = mac.doFinal(signStr.getBytes(StandardCharsets.UTF_8));
        return new String(java.util.Base64.getEncoder().encode(signData));
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.contains("unknown")) ip = request.getRemoteAddr();
        return ip.split(",")[0].trim();
    }

    private boolean isIpAllowed(String clientIp) {
        if (trustedIpsConfig == null) return true;
        List<String> list = Arrays.asList(trustedIpsConfig.split(","));
        return list.stream().anyMatch(s -> s.trim().equals(clientIp));
    }
}

步骤5:启动服务并发送钉钉卡片

  1. 启动SpringBoot服务
  2. 运行 DingTalkService.java 的 main 方法
  3. 钉钉群收到按钮卡片

步骤6:点击按钮测试

点击按钮 → 后端自动执行脚本 → 返回执行结果

五、安全机制(非常重要)

  1. 钉钉机器人加签:防止伪造消息
  2. 回调接口签名:防止非法调用接口
  3. IP白名单:只允许钉钉服务器/本机访问
  4. SSH账号密码:远程服务器权限控制

六、扩展能力

  • 支持多服务切换(修改script配置)
  • 支持多环境(dev/test/prod)
  • 支持执行结果钉钉回推
  • 支持操作日志记录
相关推荐
不想好好取名字2 小时前
Ubuntu apt启用dbg符号库
linux·运维·ubuntu
同聘云2 小时前
阿里云国际站服务器cdn网络故障的解决方法是什么?
服务器·开发语言·阿里云·php
江湖有缘3 小时前
基于开发者空间部署Eigenfocus项目管理工具【华为开发者空间】
运维·服务器·华为
丶伯爵式3 小时前
Docker 国内镜像加速 | 2026年3月26日可用
运维·docker·容器·镜像加速·国内镜像加速
小陈工4 小时前
Python安全编程实践:常见漏洞与防护措施
运维·开发语言·人工智能·python·安全·django·开源
刚入门的大一新生7 小时前
Linux-Linux的基础指令4
linux·运维·服务器
腾讯蓝鲸智云10 小时前
嘉为蓝鲸可观测系列产品入选Gartner《中国智能IT监控与日志分析工具市场指南》
运维·人工智能·信息可视化·自动化
能不能别报错12 小时前
openclaw-linux部署教程+mimo-v2-pro
linux·运维·服务器
小虎卫远程打卡app13 小时前
光通信与视频编码前沿技术综述:从超大容量传输到实时神经网络编码
运维·网络·信息与通信·视频编解码