Java 基于JSch 手动实现 SSH连接 代理 内网穿透

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 和 某个服务2之间建立代理关系, 然后将某个服务1的请求转发到服务2 或者将服务2的请求转发到服务1. 所以根据转发的方向就可以知道是正向代理还是反向代理, 比如如果服务a请求与服务b建立ssh连接后(这时a->b是正向), 如果是将服务a的请求转发到服务b就是正向代理,如果是将服务b的请求转发到服务a就是反向代理。

注意:
代理服务器需要开启端口转发允许:
- 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();

打赏

如果觉得文章有用,你可鼓励下作者

在这里插入图片描述

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值