目 录CONTENT

文章目录

apache.sshd

FatFish1
2025-04-22 / 0 评论 / 0 点赞 / 18 阅读 / 0 字 / 正在检测是否收录...

一、几种常见的ssh库和他们之间的关系

四种ssh库的对比

常听到的ssh相关库有:jsch、apache.sshd、openSSL、openSSH

  • OpenSSL:是底层加密基础库,基于C语言开发,主要是提供加密算法库(AES、RSA等)和SSL/TLS协议实现,支持证书管理和密钥生成等

  • OpenSSH:是系统级的SSH协议实现,基于C语言开发,相当于一个Linux环境下的安全远程登录工具套件(ssh客户端+sshd服务端)应用

  • apache.sshd:是嵌入式Java应用的SSH服务端,基于java开发,支持自定义SSH服务端功能(shell访问、端口转发等)

  • JSCH:是嵌入式java应用的SSH客户端,基于java开发,用于实现java端的SSH客户端库,在java应用中连接SSH服务端(执行命令、SFTP文件传输等)

OpenSSL是其他三个库的基础,OpenSSH依赖OpenSSL实现加密算法(密钥交换、数据加密等),apache.sshd和jsch可以配置依赖OpenSSL,但更推荐的是使用Java内置的加密库,例如JCE和Bouncy Castle,例如apache.sshd:sshd-sftp就是使用的Bouncy Castle

ssh-java客户端选择:apache.sshd:sshd-sftp与Jsch的优劣

除了主攻的SSH服务端,apache.sshd也提供了一些SFTP能力和ssh客户端的扩展,包括apache.sshd:sshd-sftp包

与传统的JSCH相比,它的优劣势包括:

  • apache.sshd的优势在于NIO框架的使用,适合高并发场景;同时其更新快、集成SLF4J、流式API等,对于开发非常友好;但apache.sshd客户端成熟度不足,同时依赖较为复杂,学习曲线陡峭

  • JSCH是传统成熟的ssh客户端,比较轻量级,简单易用;但是Jsch已加不再更新了,且使用老的同步I/O性能有瓶颈

ssh-java客户端对老openssh服务的适配性

因为ssh-java客户端是不使用openSSL的,那么老的openSSH和openSSL版本影响的其实是服务端对密钥算法的支持,进而使得服务端与客户端之间支持的加密算法、密钥类型、协议扩展等存在差异

  • 协商算法:旧版openSSH可能只支持ssh-rsa这种弱算法,而现代客户端库可能都禁用了这些算法,产生例如No matching key exchange method found这类报错

  • 密钥类型:旧版openSSH可能只支持RSA-SHA1类型,对rsa-sha2-256或ed25519这些不支持,如果客户端使用新密钥类型,服务端就无法验证了

  • 证书格式:旧版OpenSSH可能使用PEM格式,现代客户端默认期望OpenSSH格式,需要显式兼容

Jsch对ssh-rsa算法兼容性是更好的,但安全性不佳,而apache.sshd对ssh-rsa这类老算法是默认禁用的,可以通过配置显式启用,例如:

client.setKeyExchangeFactories(new ArrayList<>(KeyExchangeFactory.Utils.getDefaultKeyExchangeFactories()) {{
      add(new DiffieHellmanGroup1.Factory()); // 添加旧算法
  }});

二、apahce.sshd:sshd-sftp源码分析

SshClient

Sshd的客户端,是一个没有界面的“xshell”,负责创建、管理连接

try (SshClient client = SshClient.setUpDefaultClient()) {
       ...further configuration of the client...
       client.start();
……

可以看出,setUpDefaultClient做了默认client的初始化,然后可以做一些自定义配置,最后调用client.start方法,可以从这里开始分析

start

if (sessionFactory == null) {
    sessionFactory = createSessionFactory();
}
……
connector = createConnector();

首先是为了构造一个SessionFactory,这个工厂是用来基于Client构造Session的

protected ClientSessionImpl doCreateSession(IoSession ioSession)

connect - 构造session

ClientSession session = client.connect(login, host, port)
.verify(...timeout...).getSession()) {

示例中也给出了Session的构造方法,通过connect方法构造ConnectFuture,就可以通过getSession方法获取到Session

完成SshClient.connect()流程实际上只走到了算法协商阶段

ClientSession - 会话层

ClientSession会话层的能力包括:

  • 封装了这个会话对应的属性(socket地址、连接上下文、密钥/密码登录等等)

  • 具备会话相应的功能:auth、executeRemoteCommand等

  • 具有构造第三层抽象的能力:SftpFileSystem或ClientChannl

核心成员变量包括:

//  枚举session的状态
enum ClientSessionEvent {
    TIMEOUT,
    CLOSED,
    WAIT_AUTH,
    AUTHED
}

还是从sshd官方提供的使用说明开始看:

ClientSession session = client.connect(login, host, port)
                   .verify(...timeout...)
                   .getSession()) {
session.addPasswordIdentity(password);
session.auth().verify(...timeout...);

AbstractClientSession - session层能力抽象

核心成员变量包括:

// 登录模式
private final List<Object> identities = new CopyOnWriteArrayList<>();

addPasswordIdentity/addPublicKeyIdentity - 配置session登录模式

# addPasswordIdentity 
identities.add(password);
# addPublicKeyIdentity
identities.add(kp);

两个逻辑比较相似,都是在identities里面存东西,identities上面看到了是一个支持并发的list,目前暂不清楚其并发场景,存到list里面的东西后面做login的时候会用到

ClientSessionImpl - session层具体实现

auth - 认证方法

ClientUserAuthService authService = getUserAuthService();
……
future = ValidateUtils.checkNotNull(
        authService.auth(serviceName), "No auth future generated by service=%s", serviceName);

认证方法委托给ClientUserAuthService,这类只做校验和异常处理。选择一个认证方式(Password还是PublicKey)是使用auth的前置条件。

AuthFuture authFutre = session.auth.verify(10, TimeUnit.SECONDS);

auth返回一个AuthFuture,可以用来校验认证结果

getSessionState - 获取session状态

Set<ClientSessionEvent>

返回值是一个set,会把session经历的状态全部存入进去,ClientSessionEvent是ClientSession的一个内部枚举类,封装了session的状态

SftpFileSystem - 文件系统层

提供sftp操作相关的能力,可以通过一段案例引入阅读:

SftpFileSystem fs = SftpClientFactory.instance().createSftpFileSystem(session);
srcFilePath = fs.getDefaultDir().resolve(srcPath);
srcFilePath = fs.getDefaultDir().resolve(targetPath);
Files.move(srcFilePath, dstFilePath);

这里可以发现的是基于sshd的Sftp构造出来的文件系统,可以直接通过Files包进行操作,这是因为这里的Path是特殊实现的,其中携带了sshdSftp的能力提供者:FileSystemProvider

DefaultSftpClientFactory - 构造sftp

createSftpFileSystem - sshd默认提供的构造方法

ClientFactoryManager manager = session.getFactoryManager();

首先获取到ClientFactoryManager,SshClient是实现,这里实际获取到的是session对应的sshClient

SftpFileSystemProvider provider = new SftpFileSystemProvider((SshClient) manager, selector, errorDataHandler);

构造SftpFileSystemProvider

SftpFileSystem fs = provider.newFileSystem(session);

构造SftpFileSystem

createSftpClient - 构造sftpClient

new DefaultSftpClient(session, selector, errorDataHandler);

构造DefaultSftpClient

SftpFileSystemProvider

构造方法

if (client == null) {
    client = SshClient.setUpDefaultClient();
    client.start();
}

这里会判断client是否存在,如果不存在就重新执行SshClient的初始化流程

同时可以关注下传入的factory,从DefaultSftpClientFactory:: createSftpFileSystem方法调用进来的流程,传入的factory是空的

newFileSystem

String id = getFileSystemIdentifier(session);

生成id,一般为ip:port:username的形式

fileSystem = new SftpFileSystem(this, id, session, factory, getSftpVersionSelector(),getSftpErrorDataHandler());
fileSystems.put(id, fileSystem);

生成sftp并存入缓存,这里的缓存是成员变量,可知每次创建都会分别维护一个,不支持一个sftp重复构造对象时实现单例。

SftpFileSystem

核心成员变量:

// 一个存放sftpClient的池子
private final Queue<SftpClient> pool;
// 通过ThreadLocal存放的SftpClient的包装类
private final ThreadLocal<Wrapper> wrappers = new ThreadLocal<>();
// sftp服务器的默认路径
private SftpPath defaultDir;

构造函数

this.pool = new LinkedBlockingQueue<>(SftpModuleProperties.POOL_SIZE.getRequired(session));

这里会构造一个池子存sftpClient

SftpClient client = getClient();

在构造函数中就直接调用getClient构造sftpClient了

defaultDir = getPath(client.canonicalPath("."));

默认路径初始化为“.”

getClient - 构造sftpClient

判断ThreadLocal的wrapper存在,本线程就不再构造sftpClient了,没有,先从pool中获取,取到塞到wrapper中

client = factory.createSftpClient(
        session, getSftpVersionSelector(), getSftpErrorDataHandler());

取不到继续调用SftpFileSystemFactory#createSftpClient方法

isOpen

判断sftpFileSystem是否open,就是直接判断里面的session是否open

getDefaultDir

获取当前文件系统对应用户的默认家目录,获取的是Path是sshd对Path的实现:SftpPath

getDir(String path, String … paths)

拼接传入的路径,这个方法有两个点:

  • 拼出来是绝对路径还是相对路径,由参数path决定,如果path是/xx拼出来就还是/xx,如果是相对路径,拼出来也是相对路径

  • paths里面的路径可以是绝对路径/xxx,但是在这个方法里面不会把它当绝对路径处理,而是不管有没有/,都把xxx拼在path后面

这个方法获取到的Path是sshd对Path的实现:SftpPath

SftpClient/DefaultSftpClient

核心成员变量:

// 一个ClientChannel的实现,通过channel流程实现sftp协议
private final ChannelSubsystem channel;

构造函数

this.channel.open().verify(initializationTimeout);

一次构造SftpClient,就会构造出一个Channel并且开启这个Channel

init(clientSession, initialVersionSelector, initializationTimeout);

向服务器发送初始化命令并等待初始化命令的回应,这里如果catch到异常,则直接关闭channel,停止sftpClient的服务能力

基础类FileSystem

 

配置类SftpConfig

 

SftpPath - sshd对Path的实现

有SftpPath的存在,使得sshd能够像操作本地文件系统一样操作远端sftp文件系统。因为SftpPath实现自Path,是nio包下的接口,可以直接使用Files和Paths进行操作。

resolve

路径拼接方法。path.resolve(path2)方法可以将path和path2拼接成一个新路径。拼接出来是绝对还是相对,看path和path2共同决定。

  • 如果path2是相对,那么path是绝对,拼出来就是绝对,如果path是相对,拼出来就是相对

  • 如果path2是绝对路径,不会做拼接,得到的结果就是绝对路径path2

0

评论区