使用 JSch 的 SFTP
1. 引言
在本教程中,我們將深入了解如何使用JSch 庫執行 SFTP 操作。
我們將重點放在com.github.mwiede:jsch是原始com.jcraft:jsch庫的分支,因為該分支仍在積極維護,而原始庫已不再維護。
2. 依賴關係
要使用 JSch,我們首先需要將最新版本包含到我們的建置中,截至撰寫本文時,最新版本為2.27.5 。
如果我們使用 Maven,我們可以將此依賴項新增到pom.xml檔案中:
<dependency>
<groupId>com.github.mwiede</groupId>
<artifactId>jsch</artifactId>
<version>2.27.5</version>
</dependency>
現在,我們已經準備好在我們的應用程式中使用它了。
3. 連接到 SFTP 伺服器
現在我們已經在應用程式中設定好了 JSch,可以連接到遠端服務了。
在此之前,我們需要建立一個新的 JSch 會話:
JSch jSch = new JSch();
Session session = jSch.getSession(USERNAME, HOSTNAME, PORT);
這將告訴 JSch 連接到在提供的主機和連接埠上執行的 SSH 伺服器,並以提供的使用者名稱進行連線。
我們還需要提供一個UserInfo實例。此介面可協助我們處理 SSH 連線的所有互動方面,包括回答任何提示或提供任何可能需要的憑證:
session.setUserInfo(new UserInfo() {
@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) {
return false;
}
@Override
public void showMessage(String message) {
}
});
如果我們正在建立一個互動式應用程序,這個類別將負責向使用者傳遞提示訊息。如果我們正在建立一個非互動式應用程序,我們可以直接自己處理提示訊息。
此時,我們可以嘗試連接到我們的伺服器:
session.connect();
然而,就目前而言,由於以下幾個原因,這很可能會失敗。
3.1. 主機密鑰
我們將遇到的第一個問題是管理 SSH 伺服器主機金鑰。
主機金鑰的存在是為了讓 SSH 用戶端知道伺服器是否可信。如果主機金鑰與該伺服器預期的完全匹配,則可以建立連線。如果不匹配,則會因連線不安全而被拒絕。如果之前從未連接到該伺服器,則主機金鑰未知,使用者需要自行判斷連線是否安全。
如果我們不採取任何措施來支援這一點,那麼在連接到我們的伺服器時,將會拋出JSchUnknownHostKeyException異常:
com.jcraft.jsch.JSchUnknownHostKeyException: UnknownHostKey: [localhost]:50022. EDDSA key fingerprint is SHA256:VVDqlx5nL9fqS/Wzq87zX1Ze/FCQEmZKiXk5AV2G2jI
JSch 允許我們使用與其他 SSH 用戶端相同的known_hosts格式。我們可以根據需要指定使用以下格式之一:
jSch.setKnownHosts("/users/baeldung/.ssh/known_hosts");
或者,我們可以設定UserInfo對象,使其回應提示以接受主機金鑰:
@Override
public boolean promptYesNo(String message) {
if (message.startsWith("The authenticity of host")) {
return true;
}
return false;
}
我們收到的提示訊息將包含伺服器主機名稱、連接埠和主機金鑰本身。接下來,我們需要對這些資訊進行必要的檢查以確保其有效性,或者也可以像上面那樣直接接受任何密鑰。
3.2 密碼驗證
即使我們的應用程式可以信任我們正在連接的 SSH 伺服器,我們仍然會收到連接到伺服器的錯誤:
com.jcraft.jsch.JSchException: Auth cancel for methods 'publickey,password,keyboard-interactive'
接下來我們需要支援的功能是身份驗證。最簡單的形式是為要連線的使用者提供密碼。我們可以透過在UserInfo物件中實作兩個方法promptPassword()和getPassword() -來處理這個問題。
首先,我們需要回應密碼提示,表明我們是否要提供密碼:
@Override
public boolean promptPassword(String message) {
return true;
}
此處的message將顯示我們正在連接的用戶、主機和端口,以便我們確定是否需要為此連接設定密碼。
第二種實作方法是提供實際使用的密碼:
@Override
public String getPassword() {
return "te5tPa55word;
}
此時,假設我們的密碼正確,我們將成功連接到伺服器。
3.3 公鑰/私鑰認證
除了密碼驗證之外,我們還可以使用公鑰/私鑰驗證。與 SSH 連線一樣,這需要我們已經產生金鑰並將公鑰正確新增到 SSH 伺服器。
JSch 支援 RSA、DSA、ECDSA 和 Ed25519 金鑰,所有這些金鑰都可以使用諸如ssh-keygen之類的標準 SSH 工具產生。金鑰本身可以採用多種格式,包括標準的 OpenSSH 格式、PKCS#8 格式、OpenSSL PEM 格式和 PuTTY PPK 格式。具體使用哪種格式取決於我們的實際應用場景。
完成這些步驟後,我們就可以設定 JSch 來取得私鑰了:
jSch.addIdentity("ssh_keys/id_rsa");
完成此操作後,我們的連接將自動嘗試使用此密鑰連接到任何伺服器。如果伺服器接受此金鑰,則連線將成功。
但在某些情況下,我們的金鑰需要密碼才能解鎖。我們可以透過在UserInfo物件中實作promptPassphrase()和getPassphrase()方法來處理這種情況:
@Override
public String getPassphrase() {
return "te5tPa55word";
}
@Override
public boolean promptPassphrase(String message) {
return true;
}
這與密碼的使用方式完全相同,只是我們提供的是本地密鑰的密碼短語,而不是遠端使用者的密碼短語。
4. SFTP 連接
連接到 SSH 伺服器後,我們就可以開啟 SFTP 通道了。這可以透過呼叫Session.openChannel()函數來實現:
Channel channel = session.openChannel("sftp");
channel.connect();
ChannelSftp sftp = (ChannelSftp) channel;
如果我們的 SSH 連線支援 SFTP,則會傳回一個ChannelSftp實例,我們可以使用該實例執行 SFTP 操作。否則,會拋出一個JSchException異常來指出問題所在。
4.1. 更改目錄
我們的 SFTP 連線在本地端和遠端兩端都維護著一個活動目錄。所有基於檔案和目錄的命令都基於這個目錄執行。
我們可以使用ChannelSftp實例上的pwd()和lpwd()方法來確定目前目錄:
String pwd = sftp.pwd();
String lpwd = sftp.lpwd();
lpwd()方法傳回本機工作目錄,而 pwd() 方法傳回遠端伺服器上的目前工作目錄。
我們可以使用cd()和lcd()方法來改變這些值:
sftp.cd("/tmp");
sftp.lcd("/tmp");
完成後,所有命令都將基於新目錄執行。
4.2. 文件列表
我們可以使用ls()方法來取得遠端伺服器上所有檔案和目錄的清單:
List<ChannelSftp.LsEntry> remoteLs = sftp.ls(".");
此函數接受一個要列出檔案所在的目錄,該目錄可以是伺服器上的絕對目錄,也可以是相對於目前目錄的相對目錄。如果指定值為“.”,則表示目前工作目錄本身。
這將傳回LsEntry類型清單。這些類型包含目標目錄中的檔案名稱以及檔案的屬性,包括權限、檔案大小、存取日期等等:
for (ChannelSftp.LsEntry file : remoteLs) {
String filename = file.getFilename();
SftpATTRS attrs = upload.getAttrs();
}
如果我們知道檔案或目錄的名稱,我們也可以使用stat()方法直接取得其屬性,而無需列出所有內容:
SftpATTRS attrs = sftp.stat("upload");
如果檔案或目錄存在,此函數將傳回其詳細資訊。如果不存在,則會拋出SftpException異常以表示此情況。
請注意,這些方法沒有本地版本。由於本機版本只能處理本機檔案系統上的文件,而我們已經可以使用標準的 Java IO 方法來實現這一點。
4.3 文件傳輸
現在我們已經能夠連接到 SSH 伺服器並發現哪些檔案可用,接下來我們需要能夠將檔案傳輸到伺服器或從伺服器傳輸檔案。
我們可以使用put()方法將檔案傳送到遠端伺服器。此方法有多種變體,它們都接受要寫入的檔案名稱作為參數,但提供實際檔案內容的方式各不相同:
// Copy the local file to the remote file.
sftp.put("localFile.txt", "remoteFile.txt");
// Send the contents of the input stream to the remote file.
sftp.put(inputStream, "remoteFile.txt");
// Open an output stream onto the remote file.
OutputStream outputStream = sftp.put("remoteFile.txt");
InputStream版本可以從任何來源接收輸入流,而OutputStream版本則提供一個我們可以以任何方式寫入的輸出流。
檔案名稱可以是相對於對應工作目錄的相對路徑,也可以是檔案系統中任意位置的絕對路徑。
我們也可以反向傳輸檔案-從遠端伺服器取得檔案。我們使用get()方法來實現這一點。同樣,我們也可以使用類似的變體方法:
// Copy the remote file to the local file.
sftp.get("remoteFile.txt", "localFile.txt");
// Send the contents of the remote file to the output stream.
sftp.get("remoteFile.txt", outputStream);
// Open an input stream onto the remote file.
InputStream inputStream = sftp.get("remoteFile.txt");
正如我們所預期的那樣, OutputStream版本將遠端檔案寫入輸出流,而InputStream版本則為我們提供了一個指向遠端檔案的輸入流,我們可以按照我們想要的方式使用它。
與之前一樣,提供的檔案名稱可以是相對於對應工作目錄的相對檔案名,也可以是對應檔案系統上任何位置的絕對檔案名稱。
7. 總結
本文深入探討了 JSch 函式庫及其在 SFTP 操作中的應用。它還有更多用途,下次需要在系統間傳輸檔案時,不妨一試。
與往常一樣,本文中的所有範例都可以在 GitHub 上找到。