使用JSch實現ssh隧道建立
前言:
本篇文章記錄我近期研究的問題:如何利用java實現堡壘機與內部機器建立隧道問題。
問題情景描述:
在生產環境中的叢集往往在一個區域網中,而該區域網只能通過某臺特定的堡壘機來訪問。
即:為了更加安全,所以線上的伺服器都無法直接訪問,它必須通過一臺堡壘機來訪問。示意如下:
使用者想直接訪問內部伺服器,但是這些內部伺服器並沒有外網。 而堡壘機和這些伺服器在一個區域網中,堡壘機可以和內部伺服器通訊,同時堡壘機擁有外網,可以直接被使用者訪問到。那麼我們便可以先由ssh到堡壘機,然後再ssh到內部伺服器才能夠訪問。這樣做自然可以減少攻擊,但是每次要到內部機器上去執行命令,都需要經歷2次ssh,對線上的除錯與監控效率影響非常大。下面就來全面介紹一下用java如何來解決該問題。
通過ProxyCommand+Netcat
一、前提條件 流程介紹
- 本機、跳板機、目標機器(內部伺服器)三者都需要已經做過公鑰認證。
-
tip:如果不做祕鑰認證就會提示分別輸入跳板機和目標機器的密碼,需要輸入兩次密碼,非常繁瑣。而且要利用java做互動式命令輸入。這個問題最終我找到解決的辦法,不需要手動進行互動式命令輸入,Jsch中UIKeyboardInteractive 能夠進行賦值,解決這個問題。
-
利用java做公鑰認證的方式,暫時我沒有解決掉。我改為上面的 利用命令做互動式賦值來解決的。
- linux伺服器安裝Netcat
- 以CentOS Linux 為例:
yum install nc
- 配置本機ssh config
-
執行命令:
vim ~/.ssh/config
-
內容
vim ~/.ssh/config
如下:
Host foo #目標主機(內網伺服器)別名,也可寫成目標伺服器IP,可使用萬用字元,如:Host 10.208.* HostName 192.168.0.11 #目標機域名或IP地址 User root #SSH使用者名稱 Port 22 #SSH埠 ProxyCommand ssh -q -p 22 [email protected] nc %h %p #這地方的使用者名稱, ip 是指的 堡壘機的 IdentityFile ~/.ssh/id_rsa #登陸堡壘機的私鑰所在位置,如預設位置可不用顯示指定
4、原理
通過ProxyCommand ,可以在開啟ssh之前執行一個命令開啟代理隧道,這個命令 nc %h % p
是在堡壘機上使用nc開啟了遠端隧道。
ProxyCommand 引數 中的-q 是為了防止和堡壘機的ssh連線產生多餘的輸出,比如 不加 -q 就會導致每次斷開連線時會多一句 killed by signal 1
。
二、程式碼搬上來:
清單一 :gradle專案中匯入使用的包
// ssh
compile 'com.jcraft:jsch:0.1.54'
// google json
compile 'com.google.code.gson:gson:2.8.5'
注意:如果你們使用的是maven構建的專案,那就去maven官網中去找對應的包,引入。
清單二:MyUserInfo.java 使用者資訊類
實現 Jsch包中的UserInfo,UIKeyboardInteractive,用來存使用者資訊,以及進行互動式命令的賦值。
注意:promptYesNo()方法,要手動改為true,只用這樣,在執行的時候才能,賦值為yes,便不會再提示輸入跳板機和目標機器的密碼。
import com.jcraft.jsch.UIKeyboardInteractive;
import com.jcraft.jsch.UserInfo;
/**
* @author wangchunlan
* @Description
* @date 2018/10/12 14:46
**/
public abstract class MyUserInfo implements UserInfo,UIKeyboardInteractive {
@Override
public String[] promptKeyboardInteractive(String destination, String name, String instruction, String[] prompt, boolean[] echo) {
return new String[0];
}
@Override
public String getPassphrase() {
return null;
}
@Override
public String getPassword() {
return null;
}
@Override
public boolean promptPassword(String message) {
return false;
}
@Override
public boolean promptPassphrase(String message) {
return false;
}
@Override
public boolean promptYesNo(String message) {
// 注意此處改為true
return true;
}
@Override
public void showMessage(String message) {
}
}
清單三、 SSHInfo.java 堡壘機與目標機器的常用屬性封裝
注意:
1、我在setCommandOutput()方法中做了修改,添加了一句reader = new BufferedReader(new InputStreamReader(commandOutput));。
2、 SSHInfo()構造方法, 建立了物件:this.ssh =new JSch();
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* 跳板機 與目標機器的 常用屬性 封裝
* @author wangchunlan
* @Description
* @date 2018/10/12 14:52
**/
public class SSHInfo {
private Session session;
private JSch ssh;
// 目標機器
private String targer_username;
private String targer_password;
private String targer_host;
// 堡壘機
private String jump_username;
private String jump_password;
private String jump_host;
private int port = 22;
private InputStream commandOutput;
private BufferedReader reader;
private Channel channel;
private boolean ready;
public SSHInfo(){
}
public SSHInfo(String targer_username, String targer_password, String targer_host, String jump_username, String jump_password, String jump_host, int port) {
this.ssh =new JSch();
this.targer_username = targer_username;
this.targer_password = targer_password;
this.targer_host = targer_host;
this.jump_username = jump_username;
this.jump_password = jump_password;
this.jump_host = jump_host;
this.port = port;
}
public SSHInfo(String targer_username, String targer_password, String targer_host, String jump_username, String jump_password, String jump_host, int port, InputStream commandOutput) {
this.ssh =new JSch();
this.targer_username = targer_username;
this.targer_password = targer_password;
this.targer_host = targer_host;
this.jump_username = jump_username;
this.jump_password = jump_password;
this.jump_host = jump_host;
this.port = port;
this.commandOutput = commandOutput;
}
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
public JSch getSsh() {
return ssh;
}
public void setSsh(JSch ssh) {
this.ssh = ssh;
}
public String getTarger_username() {
return targer_username;
}
public void setTarger_username(String targer_username) {
this.targer_username = targer_username;
}
public String getTarger_password() {
return targer_password;
}
public void setTarger_password(String targer_password) {
this.targer_password = targer_password;
}
public String getTarger_host() {
return targer_host;
}
public void setTarger_host(String targer_host) {
this.targer_host = targer_host;
}
public String getJump_username() {
return jump_username;
}
public void setJump_username(String jump_username) {
this.jump_username = jump_username;
}
public String getJump_password() {
return jump_password;
}
public void setJump_password(String jump_password) {
this.jump_password = jump_password;
}
public String getJump_host() {
return jump_host;
}
public void setJump_host(String jump_host) {
this.jump_host = jump_host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public InputStream getCommandOutput() {
return commandOutput;
}
public void setCommandOutput(InputStream commandOutput) {
this.commandOutput = commandOutput;
reader = new BufferedReader(new InputStreamReader(commandOutput));
}
public BufferedReader getReader() {
return reader;
}
public void setReader(BufferedReader reader) {
this.reader = reader;
}
public Channel getChannel() {
return channel;
}
public void setChannel(Channel channel) {
this.channel = channel;
}
public boolean isReady() {
return ready;
}
public void setReady(boolean ready) {
this.ready = ready;
}
}
清單四、SSHConnection.java 連結工具類
import com.jcraft.jsch.*;
import top.smartpos.itom.utils.LogUtils;
import java.io.File;
import java.util.List;
/**
* SSH連結工具類
* @author wangchunlan
* @Description
* @date 2018/10/12 15:43
**/
public class SSHConnection {
// private SSHInfo sshInfo=new SSHInfo();
private SSHInfo sshInfo=new SSHInfo("root","targer_password","192.168.0.11","root","jump_password","192.168.0.85",22);
public boolean connect(){
try {
String config=config(sshInfo.getPort(),sshInfo.getTarger_username(),sshInfo.getTarger_host(),sshInfo.getJump_username(),sshInfo.getJump_host());
System.out.println(config);
ConfigRepository configRepository=OpenSSHConfig.parse(config);
sshInfo.getSsh().setConfigRepository(configRepository);
Session session=sshInfo.getSsh().getSession("foo");
session.setPassword(sshInfo.getTarger_password());
session.setUserInfo(new MyUserInfo() {});
session.connect(30000);
sshInfo.setSession(session);
sshInfo.setReady(true);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public String write(String command) {
try {
sshInfo.setChannel(sshInfo.getSession().openChannel("exec"));
Channel channel= sshInfo.getChannel();
((ChannelExec) channel).setCommand(command);
sshInfo.setCommandOutput(channel.getInputStream());
channel.connect(3000);
StringBuilder sBuilder = new StringBuilder();
String lido = sshInfo.getReader().readLine();
while (lido != null) {
sBuilder.append(lido);
sBuilder.append("\n");
lido = sshInfo.getReader().readLine();
}
System.out.println("The remote command is: " + command);
return sBuilder.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 斷開 通道和會話
public void close() {
if (sshInfo.getChannel() != null)
sshInfo.getChannel().disconnect();
if (sshInfo.getSession() != null)
sshInfo.getSession() .disconnect();
sshInfo.setReady(false);
}
public String config(int port,String targer_username,String targer_host,String jump_username,String jump_host){
// todo :foo 要改為final 常量
String bastion=jump_username+"@"+jump_host+":"+port;
String config="";
config=
"Port "+port+"\n"+
"\n"+
"Host foo"+"\n"+
" User "+targer_username+"\n"+
" Hostname "+targer_host+"\n"+
" ProxyJump "+bastion+"\n"+
"Host *\n"+
" ConnectTime 30000\n"+
" PreferredAuthentications keyboard-interactive,password,publickey\n"+
" #ForwardAgent yes\n"+
" #StrictHostKeyChecking no\n"+
" #IdentityFile ~/.ssh/id_rsa\n"+ //登陸跳板機的私鑰所在位置,如預設位置可不用顯示指定
" #UserKnownHostsFile ~/.ssh/known_hosts";
return config;
}
/**
* 上傳檔案
* @param sourceFile 本地路徑
* @param dirDestino 上傳檔案絕對路徑 如:/root/kvm2.xml
* @return
*/
/**
* 上傳檔案
* @param sourceFile 本地檔案絕對路徑 如:c:/kvm.xml
* @param targetDirFileLocation 上傳檔案所在目錄 如:/root/
* @return
*/
public boolean upload(String sourceFile,String targetDirFileLocation) {
try {
File origem_ = new File(sourceFile);
targetDirFileLocation = targetDirFileLocation.replace(" ", "_");
String targetFile = targetDirFileLocation.concat("/").concat(origem_.getName());
return upload(sourceFile, targetFile, targetDirFileLocation);
} catch (Exception e) {
throw new SSHException(e);
}
}
/**
* 上傳檔案
* @param sourceFile 本地檔案絕對路徑 如:c:/kvm.xml
* @param targetFile 目標檔案絕對路徑 如:/root/kvm2.xml
* @param targetDirFileLocation 上傳檔案所在目錄 如:/root/
* @return
*/
public boolean upload(String sourceFile,String targetFile,String targetDirFileLocation) {
try {
ChannelSftp sftp = (ChannelSftp) sshInfo.getSession().openChannel("sftp");
sftp.connect();
targetDirFileLocation = targetDirFileLocation.replace(" ", "_");
sftp.cd(targetDirFileLocation);
sftp.put(sourceFile, targetFile);
sftp.disconnect();
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 下載檔案
* @param sourceFile 下載檔案絕對路徑名稱 如:/root/kvm2.xml
* @param targetFile 下載檔案目標位置絕對路徑名稱 如:C:\Users\kvm2.xml
* @return
*/
public boolean download(String sourceFile, String targetFile){
try {
ChannelSftp sftp = (ChannelSftp) sshInfo.getSession().openChannel("sftp");
sftp.connect();
sftp.get(sourceFile, targetFile);
sftp.disconnect();
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 判斷單個原始檔[上傳檔案] 是否存在
* @param sourceFile 原始檔
* @return
*/
public boolean prepareUpload(String sourceFile) {
File file = new File(sourceFile);
if (file.exists() && file.isFile()) {
return true;
}
return false;
}
/**
* 判斷多個原始檔[上傳檔案] 是否存在
* @param sourceFiles 原始檔
* @return
*/
public boolean prepareUpload(List<String> sourceFiles) {
for(String item:sourceFiles){
File file = new File(item);
boolean isTrue=file.exists() && file.isFile();
if (!isTrue) {
return false;
}
continue;
}
return true;
}
public SSHInfo getSshInfo() {
return sshInfo;
}
public void setSshInfo(SSHInfo sshInfo) {
this.sshInfo = sshInfo;
}
}
清單五、TestDemo.java 測試用例
/**
* 測試
* @author wangchunlan
* @Description
* @date 2018/10/12 16:29
**/
public class TestDemo {
public static void main(String[] args) {
// 測試一、建立目錄
createDir("wangchunlan");
// 測試二、 上傳單個檔案
uploadTo("/root/kvm2.txt","/root/","C:\\Users\\Administrator\\Desktop\\kvm2.xml");
}
/**
* 建立資料夾
* tip:當資料夾存在時,不報錯。
* @param targetDirFileLocation 建立(目標)資料夾的絕對路徑 如:/root/ma
*/
public static void createDir(String targetDirFileLocation) {
SSHConnection ssh = new SSHConnection();
try {
ssh.connect();
if (ssh.getSshInfo().isReady()) {
ssh.write("mkdir -p " + targetDirFileLocation);
String out = ssh.write("ifconfig");
System.out.print(out);
ssh.close();
}
} catch (Exception e) {
e.