1、简介
- JSch是SSH2的纯Java实现。
- 使用JSch 我们可以用编程的方式去ssh远程连接服务器,包括指令执行,端口转发,X11转发,文件传输等等
2、使用
2-1导入依赖
implementation group: 'com.jcraft', name: 'jsch', version: '0.1.54'
2-2 完整实现
功能点
- 本地端口转发、
- 远程端口转发
- 上传文件
- 下载文件
- 发送shell指令
@Data
@AllArgsConstructor
public class SshContextConf {
private String remoteHost; //远程连接服务器地址
private int remotePort = 22; //远程连接服务器端口
private String userName ; // 远程连接的用户名
private String password; //远程连接用户的密码
private String identity = "~/.ssh/id_rsa";
private String passphrase = "";
public SshContextConf(String userName, String remoteHost, String password) {
this.userName = userName;
this.remoteHost = remoteHost;
this.password = password;
}
}
public class SshClient {
private JSch jsch;
private Session session;
private SshContextConf conf;
public SshClient(SshContextConf conf) {
this.conf = conf;
jsch = new JSch();
connect();
}
/**
* 关闭ssh连接
*/
public void close(){
session.disconnect();
}
/**
* 本地端口转发
* @param localPort 被转发的本地端口
* @param remoteHost 转发后的服务器
* @param remoteHostPost 转发后的服务器的端口
*/
public void forwardingL(int localPort, String remoteHost, int remoteHostPost) {
if (session == null)
throw new RuntimeException("please establish ssh connection before forwardingL");
try {
int assinged_port = session.setPortForwardingL(localPort, remoteHost, remoteHostPost);
System.out.println("本地端口转发成功 from localhost:"+assinged_port+" to "+remoteHost+":"+remoteHostPost);
} catch (JSchException e) {
e.printStackTrace();
}
}
/**
* 远程端口转发
* @param remotePort 被转发的远程端口
* @param localHost 转发后的服务器地址
* @param localPort 转发后的服务器的端口
*/
public void forwardingR(int remotePort, String localHost, int localPort){
if (session == null)
throw new RuntimeException("please establish ssh connection before forwardingR");
try {
session.setPortForwardingR(remotePort, localHost, localPort);
System.out.println("远程端口转发成功 from "+conf.getRemoteHost()+":"+remotePort+" to "+localHost+":"+localPort);
} catch (JSchException e) {
e.printStackTrace();
}
}
/**
* 取消已分配的本地端口转发
* @param localPort 被转发的本地端口
*/
public void delForwardingL(int localPort){
try {
session.delPortForwardingL(localPort);
} catch (JSchException e) {
e.printStackTrace();
}
}
/**
* 取消已分配的远程端口转发
* @param remotePort 被转发的远程端口
*/
public void delForwardingR(int remotePort){
try {
session.delPortForwardingR(remotePort);
} catch (JSchException e) {
e.printStackTrace();
}
}
/** 执行指令没有返回结果 */
public void executeExecN(String command) {
executeExec(command,false);
}
/** 执行指令有返回结果 */
public List<String> executeExec(String command) {
return executeExec(command,true);
}
/**
* 执行指令
* @param command 指令
* @param needResult 是否需要返回急指令执行结果
* @return 指令执行结果
*/
public List<String> executeExec(String command,boolean needResult) {
isDisconnect();
List<String> resultLines = null;
ChannelExec execChannel = null;
try {
execChannel = (ChannelExec)session.openChannel("exec");
execChannel.setCommand(command);
execChannel.setErrStream(System.err);
execChannel.connect(10000);
if (needResult)
resultLines = collectResult(execChannel.getInputStream());
} catch (IOException | JSchException e) {
e.printStackTrace();
} finally {
if (execChannel != null) {
try {
execChannel.disconnect();
} catch (Exception e) {
System.out.println("JSch channel disconnect error:"+e);
}
}
}
return resultLines;
}
/**
* 收集脚本执行的结果
* @param input ssh连接通道输入流
* @return 脚本执行的结果
*/
private List<String> collectResult(InputStream input) {
List<String> resultLines = new ArrayList<>();
try {
BufferedReader inputReader = new BufferedReader(new InputStreamReader(input));
String inputLine = null;
while((inputLine = inputReader.readLine()) != null) {
resultLines.add(inputLine);
}
}catch (IOException e){
e.printStackTrace();
} finally {
if (input != null) {
try {
input.close();
} catch (Exception e) {
System.err.println("JSch inputStream close error:"+e);
}
}
}
return resultLines;
}
/**
* 上传文件
* @param lfile 被上传的本地文件
* @param rfile 上传后的服务器保存的位置
*/
public void upload(String lfile,String rfile){
FileInputStream fis=null;
try{
boolean ptimestamp = false;
// exec 'scp -t rfile' remotely
rfile=rfile.replace("'", "'\\''");
rfile="'"+rfile+"'";
String command="scp " + (ptimestamp ? "-p" :"") +" -t "+rfile;
Channel channel=session.openChannel("exec");
((ChannelExec)channel).setCommand(command);
// get I/O streams for remote scp
OutputStream out=channel.getOutputStream();
InputStream in=channel.getInputStream();
channel.connect();
if(checkAck(in)!=0){
return;
}
File _lfile = new File(lfile);
if(ptimestamp){
command="T "+(_lfile.lastModified()/1000)+" 0";
// The access time should be sent here,
// but it is not accessible with JavaAPI ;-<
command+=(" "+(_lfile.lastModified()/1000)+" 0\n");
out.write(command.getBytes()); out.flush();
if(checkAck(in)!=0){
System.exit(0);
}
}
// send "C0644 filesize filename", where filename should not include '/'
long filesize=_lfile.length();
command="C0644 "+filesize+" ";
if(lfile.lastIndexOf('/')>0){
command+=lfile.substring(lfile.lastIndexOf('/')+1);
}
else{
command+=lfile;
}
command+="\n";
out.write(command.getBytes()); out.flush();
if(checkAck(in)!=0){
return;
}
// send a content of lfile
fis=new FileInputStream(lfile);
byte[] buf=new byte[1024];
while(true){
int len=fis.read(buf, 0, buf.length);
if(len<=0) break;
out.write(buf, 0, len); //out.flush();
}
fis.close();
fis=null;
// send '\0'
buf[0]=0; out.write(buf, 0, 1); out.flush();
if(checkAck(in)!=0){
return;
}
out.close();
channel.disconnect();
} catch(Exception e){
e.printStackTrace();
try{
if(fis!=null)
fis.close();
}catch(Exception e1){
e1.printStackTrace();
}
}
}
/**
* 下载文件
* @param source 被下载的文件
* @param destination 下载后本地保存的路径
* @return
*/
public long download(String source, String destination) {
FileOutputStream fileOutputStream = null;
try {
ChannelExec channel = (ChannelExec) session.openChannel("exec");
channel.setCommand("scp -f " + source);
OutputStream out = channel.getOutputStream();
InputStream in = channel.getInputStream();
channel.connect();
byte[] buf = new byte[1024];
//send '\0'
buf[0] = 0;
out.write(buf, 0, 1);
out.flush();
while(true) {
if (checkAck(in) != 'C') {
break;
}
}
//read '644 '
in.read(buf, 0, 4);
long fileSize = 0;
while (true) {
if (in.read(buf, 0, 1) < 0) {
break;
}
if (buf[0] == ' ') {
break;
}
fileSize = fileSize * 10L + (long)(buf[0] - '0');
}
String file = null;
for (int i = 0; ; i++) {
in.read(buf, i, 1);
if (buf[i] == (byte) 0x0a) {
file = new String(buf, 0, i);
break;
}
}
// send '\0'
buf[0] = 0;
out.write(buf, 0, 1);
out.flush();
// read a content of lfile
if (Files.isDirectory(Paths.get(destination))) {
fileOutputStream = new FileOutputStream(destination + File.separator +file);
} else {
fileOutputStream = new FileOutputStream(destination);
}
long sum = 0;
while (true) {
int len = in.read(buf, 0 , buf.length);
if (len <= 0) {
break;
}
sum += len;
if (len >= fileSize) {
fileOutputStream.write(buf, 0, (int)fileSize);
break;
}
fileOutputStream.write(buf, 0, len);
fileSize -= len;
}
return sum;
} catch(Exception e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return -1;
}
private int checkAck(InputStream in) throws IOException{
int b=in.read();
// b may be 0 for success,
// 1 for error,
// 2 for fatal error,
// -1
if(b==0) return b;
if(b==-1) return b;
if(b==1 || b==2){
StringBuffer sb=new StringBuffer();
int c;
do {
c=in.read();
sb.append((char)c);
}
while(c!='\n');
if(b==1){
// error
System.out.print(sb.toString());
}
if(b==2){
// fatal error
System.out.print(sb.toString());
}
}
return b;
}
/** 判断是否断开ssh连接进行重连 */
private void isDisconnect(){
if (!session.isConnected()){
connect();
}
}
/** 建立ssh连接 */
private void connect(){
try {
if (Files.exists(Paths.get(conf.getIdentity()))) {
jsch.addIdentity(conf.getIdentity(), conf.getPassphrase());
}
session = jsch.getSession(conf.getUserName(),conf.getRemoteHost(),conf.getRemotePort());
session.setPassword(conf.getPassword());
session.setConfig("StrictHostKeyChecking", "no"); // 关闭确认提示
session.connect(30000);
} catch (JSchException e) {
e.printStackTrace();
}
}
}
3、应用
3-1 正向代理
- 假如我们的服务器A需要访问服务器B,但是因为某些原因无法直接访问,而服务器B和服务器C之间是可以互相访问的, 那么就可以把服务器C当作跳板机、服务器A与服务器C建立SSH连接,然后把服务器A的端口访问转发到代理服务器C上,代理服务器C又会把请求转发给服务器B、进而就到了访问服务器B的效果
测试代码
SshClient client = new SshClient(new SshContextConf("服务器C用户名", "服务器C的IP地址", "服务器C的密码"));
// 假设本地就是服务器A、将本地端口9997 转发到 服务器B的4040 端口
client.forwardingL(9997,"服务器B",4040);
//client.delForwardingL(9997); 取消本地端口转发
//client.close(); // 关闭ssh连接会话
这时我们访问 本地的9997 端口 就会被转发到 服务器B的4040 端口上面
3-2 反向代理(内网穿透)
- 假设外网的服务B要访问服务器A,但是因为服务器A不是在公网无法直接访问、那么我们可以用一个公网服务器(也就是代理服务器C)进行反向代理到服务器A上面,这样服务器B访问服务器C的请求就会被转发到服务器A上面、
- 这个可以通过服务器A与代理服务器C之间建立SSH连接,然后将代理服务器C的端口转发到服务器A上面
- 基于这个我们就可以达到类似内网穿透的效果、就是本地启动的服务,外网也能访问、比如你本地服务器A启动了一个网站服务比如9997端口,你想让你朋友的电脑也能够访问,那么就可以把本地服务9997端口代理到服务器C上做一个反向代理, 然后把公网服务器的80端口转发到本地服务器A的9997端口上, 这样你朋友直接访问 公网服务器的80端口就相当于 你本地访问9997端口
注意:
代理服务器需要开启端口转发允许:
- 1、vim /etc/ssh/sshd_config
- 2、添加 GatewayPorts yes
测试
- 执行后,你访问 公网IP下的80端口将会被转发到本地的9997端口
SshClient client = new SshClient(new SshContextConf("公网IP用户名", "公网IP地址", "公网IP密码"));
client.forwardingR(80,"127.0.0.1",9997);
//client.delForwardingR(9997); // 取消远程端口转发
//client.close(); 关闭ssh连接会话
3-3 发送指令
// 与服务器建立SSH连接
SshContextConf conf = new SshContextConf("服务器用户名","服务器IP地址","服务器密码");
SshClient sshClient = new SshClient(conf);
// 发送单条命令
List<String> result = sshClient.executeExec("ls");
result.forEach(System.out::println);
// 发送复合命令
/*
每个命令之间用 ; 隔开。
说明:各命令的执行给果,不会影响其它命令的执行。换句话说,各个命令都会执行,但不保证每个命令都执行成功。
每个命令之间用 && 隔开。
说明:若前面的命令执行成功,才会去执行后面的命令。这样可以保证所有的命令执行完毕后,执行过程都是成功的。
每个命令之间用 || 隔开。
说明:|| 是或的意思,只有前面的命令执行失败后才去执行下一条命令,直到执行成功一条命令为止。
*/
sshClient.executeExecN("cd tmp && touch xxx.txt");
sshClient.close();
3-4 上传文件
// 与服务器建立SSH连接
SshContextConf conf = new SshContextConf("服务器用户名","服务器IP地址","服务器密码");
SshClient sshClient = new SshClient(conf);
sshClient.upload("本地文件路径","上传服务器后保存的路径");
sshClient.close();
3-5 下载文件
// 与服务器建立SSH连接
SshContextConf conf = new SshContextConf("服务器用户名","服务器IP地址","服务器密码");
SshClient sshClient = new SshClient(conf);
sshClient.download("服务器文件路径"," 下载后本地保存的路径");
sshClient.close();