一、几种常见的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
评论区