随着安全要求越来越严格,逐渐内网的FTP服务器都被替换为了基于SSH的SFTP服务器。
旧的程序也都被替换成了Java程序用JSCH来连接FTP/SFTP并下载文件。
虽然SFTP是基于SSH,但一般都是设置成用户名+密码的方式访问的。
PS:之前遇到相关的问题:
键盘交互:《SFTP服务器认证类型导致无法登录的问题》
认证指令:《华为网元设备客户端连接我方FTP服务端Serv-U的问题》
第一个小问题是主机身份(指纹)判断,如果你百度了一些说法,不严格验证主机指纹,那么连接倒是OK了。但后续无法保证不受到中间人攻击(MITM Attack),简单说就是有人通过伪装成目标主机,获得你的身份信息。
所以严格验证主机指纹还是需要的:
session.setConfig("StrictHostKeyChecking", "yes");
这时直接登录会报异常:
reject HostKey
当然就是记录已知主机到.ssh目录known_hosts文件中。
格式是每一行一个主机,最好把同一台主机弄成一行,不要每个IP都一行很冗余,如下:
host1,ip1,ip2,ip3,ip4 ssh-rsa AAABBBCCCxxyyzzxxyyzz==
host2,ipv4,ipv6 ssh-rsa DDDEEEFFFxxyyzzxxyyzz==
Windows下正常用户位置:
C:\Users\用户名\.ssh\known_hosts
Windows下系统服务认的“用户目录”位置:
%SystemRoot%\System32\config\systemprofile\.ssh\known_hosts
Linux下位置:
/home/用户名/.ssh/known_hosts
那么如何得到主机的身份(指纹)信息呢?
有下面三个办法:
先手动ssh连接一次这台确认过的服务器。
$shion@shionlnx ~> ssh 主机名或IP
当你同意连接时,主机的信息会被记录到用户目录的known_hosts中。
一个主机因为多个IP或名称可能会重复多条,可以自行整理成上面的单条格式。
或者,执行这个指令可以得到目标主机的身份(指纹)信息。
$shion@shionlnx ~> ssh-keygen -f "/home/用户名/.ssh/known_hosts" -R "主机名或IP"
如果程序用公钥方式登录,那么和手动SSH类似。
第一次执行时,主机的信息会被记录到用户目录的known_hosts中。
今后程序就不会再提示未知指纹了(见最后的代码)。
PS:密码方式登录不会自动记录。
如果发生中间人攻击,或者主机指纹确实发生了变化,无论程序还是手动SSH登录都会得到大概下面的错误,并且不会让你继续登录。
程序:
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that the RSA host key has just been changed.
The fingerprint for the RSA key sent by the remote host 192.168.168.XX is
aa:bb:cc:11:22:33:44:55:66:77:88:99:00:dd:ee:ff.
Please contact your system administrator.
Add correct host key in /home/用户名/.ssh/known_hosts to get rid of this message.
手动:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
SHA256:saj23d4f6k7a8sjfkdlsdfjk/3dS8juih3ys.
Please contact your system administrator.
Add correct host key in /home/用户名/.ssh/known_hosts to get rid of this message.
Offending RSA key in /home/用户名/.ssh/known_hosts:1
remove with:
ssh-keygen -f "/home/用户名/.ssh/known_hosts" -R "shion-haier"
RSA host key for shion-haier has changed and you have requested strict checking.
Host key verification failed.
如果确认主机没有问题,只是别的原因指纹变化了。
那么需要从known_hosts中删除对应的那条记录,重新记录一次新的主机身份(指纹)。
如果还没设好密钥登录,那么请自行先设置好,用ssh指令试试看。
可以参考我的:《从零开始学习大数据平台(Episode 1)》里面
章节(1.3)设置虚拟机环境【4】配置ssh密钥方式登录(免密码登录)
如果你已经设置好了密钥并且可以手动ssh免密码登录,程序用JSCH就可以这样:
jsch.addIdentity(thePrivateKeyFileName); //直接用私钥文件名
...
session = jsch.getSession(username, host, port);
session.setConfig("PreferredAuthentications", "publickey"); //指定验证方式。
函数addIdentity大概这样的:
//com.jcraft.jsch.JSch
//Maven: com.jcraft:jsch:0.1.55 (jsch-0.1.55.jar)
public void addIdentity(String prvkey) throws JSchException
//Sets the private key, which will be referred in the public key authentication.
Parameters:
//prvkey - filename of the private key.
Throws:
//JSchException - if prvkey is invalid.
设置ssh密钥登录(免密码登录)时。
生成的密钥由于工具或版本的原因,JSCH可能用不了。
需要这样:
$shion@shionlnx ~> ssh-keygen -m PEM -t rsa
指定rsa加密方式(目前还是默认),并用了参数 -m PEM指定格式:
-m key_format Specify a key format for key generation, the -i (import), -e (export) conversion options, and the -p change passphrase operation. The latter may be used to convert between OpenSSH private key and PEM private key formats. The supported key formats are: “RFC4716” (RFC 4716/SSH2 public or private key), “PKCS8” (PKCS8 public or private key) or “PEM” (PEM public key). By default OpenSSH will write newly-generated private keys in its own format, but when converting public keys for export the default format is “RFC4716”. Setting a format of “PEM” when generating or updating a supported private key type will cause the key to be stored in the legacy PEM private key format.
这个弄了很久一直搞不懂。
最后发现只是一个选项的问题……
新建或选中一个用户 >> User Information >> SSH Keys >> ManageKeys
Add Key,增加一个公钥文件。
这个公钥不需要是当前用户名生成的(呃……这也……)。
当然也可以用它Create Key来新建密钥。
但是这样违背了SSH的安全原则,也就是说Serv-U是服务方,它应该只保存用户的公钥(从客户机拷贝过来)而不是生成私钥拷贝到客户机(私钥不应在计算机间传输)。
1)在你需要登录的用户的【Limits & Settings】下。或者全局的【Limits & Settings】的 Limits页。
2)选中【Limit Type】下拉框,切换到【Password】。
3)定位到【SSH authentication type】项目双击它。PS:由于默认项目的不能编辑,所以双击不是修改而是新建一条同命项目【SSH authentication type】。
4)将 Password and Public Key 改为 Password or Public Key 。⭐️
虽然让人不太理解……
我改的是全局,大概截图如下:
如果不改这一项,那么你将得到返回是:
Auth fail
因为觉得不理解不爽,所以又看了一下Serv-U的日志。
发现没有任何帮助……
设置 Password and Public Key的失败记录:
[02] Mon 13Dec21 17:38:43 - (000003) Connected to 192.168.50.88 (local address 192.168.50.88, port 22)
[30] Mon 13Dec21 17:38:43 - (000003) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: none
[31] Mon 13Dec21 17:38:43 - (000003) SSH2_MSG_USERAUTH_FAILURE: login failed
[30] Mon 13Dec21 17:38:43 - (000003) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: publickey
[30] Mon 13Dec21 17:38:43 - (000003) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: publickey
[31] Mon 13Dec21 17:38:43 - (000003) SSH_MSG_DISCONNECT: client has requested a disconnect. Reason code: 3
[02] Mon 13Dec21 17:38:43 - (000003) Closed session
设置 Password or Public Key 的成功记录:
[02] Mon 13Dec21 17:42:12 - (000004) Connected to 192.168.50.88 (local address 192.168.50.88, port 22)
[30] Mon 13Dec21 17:42:13 - (000004) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: none
[31] Mon 13Dec21 17:42:13 - (000004) SSH2_MSG_USERAUTH_FAILURE: login failed
[30] Mon 13Dec21 17:42:13 - (000004) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: publickey
[30] Mon 13Dec21 17:42:13 - (000004) SSH2_MSG_USERAUTH_REQUEST: user: Shion; service: ssh-connection; type: publickey
[02] Mon 13Dec21 17:42:13 - (000004) User "Shion" logged in
[31] Mon 13Dec21 17:42:13 - (000004) SSH2_MSG_USERAUTH_SUCCESS: successful login
考虑到实际场景,先尝试严格方式,如果成功则日志记录指纹已知。
但是如果没有设置known_hosts则再尝试不严格的方式,并告警指纹未知有风险。
如果密码是一个私钥文件名并且文件存在,则采用密钥方式登录,否则密码方式登录。
UserInfo 部分请继续参考键盘交互:《SFTP服务器认证类型导致无法登录的问题》
public void getConnect(String host, int port, String username,String password, boolean psv) throws Exception {
fpassword=password;
UserInfo ui = new MyUserInfo();
try {
JSch jsch = new JSch();
String knownHosts = System.getProperty("user.home")+"/.ssh/known_hosts";
if (new File(knownHosts).exists()) {
jsch.setKnownHosts(knownHosts);
}
if (new File(password).exists()) { //如果密码位置输入的是密钥文件
jsch.addIdentity(password);
}
session = jsch.getSession(username, host, port);
session.setUserInfo(ui);
session.setConfig("userauth.gssapi-with-mic", "no");
session.setConfig("StrictHostKeyChecking", "yes"); // 验证 HostKey
if (new File(password).exists()){ //如果密码位置输入的是密钥文件
session.setConfig("PreferredAuthentications", "publickey"); // 公钥校验
} else {
session.setConfig("PreferredAuthentications", "password");
session.setPassword(password);
}
session.connect(5000);
CrossLog.MyLog(TNU.LogType.INFO,"主机指纹: ["+session.getHostKey().getFingerPrint(jsch)+"]已知");
} catch (JSchException jse) {
if (jse.getMessage().contains("reject HostKey")) {
JSch jsch = new JSch();
String knownHosts = System.getProperty("user.home")+"/.ssh/known_hosts";
if (new File(knownHosts).exists()) {
jsch.setKnownHosts(knownHosts);
}
if (new File(password).exists()) { //如果密码位置输入的是密钥文件
jsch.addIdentity(password);
}
session = jsch.getSession(username, host, port);
session.setUserInfo(ui);
session.setConfig("userauth.gssapi-with-mic", "no");
session.setConfig("StrictHostKeyChecking", "no"); // 不验证 HostKey
if (new File(password).exists()){ //如果密码位置输入的是密钥文件
session.setConfig("PreferredAuthentications", "publickey"); // 公钥校验
} else {
session.setConfig("PreferredAuthentications", "password");
session.setPassword(password);
}
session.connect(5000);
CrossLog.MyLog(TNU.LogType.WARNING,"主机指纹: ["+session.getHostKey().getFingerPrint(jsch)+"]未知!");
CrossLog.MyLog(TNU.LogType.WARNING,"请手动确认主机指纹,避免中间人攻击(MITM Attack)危险!");
}
else {
throw new Exception("连接服务器失败(JSCH)! "+jse.getMessage());
}
} catch (Exception e) {
if (session.isConnected())
session.disconnect();
throw new Exception("连接服务器失败! "+e.getMessage());
}
channel = session.openChannel("sftp");
try {
channel.connect();
} catch (Exception e) {
if (channel.isConnected())
channel.disconnect();
throw new Exception("连接服务器失败(channel)! "+e.getMessage());
}
sftp = (ChannelSftp) channel;
}
实际工作中,用到SFTP都是管理员分配给我们一个用户名和密码,所以无法密钥登录方式。
但软件功能完善一点,总归是好的
the end