lifuquan 2 سال پیش
والد
کامیت
c4afc03c16

+ 6 - 2
common/pom.xml

@@ -5,16 +5,20 @@
     <parent>
         <groupId>com.nokia</groupId>
         <artifactId>hb_springboot_parent</artifactId>
-        <version>1.0</version>
+        <version>1.0-SNAPSHOT</version>
     </parent>
 
     <groupId>com.nokia.common</groupId>
     <artifactId>common</artifactId>
-    <version>1.0</version>
+    <version>1.0-SNAPSHOT</version>
 
     <packaging>jar</packaging>
 
     <dependencies>
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jsch</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>

+ 187 - 0
common/src/main/java/com/nokia/common/dao/SnowFlakeUtil.java

@@ -0,0 +1,187 @@
+package com.nokia.common.dao;
+
+import java.util.Date;
+
+/*
+ * 雪花算法
+ * 
+ * 算法来源: https://blog.csdn.net/jiaomubai/article/details/124385324
+ * 
+ * 雪花算法的原理就是生成一个的 64 位比特位的 long 类型的唯一 id。
+ * 最高 1 位固定值 0,因为生成的 id 是正整数,如果是 1 就是负数了。
+ * 接下来 41 位存储毫秒级时间戳,2^41/(1000*60*60*24*365)=69,大概可以使用 69 年。
+ * 再接下 10 位存储机器码,包括 5 位 datacenterId 和 5 位 workerId。最多可以部署 2^10=1024 台机器。
+ * 最后 12 位存储序列号。同一毫秒时间戳时,通过这个递增的序列号来区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成 2^12=4096 个不重复 id。
+ * 
+ * 高并发分布式环境下生成不重复 id,每秒可生成百万个不重复 id。
+ * 基于时间戳,以及同一时间戳下序列号自增,基本保证 id 有序递增。
+ * 不依赖第三方库或者中间件。
+ * 算法简单,在内存中进行,效率高。
+ */
+public class SnowFlakeUtil {
+    private static SnowFlakeUtil snowFlakeUtil;
+    static {
+        snowFlakeUtil = new SnowFlakeUtil();
+    }
+
+    // 初始时间戳(纪年),可用雪花算法服务上线时间戳的值
+    // 1650789964886:2022-04-24 16:45:59
+    private static final long INIT_EPOCH = 1650789964886L;
+
+    // 时间位取&
+    private static final long TIME_BIT = 0b1111111111111111111111111111111111111111110000000000000000000000L;
+
+    // 记录最后使用的毫秒时间戳,主要用于判断是否同一毫秒,以及用于服务器时钟回拨判断
+    private long lastTimeMillis = -1L;
+
+    // dataCenterId占用的位数
+    private static final long DATA_CENTER_ID_BITS = 5L;
+
+    // dataCenterId占用5个比特位,最大值31
+    // 0000000000000000000000000000000000000000000000000000000000011111
+    private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
+
+    // dataCenterId
+    private long dataCenterId;
+
+    // workId占用的位数
+    private static final long WORKER_ID_BITS = 5L;
+
+    // workId占用5个比特位,最大值31
+    // 0000000000000000000000000000000000000000000000000000000000011111
+    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
+
+    // workId
+    private long workerId;
+
+    // 最后12位,代表每毫秒内可产生最大序列号,即 2^12 - 1 = 4095
+    private static final long SEQUENCE_BITS = 12L;
+
+    // 掩码(最低12位为1,高位都为0),主要用于与自增后的序列号进行位与,如果值为0,则代表自增后的序列号超过了4095
+    // 0000000000000000000000000000000000000000000000000000111111111111
+    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
+
+    // 同一毫秒内的最新序号,最大值可为 2^12 - 1 = 4095
+    private long sequence;
+
+    // workId位需要左移的位数 12
+    private static final long WORK_ID_SHIFT = SEQUENCE_BITS;
+
+    // dataCenterId位需要左移的位数 12+5
+    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
+
+    // 时间戳需要左移的位数 12+5+5
+    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
+
+    /**
+     * 无参构造
+     */
+    public SnowFlakeUtil() {
+        this(1, 1);
+    }
+
+    /**
+     * 有参构造
+     * 
+     * @param dataCenterId
+     * @param workerId
+     */
+    public SnowFlakeUtil(long dataCenterId, long workerId) {
+        // 检查dataCenterId的合法值
+        if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_ID) {
+            throw new IllegalArgumentException(
+                    String.format("dataCenterId 值必须大于 0 并且小于 %d", MAX_DATA_CENTER_ID));
+        }
+        // 检查workId的合法值
+        if (workerId < 0 || workerId > MAX_WORKER_ID) {
+            throw new IllegalArgumentException(String.format("workId 值必须大于 0 并且小于 %d", MAX_WORKER_ID));
+        }
+        this.workerId = workerId;
+        this.dataCenterId = dataCenterId;
+    }
+
+    /**
+     * 获取唯一ID
+     * 
+     * @return
+     */
+    public static Long getSnowFlakeId() {
+        return snowFlakeUtil.nextId();
+    }
+
+    /**
+     * 通过雪花算法生成下一个id,注意这里使用synchronized同步
+     * 
+     * @return 唯一id
+     */
+    public synchronized long nextId() {
+        long currentTimeMillis = System.currentTimeMillis();
+        System.out.println(currentTimeMillis);
+        // 当前时间小于上一次生成id使用的时间,可能出现服务器时钟回拨问题
+        if (currentTimeMillis < lastTimeMillis) {
+            throw new RuntimeException(
+                    String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,
+                            lastTimeMillis));
+        }
+        if (currentTimeMillis == lastTimeMillis) {
+            // 还是在同一毫秒内,则将序列号递增1,序列号最大值为4095
+            // 序列号的最大值是4095,使用掩码(最低12位为1,高位都为0)进行位与运行后如果值为0,则自增后的序列号超过了4095
+            // 那么就使用新的时间戳
+            sequence = (sequence + 1) & SEQUENCE_MASK;
+            if (sequence == 0) {
+                currentTimeMillis = getNextMillis(lastTimeMillis);
+            }
+        } else { // 不在同一毫秒内,则序列号重新从0开始,序列号最大值为4095
+            sequence = 0;
+        }
+        // 记录最后一次使用的毫秒时间戳
+        lastTimeMillis = currentTimeMillis;
+        // 核心算法,将不同部分的数值移动到指定的位置,然后进行或运行
+        // <<:左移运算符, 1 << 2 即将二进制的 1 扩大 2^2 倍
+        // |:位或运算符, 是把某两个数中, 只要其中一个的某一位为1, 则结果的该位就为1
+        // 优先级:<< > |
+        return
+        // 时间戳部分
+        ((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)
+                // 数据中心部分
+                | (dataCenterId << DATA_CENTER_ID_SHIFT)
+                // 机器表示部分
+                | (workerId << WORK_ID_SHIFT)
+                // 序列号部分
+                | sequence;
+    }
+
+    /**
+     * 获取指定时间戳的接下来的时间戳,也可以说是下一毫秒
+     * 
+     * @param lastTimeMillis 指定毫秒时间戳
+     * @return 时间戳
+     */
+    private long getNextMillis(long lastTimeMillis) {
+        long currentTimeMillis = System.currentTimeMillis();
+        while (currentTimeMillis <= lastTimeMillis) {
+            currentTimeMillis = System.currentTimeMillis();
+        }
+        return currentTimeMillis;
+    }
+
+    /**
+     * 获取随机字符串,length=13
+     * 
+     * @return
+     */
+    public static String getRandomStr() {
+        return Long.toString(getSnowFlakeId(), Character.MAX_RADIX);
+    }
+
+    /**
+     * 从ID中获取时间
+     * 
+     * @param id 由此类生成的ID
+     * @return
+     */
+    public static Date getTimeBySnowFlakeId(long id) {
+        return new Date(((TIME_BIT & id) >> 22) + INIT_EPOCH);
+    }
+
+}

+ 15 - 0
common/src/main/java/com/nokia/common/dao/annotation/IsTableField.java

@@ -0,0 +1,15 @@
+package com.nokia.common.dao.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/*
+ * 是否为数据库表字段
+ * 
+ * 仅用于标识,不处理
+ */
+// 仅用于类的成员变量
+@Target(ElementType.FIELD)
+public @interface IsTableField {
+    boolean value() default true;
+}

+ 3 - 1
common/src/main/java/com/nokia/common/mvc/interceptor/ControllerLogInterceptor.java

@@ -2,6 +2,8 @@ package com.nokia.common.mvc.interceptor;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.lang.Nullable;
 import org.springframework.util.StopWatch;
 import org.springframework.web.servlet.HandlerInterceptor;
 import lombok.extern.slf4j.Slf4j;
@@ -27,7 +29,7 @@ public class ControllerLogInterceptor implements HandlerInterceptor {
 
     @Override
     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
-            Exception ex) {
+            @Nullable Exception ex) {
         watch.stop();
         log.debug("完成请求,耗时 {} ms", watch.getLastTaskTimeMillis());
     }

+ 65 - 0
common/src/main/java/com/nokia/common/security/AESUtil.java

@@ -0,0 +1,65 @@
+package com.nokia.common.security;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.tomcat.util.codec.binary.Base64;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class AESUtil {
+    // 加密
+    public static String Encrypt(String sSrc, String sKey) throws Exception {
+        if (sKey == null) {
+            System.out.print("Key为空null");
+            return null;
+        }
+        // 判断Key是否为16位
+        if (sKey.length() != 16) {
+            System.out.print("Key长度不是16位");
+            return null;
+        }
+        byte[] raw = sKey.getBytes("utf-8");
+        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
+        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");// "算法/模式/补码方式"
+        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
+        byte[] encrypted = cipher.doFinal(sSrc.getBytes("utf-8"));
+
+        return new Base64().encodeToString(encrypted);// 此处使用BASE64做转码功能,同时能起到2次加密的作用。
+    }
+
+    // 解密
+    public static String Decrypt(String sSrc, String sKey) throws Exception {
+        try {
+            // 判断Key是否正确
+            if (sKey == null) {
+                System.out.print("Key为空null");
+                return null;
+            }
+            // 判断Key是否为16位
+            if (sKey.length() != 16) {
+                System.out.print("Key长度不是16位");
+                return null;
+            }
+            byte[] raw = sKey.getBytes("utf-8");
+            SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
+            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+            cipher.init(Cipher.DECRYPT_MODE, skeySpec);
+            byte[] encrypted1 = new Base64().decode(sSrc);// 先用base64解密
+            try {
+                byte[] original = cipher.doFinal(encrypted1);
+                String originalString = new String(original, "utf-8");
+                return originalString;
+            } catch (Exception e) {
+                log.error("AESUtil解密出错: {}", e.toString());
+                e.printStackTrace();
+                return null;
+            }
+        } catch (Exception e) {
+            log.error("AESUtil解密出错: {}", e.toString());
+            e.printStackTrace();
+            return null;
+        }
+    }
+}

+ 5 - 0
common/src/main/java/com/nokia/common/security/MD5Util.java

@@ -0,0 +1,5 @@
+package com.nokia.common.security;
+
+public class MD5Util {
+    
+}

+ 376 - 0
common/src/main/java/com/nokia/common/ssh/SSHUtil.java

@@ -0,0 +1,376 @@
+package com.nokia.common.ssh;
+
+import com.jcraft.jsch.*;
+import com.nokia.common.ssh.entity.SSHServer;
+import com.nokia.common.ssh.entity.UserInfoImpl;
+import com.nokia.common.ssh.exception.SSHUtilException;
+import com.nokia.common.ssh.exception.ScpAckErrorException;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.Vector;
+
+/**
+ * 使用jsch库实现的ssh的工具类
+ * <p>
+ * todo: scpTo和scpFrom 在本机和targetServer默认编码不一致的时候,文件名中的中文会乱码,但是不会影响到文件内容,
+ */
+
+@Slf4j
+public class SSHUtil {
+
+    @Getter
+    @Setter
+    private SSHServer targetServer = new SSHServer();
+    private Session session = null;
+    private Channel channel = null;
+    private JSch jSch = null;
+    private FileInputStream fileInputStream = null;
+    private FileOutputStream fileOutputStream = null;
+    private OutputStream outputStream = null;
+    private InputStream inputStream = null;
+    private ChannelSftp channelSftp = null;
+
+    public SSHUtil() {
+    }
+
+    public SSHUtil(String host, String user, String password) {
+        targetServer = new SSHServer(host, 22, user, password);
+    }
+
+    public SSHUtil(String host, Integer port, String user, String password) {
+        targetServer = new SSHServer(host, port, user, password);
+    }
+
+    /**
+     * 获取文件列表
+     */
+    @SuppressWarnings("rawtypes")
+    public List<String> ls(String path) throws JSchException, SftpException {
+        session = getConnectSession();
+        channelSftp = (ChannelSftp) session.openChannel("sftp");
+        channelSftp.connect();
+        List<String> fileNameList = new ArrayList<>();
+        // 这里 jsch 源代码 直接使用了未指定类型的泛型
+        Vector fileList = channelSftp.ls(path);
+        for (Object o : fileList) {
+            String fileName = ((ChannelSftp.LsEntry) o).getFilename();
+            if (".".equals(fileName) || "..".equals(fileName)) {
+                continue;
+            }
+            fileNameList.add(fileName);
+            channelSftp.quit();
+            session.disconnect();
+        }
+
+        return fileNameList;
+    }
+
+    /**
+     * 下载文件
+     */
+    public void get(String src, String dst) throws JSchException, SftpException, IOException {
+        session = getConnectSession();
+        channelSftp = (ChannelSftp) session.openChannel("sftp");
+        channelSftp.connect();
+        try (OutputStream out = Files.newOutputStream(Paths.get(dst))) {
+            channelSftp.get(src, out);
+        } finally {
+            channelSftp.quit();
+            session.disconnect();
+        }
+    }
+
+    /**
+     * 删除文件
+     */
+    public void rm(String path) throws JSchException, SftpException {
+        session = getConnectSession();
+        channelSftp = (ChannelSftp) session.openChannel("sftp");
+        channelSftp.connect();
+        channelSftp.rm(path);
+        channelSftp.quit();
+        session.disconnect();
+    }
+
+    /**
+     * 远程执行指令
+     */
+    public String exec(String command) throws JSchException, IOException {
+        StringBuilder stringBuilder = new StringBuilder();
+        try {
+            session = getConnectSession();
+            channel = session.openChannel("exec");
+            // jsch的登陆是无环境登陆即非login状态登陆,因此是没有环境变量的,
+            String execCommand;
+            // 在命令前添加 bash --login -c "command"以获取环境变量
+            // source .bashrc && command 也可以解决问题, 但是可能环境加载不全
+            if (command.startsWith("bash --login -c")) {
+                execCommand = command;
+            } else {
+                execCommand = String.format("bash --login -c \"%s\"", command);
+            }
+            ((ChannelExec) channel).setCommand(execCommand);
+            channel.setInputStream(null);
+            ((ChannelExec) channel).setErrStream(System.err);
+            InputStream in = channel.getInputStream();
+            channel.connect();
+            byte[] tmp = new byte[1024];
+            while (true) {
+                while (in.available() > 0) {
+                    int i = in.read(tmp, 0, 1024);
+                    if (i < 0) {
+                        break;
+                    }
+                    stringBuilder.append(new String(tmp, 0, i));
+                }
+                if (channel.isClosed()) {
+                    if (in.available() > 0) {
+                        continue;
+                    }
+                    break;
+                }
+            }
+        } finally {
+            disconnect();
+        }
+        return stringBuilder.toString();
+    }
+
+    /**
+     * 使用SCP把本地文件推送到targetServer目录下
+     * <p>
+     * 注意,文件名不能包含中文
+     */
+    public boolean scpTo(String sourceFilePath, String targetPath) throws JSchException, IOException, SSHUtilException {
+        try {
+            session = getConnectSession();
+            // scp内置了两个参数 -t 和 -f ,这两个参数是隐藏的,不会被用户显式提供,
+            // 两个scp进程之间传输数据时,远端机器上的scp进程被本地scp进程启动起来时提供上去。
+            // 需要说明的是,这是通过本地scp进程经ssh远程过去开启远端机器的scp进程来实现的。
+            // -t 指定为to 也就是目的端模式 指定的对象就是session对应的连接对象targetServer
+            String command = "scp " + "-t " + targetPath;
+            channel = session.openChannel("exec");
+            ((ChannelExec) channel).setCommand(command);
+            outputStream = channel.getOutputStream();
+            inputStream = channel.getInputStream();
+            channel.connect();
+            if (checkAck(inputStream) != 0) {
+                log.error("scpTo 执行失败");
+                return false;
+            }
+            File sourceFile = new File(sourceFilePath);
+            if (sourceFile.isDirectory()) {
+                log.error("sourceFilePath 必须是文件");
+                return false;
+            }
+            long fileSize = sourceFile.length();
+            command = "C0644 " + fileSize + " " + sourceFile.getName() + "\n";
+            outputStream.write(command.getBytes());
+            outputStream.flush();
+            if (checkAck(inputStream) != 0) {
+                log.error("scpTo 执行失败");
+                return false;
+            }
+            fileInputStream = new FileInputStream(sourceFile);
+            byte[] buffer = new byte[1024];
+            while (true) {
+                int len = fileInputStream.read(buffer, 0, buffer.length);
+                if (len <= 0) {
+                    break;
+                }
+                outputStream.write(buffer, 0, len);
+            }
+            buffer[0] = 0;
+            outputStream.write(buffer, 0, 1);
+            outputStream.flush();
+            return checkAck(inputStream) == 0;
+        } finally {
+            disconnect();
+        }
+    }
+
+    /**
+     * 使用scp把targetServer目录下的文件复制到本地
+     */
+    public boolean scpFrom(String sourceFilePath, String targetPath)
+            throws JSchException, IOException, SSHUtilException {
+        try {
+            log.info(sourceFilePath);
+            session = getConnectSession();
+            // scp内置了两个参数 -t 和 -f ,这两个参数是隐藏的,不会被用户显式提供,
+            // 两个scp进程之间传输数据时,远端机器上的scp进程被本地scp进程启动起来时提供上去。
+            // 需要说明的是,这是通过本地scp进程经ssh远程过去开启远端机器的scp进程来实现的。
+            // -f 指定对端为from 也就是源端模式 指定的对象就是session对应的连接对象targetServer
+            String command = "scp -f " + sourceFilePath;
+            Channel channel = session.openChannel("exec");
+            ((ChannelExec) channel).setCommand(command);
+            outputStream = channel.getOutputStream();
+            inputStream = channel.getInputStream();
+            channel.connect();
+            byte[] buf = new byte[1024];
+            // 发送指令 '0'
+            // 源端会一直等宿端的回应, 直到等到回应才会传输下一条协议文本.
+            // 在送出最后一条协议文本后, 源端会传出一个大小为零的字符'0'来表示真正文件传输的开始.
+            // 当文件接收完成后, 宿端会给源端发送一个'0'
+            buf[0] = 0;
+            outputStream.write(buf, 0, 1);
+            outputStream.flush();
+            // 接收C0644 这条消息携带了文件的信息
+            while (true) {
+                int c = checkAck(inputStream);
+                // 遇到C时跳出循环
+                if (c == 'C') {
+                    break;
+                }
+            }
+            // 接收 '0644 ' 这段字符表示文件的权限
+            inputStream.read(buf, 0, 5);
+            // 获取filesize
+            long filesize = 0L;
+            while (true) {
+                if (inputStream.read(buf, 0, 1) < 0) {
+                    break;
+                }
+                if (buf[0] == ' ') {
+                    break;
+                }
+                filesize = filesize * 10L + (long) (buf[0] - '0');
+            }
+            // 从 C0644命令读取文件名,命令中的文件名是不带路径的
+            String file = null;
+            for (int i = 0;; i++) {
+                inputStream.read(buf, i, 1);
+                // 0x0a 是LF 换行符
+                if (buf[i] == (byte) 0x0a) {
+                    file = new String(buf, 0, i);
+                    break;
+                }
+            }
+            log.info("filesize={}, file={}", filesize, file);
+            // 发送 '0'
+            buf[0] = 0;
+            outputStream.write(buf, 0, 1);
+            outputStream.flush();
+            // 如果目标是目录,则需要加上文件名
+            File target = new File(targetPath);
+            if (target.isDirectory()) {
+                log.info("{} 是目录,需要添加文件名", target.getAbsolutePath());
+                target = new File(targetPath + File.separator + file);
+            }
+
+            fileOutputStream = new FileOutputStream(target);
+            int foo;
+            while (true) {
+                if (buf.length < filesize) {
+                    foo = buf.length;
+                } else {
+                    foo = (int) filesize;
+                }
+                foo = inputStream.read(buf, 0, foo);
+                if (foo < 0) {
+                    break;
+                }
+                fileOutputStream.write(buf, 0, foo);
+                filesize -= foo;
+                if (filesize == 0L) {
+                    break;
+                }
+            }
+            if (checkAck(inputStream) != 0) {
+                return false;
+            }
+            // 发送 '0'
+            buf[0] = 0;
+            outputStream.write(buf, 0, 1);
+            outputStream.flush();
+            log.info("scp from {}@{}:{}{} to {} 完成", targetServer.getUser(), targetServer.getHost(),
+                    targetServer.getPort(), sourceFilePath, target.getAbsolutePath());
+            return true;
+        } finally {
+            disconnect();
+        }
+    }
+
+    private Session getConnectSession() throws JSchException {
+        jSch = new JSch();
+        session = jSch.getSession(targetServer.getUser(), targetServer.getHost(), targetServer.getPort());
+        session.setPassword(targetServer.getPassword());
+        session.setUserInfo(new UserInfoImpl());
+        // 不需要输入保存ssh安全密钥的yes或no
+        Properties properties = new Properties();
+        properties.put("StrictHostKeyChecking", "no");
+        session.setConfig(properties);
+        session.connect();
+        log.info("已连接到{}@{}:{}", targetServer.getUser(), targetServer.getHost(), targetServer.getPort());
+        return session;
+    }
+
+    private void disconnect() throws IOException {
+        if (fileOutputStream != null) {
+            fileOutputStream.close();
+            fileOutputStream = null;
+        }
+        if (fileInputStream != null) {
+            fileInputStream.close();
+            fileInputStream = null;
+        }
+        if (outputStream != null) {
+            outputStream.close();
+            outputStream = null;
+        }
+        if (channel != null) {
+            channel.disconnect();
+            channel = null;
+        }
+        if (session != null) {
+            session.disconnect();
+            session = null;
+        }
+        jSch = null;
+    }
+
+    /**
+     * 来自源端的每条消息和每个传输完毕的文件都需要宿端的确认和响应.
+     * 宿端会返回三种确认消息: 0(正常), 1(警告)或2(严重错误, 将中断连接).
+     * 消息1和2可以跟一个字符串和一个换行符, 这个字符串将显示在scp的源端. 无论这个字符串是否为空, 换行符都是不可缺少的.
+     */
+    private static int checkAck(InputStream in) throws IOException, SSHUtilException {
+        int b = in.read();
+        // b 取值为0表示成功
+        if (b == 0) {
+            return b;
+        }
+        if (b == -1) {
+            return b;
+        }
+
+        // 1表示警告 2表示严重错误,将中断连接
+        // 1和2 后面会携带一条错误信息,以\n结尾
+        if (b == 1 || b == 2) {
+            // 打印消息后面跟的字符串
+            StringBuilder sb = new StringBuilder();
+            int c;
+            do {
+                // 读取字符串直到遇到换行符
+                c = in.read();
+                sb.append((char) c);
+            } while (c != '\n');
+            log.info("checkAck发现错误消息: ack={}-msg={}", b, sb);
+            if (b == 1 && sb.toString().endsWith("No such file or directory")) {
+                throw new NoSuchFileException(sb.toString());
+            } else {
+                throw new ScpAckErrorException(sb.toString());
+            }
+        }
+        return b;
+    }
+}

+ 15 - 0
common/src/main/java/com/nokia/common/ssh/entity/SSHServer.java

@@ -0,0 +1,15 @@
+package com.nokia.common.ssh.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class SSHServer {
+    private String host;
+    private int port = 22;
+    private String user;
+    private String password;
+}

+ 35 - 0
common/src/main/java/com/nokia/common/ssh/entity/UserInfoImpl.java

@@ -0,0 +1,35 @@
+package com.nokia.common.ssh.entity;
+
+import com.jcraft.jsch.UserInfo;
+
+public class UserInfoImpl implements UserInfo {
+    @Override
+    public String getPassphrase() {
+        return null;
+    }
+
+    @Override
+    public String getPassword() {
+        return null;
+    }
+
+    @Override
+    public boolean promptPassword(String s) {
+        return false;
+    }
+
+    @Override
+    public boolean promptPassphrase(String s) {
+        return false;
+    }
+
+    @Override
+    public boolean promptYesNo(String s) {
+        return false;
+    }
+
+    @Override
+    public void showMessage(String s) {
+
+    }
+}

+ 8 - 0
common/src/main/java/com/nokia/common/ssh/exception/SSHUtilException.java

@@ -0,0 +1,8 @@
+package com.nokia.common.ssh.exception;
+
+public class SSHUtilException extends Exception{
+
+    public SSHUtilException(String message) {
+        super(message);
+    }
+}

+ 7 - 0
common/src/main/java/com/nokia/common/ssh/exception/ScpAckErrorException.java

@@ -0,0 +1,7 @@
+package com.nokia.common.ssh.exception;
+
+public class ScpAckErrorException extends SSHUtilException {
+    public ScpAckErrorException(String message) {
+        super(message);
+    }
+}

+ 8 - 2
pom.xml

@@ -10,7 +10,7 @@
 
     <groupId>com.nokia</groupId>
     <artifactId>hb_springboot_parent</artifactId>
-    <version>1.0</version>
+    <version>1.0-SNAPSHOT</version>
 
     <packaging>pom</packaging>
     <modules>
@@ -31,7 +31,13 @@
             <dependency>
                 <groupId>com.nokia.common</groupId>
                 <artifactId>common</artifactId>
-                <version>1.0</version>
+                <version>1.0-SNAPSHOT</version>
+            </dependency>
+            <!-- jsch -->
+            <dependency>
+                <groupId>com.jcraft</groupId>
+                <artifactId>jsch</artifactId>
+                <version>0.1.55</version>
             </dependency>
             <!-- 项目的子模块版本结束 -->
             <!-- https://mvnrepository.com/artifact/de.siegmar/fastcsv -->