手里有个CS架构的老系统,服务端要用SSH的方式传文件。没想到写了两天!遇到一堆问题,于是记录下。(老系统真恶心啊!)
一开始使用原有的jsch包,但是ssh连不上,查资料后发现,服务器的ssh版本升级了,而且中央仓库的jsch包十几年不更新了,缺少很多密钥算法。
改为使用apache的sshd包,我看一直有更新,因为线上服务器的ssh更高,于是用了比较新的版本。而且需要三个包,详细代码后面有。
sshd-core
sshd-sftp
sshd-common (这里和第二个坑有关!)
使用sshd后,本地测试没问题,而放到测试服务器,服务端就一直报找不到sshd下面的ClientSession。
因为是ClassLoader报的,原以为是二次代理,导致的ClassLoader无法获取第二次代理的外部jar(老架构的service全是用代理模式获取的,之前遇到过因二次代理导致service中无法获取hutool的问题)。后来想想这里不存在二次代理的问题,应该就只是ClassLoader获取外部jar包,于是找了下这个类,谁知。。(这里是idea用maven看的,netbean看起来实在麻烦)
core和common都有org.apache.sshd.client包!ClientSession在core里(不理解为啥不都装到core里!)
ClassLoader会先去找上面的common包找org.apache.sshd.client.ClientSession,找不到就不接着找了。
用URLClassLoader去指定加载core里的ClientSession,但是想想太麻烦算了,于是准备把ssh传文件写成脚本,service去调用脚本的方式。
开始写脚本,因为服务器没有go环境,python也有各种问题不想用,就用java写了个脚本。
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.kex.DHGClient;
import org.apache.sshd.client.kex.DHGEXClient;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.kex.BuiltinDHFactories;
import org.apache.sshd.common.kex.KeyExchangeFactory;
import org.apache.sshd.scp.client.ScpClient;
import org.apache.sshd.scp.client.ScpClientCreator;
import java.util.Arrays;
/**
* ssh执行器
*
* @author zilong
* @date 2023/12/14
**/
public class SshExcute {
public static void main(String args[]) {
String username = null;
String password = null;
String ip = null;
String sourcePath = null;
String targetPath = null;
String fileName = null;
if (args[0] != null) {
username = args[0];
}
if (args[1] != null) {
password = args[1];
}
if (args[2] != null) {
ip = args[2];
}
if (args[3] != null) {
sourcePath = args[3];
}
if (args[4] != null) {
targetPath = args[4];
}
if (args[5] != null) {
fileName = args[5];
}
sendFile(
username,
password,
ip,
sourcePath,
targetPath,
fileName
);
}
public static boolean sendFile (
String username, String password, String ip, String sourcePath,
String targetPath, String fileName
) {
Security.addProvider(new BouncyCastleProvider());
int port = 22;
try {
sourcePath = sourcePath + "/";
String osName = System.getProperty("os.name");
if (osName.toUpperCase().contains("WINDOWS")) {
sourcePath = sourcePath.replaceAll("/", "\\\\");
}
String source = sourcePath + fileName;
String target = targetPath + "/" + fileName;
SshClient client = SshClient.setUpDefaultClient();
client.start();
ClientSession session = client.connect(username, ip, port).verify().getSession();
session.addPasswordIdentity(password);
boolean isSuccess = session.auth().verify().isSuccess();
if (isSuccess) {
ScpClientCreator creator = ScpClientCreator.instance();
ScpClient scpClient = creator.createScpClient(session);
System.out.println("Scp beginning...");
scpClient.upload(source, target, ScpClient.Option.Recursive);
System.out.println("Scp finished...");
if (scpClient != null) {
scpClient = null;
}
if (session != null && session.isOpen()) {
session.close();
}
if (client != null && client.isOpen()) {
client.stop();
client.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
}
java.lang.IllegalArgumentException: KeyExchangeFactories not set
at org.apache.sshd.common.util.ValidateUtils.createFormattedException(ValidateUtils.java:213)
at org.apache.sshd.common.util.ValidateUtils.throwIllegalArgumentException(ValidateUtils.java:179)
at org.apache.sshd.common.util.ValidateUtils.checkTrue(ValidateUtils.java:174)
at org.apache.sshd.common.util.ValidateUtils.checkNotNullAndNotEmpty(ValidateUtils.java:80)
at org.apache.sshd.common.helpers.AbstractFactoryManager.checkConfig(AbstractFactoryManager.java:513)
at org.apache.sshd.client.SshClient.checkConfig(SshClient.java:389)
at org.apache.sshd.client.SshClient.start(SshClient.java:450)
at Ss.sendFile(Ss.java:81)
at Ss.main(Ss.java:46)
debug看源码
本地测试是有这些密钥算法的,但是服务器上没有,不知道从哪获取的,手动加上试试
又报错
java.security.NoSuchAlgorithmException: Algorithm ECDH not avaliable
感觉还是得从源头下手,应该是服务器缺少相关环境,于是百度,发现是jre缺少加密算法相关的包导致的,那么手动加下就好了。
The Bouncy Castle Crypto package
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.kex.DHGClient;
import org.apache.sshd.client.kex.DHGEXClient;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.kex.BuiltinDHFactories;
import org.apache.sshd.common.kex.KeyExchangeFactory;
import org.apache.sshd.scp.client.ScpClient;
import org.apache.sshd.scp.client.ScpClientCreator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
import java.util.Arrays;
/**
* ssh执行器
*
* @author zilong
* @date 2023/12/14
**/
public class SshExcute {
public static void main(String args[]) {
String username = null;
String password = null;
String ip = null;
String sourcePath = null;
String targetPath = null;
String fileName = null;
if (args[0] != null) {
username = args[0];
}
if (args[1] != null) {
password = args[1];
}
if (args[2] != null) {
ip = args[2];
}
if (args[3] != null) {
sourcePath = args[3];
}
if (args[4] != null) {
targetPath = args[4];
}
if (args[5] != null) {
fileName = args[5];
}
sendFile(
username,
password,
ip,
sourcePath,
targetPath,
fileName
);
}
public static boolean sendFile (
String username, String password, String ip, String sourcePath,
String targetPath, String fileName
) {
Security.addProvider(new BouncyCastleProvider());
int port = 22;
try {
sourcePath = sourcePath + "/";
String osName = System.getProperty("os.name");
if (osName.toUpperCase().contains("WINDOWS")) {
sourcePath = sourcePath.replaceAll("/", "\\\\");
}
String source = sourcePath + fileName;
String target = targetPath + "/" + fileName;
SshClient client = SshClient.setUpDefaultClient();
client.setKeyExchangeFactories(Arrays.<KeyExchangeFactory>asList(
DHGClient.newFactory(BuiltinDHFactories.ecdhp521),
DHGClient.newFactory(BuiltinDHFactories.ecdhp384),
DHGClient.newFactory(BuiltinDHFactories.ecdhp256),
DHGEXClient.newFactory(BuiltinDHFactories.dhgex256),
DHGClient.newFactory(BuiltinDHFactories.dhg18_512),
DHGClient.newFactory(BuiltinDHFactories.dhg17_512),
DHGClient.newFactory(BuiltinDHFactories.dhg16_512),
DHGClient.newFactory((BuiltinDHFactories.dhg15_512)),
DHGClient.newFactory(BuiltinDHFactories.dhg14_256)));
client.start();
ClientSession session = client.connect(username, ip, port).verify().getSession();
session.addPasswordIdentity(password);
boolean isSuccess = session.auth().verify().isSuccess();
if (isSuccess) {
ScpClientCreator creator = ScpClientCreator.instance();
ScpClient scpClient = creator.createScpClient(session);
System.out.println("Scp beginning...");
scpClient.upload(source, target, ScpClient.Option.Recursive);
System.out.println("Scp finished...");
if (scpClient != null) {
scpClient = null;
}
if (session != null && session.isOpen()) {
session.close();
}
if (client != null && client.isOpen()) {
client.stop();
client.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
}
java运行shell命令
Process p = Runtime
.getRuntime()
.exec("nohup java -Djava.ext.dirs=nosignlib SshExcute " +
"root root 192.168.1.15 /file /file a.txt >ssh.out &");
int rc = p.waitFor();
if (rc != 0) {
InputStream errorStream = p.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
遇到了各种问题,有rc是0,但是没执行的,还有其他错误。
java里执行nohup命令,会卡到那无法退出,并且很多问题可以通过sh运行脚本来解决,于是封装个脚本
# ssh_excute.sh
nohup java -Djava.ext.dirs=nosignlib SshExcute $1 $2 $3 $4 $5 $6 >ssh.out &
echo "excute completed..."
exit 0
对应java代码
1、必须用绝对路径,不然会找不到脚本。
2、必须用数组的构造器。字符串的构造器,底层会因特殊字符,将命令转换错误。
3、必须带 -c,要不然底层会把后面命令按空格截断。
String c = "/xxx/ssh_excute.sh root root 192.168.1.15 /file /file a.txt"
String[] command = {"/bin/sh", "-c", c};
ProcessBuilder pb = new ProcessBuilder(command);
p = pb.start();