1. 程式人生 > >使用JSch實現ssh隧道建立

使用JSch實現ssh隧道建立

前言:

本篇文章記錄我近期研究的問題:如何利用java實現堡壘機與內部機器建立隧道問題。

問題情景描述:

在生產環境中的叢集往往在一個區域網中,而該區域網只能通過某臺特定的堡壘機來訪問。

即:為了更加安全,所以線上的伺服器都無法直接訪問,它必須通過一臺堡壘機來訪問。示意如下:

利用堡壘機建立隧道連結.png

使用者想直接訪問內部伺服器,但是這些內部伺服器並沒有外網。 而堡壘機和這些伺服器在一個區域網中,堡壘機可以和內部伺服器通訊,同時堡壘機擁有外網,可以直接被使用者訪問到。那麼我們便可以先由ssh到堡壘機,然後再ssh到內部伺服器才能夠訪問。這樣做自然可以減少攻擊,但是每次要到內部機器上去執行命令,都需要經歷2次ssh,對線上的除錯與監控效率影響非常大。下面就來全面介紹一下用java如何來解決該問題。

通過ProxyCommand+Netcat

一、前提條件 流程介紹
  1. 本機、跳板機、目標機器(內部伺服器)三者都需要已經做過公鑰認證。
  • tip:如果不做祕鑰認證就會提示分別輸入跳板機和目標機器的密碼,需要輸入兩次密碼,非常繁瑣。而且要利用java做互動式命令輸入。這個問題最終我找到解決的辦法,不需要手動進行互動式命令輸入,Jsch中UIKeyboardInteractive 能夠進行賦值,解決這個問題。

  • 利用java做公鑰認證的方式,暫時我沒有解決掉。我改為上面的 利用命令做互動式賦值來解決的。

  1. linux伺服器安裝Netcat
  • 以CentOS Linux 為例:yum install nc
  1. 配置本機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.