Java 实现 SSH 协议的客户端登录认证方式--转载
背景
在開篇之前,讓我們先對 SSH 協議有個宏觀的大致了解,這樣更有利于我們對本文的加深了解。首先要提到的就是計算機網絡協議,所謂計算機網絡協議,簡單的說就是定義了一套標準和規則,使得不同計算機之間能夠進行正常的網絡通信,不至于出現在一臺機器上發出的指令到另一臺機器上成了不可認的亂碼,SSH 就是眾多協議的其中之一。經典的七層 OSI 模型(Open System Interconnection Reference Model)出現后,大大地解決了網絡互聯的兼容性問題,它將網絡劃分成服務、接口和協議三個部分,而協議就是說明本層的服務是如何實現的。SSH、Telnet 協議則主要被使用在用戶層中(如圖 1 深色部分所示),即應用層、表現層和會話層。
圖 1. 七層 OSI 模型
回頁首
介紹 SSH
什么是 SSH
SSH(Secure Shell Protocol)是在一個不安全的網絡,進行安全遠程登錄和其他安全網絡服務的協議。這個定義出自于 IETF(Internet Engineering Task Force)。在 TCP/IP 五層模型中,SSH 是被應用于應用層和傳輸層的安全協議。
SSH 的優點
傳統的網絡傳輸,如:Telnet、FTP 等,采用的是明文傳輸數據和口令,這樣很容易被黑客這樣的中間人嗅探到傳輸過程中的數據,大大降低了網絡的通信安全。而 SSH 協議則采用數據加密的方式建立起一個安全的網絡傳輸信道,增強了數據在網絡傳輸過程中的安全性。數據加密程度的復雜,會導致占用更多的網絡資源。SSH 會對加密數據進行一定的壓縮操作,從而減緩對網絡帶寬的占用。總結起來,SSH 的優點如下:
- 數據加密,提高安全性
- 數據壓縮,提高網絡的傳輸速度。
SSH 的架構
在對 SSH 有了一個初步的認識之后,我們來看看 SSH 協議是如何做到數據的安全通信。首先來看下 SSH 協議的主要架構:
圖 2. SSH 協議的構成
傳輸層協議: 通常運行在 TCP/IP 的上層,是許多安全網絡服務的基礎,提供了數據加密、壓縮、服務器認證以及保證數據的完整性。比如,公共密鑰算法、對稱加密算法、消息驗證算法等。
用戶認證協議:運行在 SSH 協議的傳輸層之上,用來檢測客戶端的驗證方式是否合法。
連接協議:運行在用戶認證層之上,提供了交互登錄會話、遠程命令的執行、轉發 TCP/IP 連接等功能,給數據通訊提供一個安全的,可靠的加密傳輸信道。
SSH 的應用
在實際的工作中,很多目標機器往往是我們無法直接操作的,這些機器可能是一個公司機房的服務器,也可能是一個遠在大洋彼岸的客戶環境。這時候我們必須要遠程登錄到目標機器,執行我們需要的操作,這樣不僅降低了運營成本,也提高了執行效率。我們常見的遠程登錄協議有 SSH、Telnet 等。如上文所提到,Telnet 使用的是明文傳輸,這樣對別有用心的“中間人”來說就有了可乘之機,相對 Telnet 協議,SSH 協議的安全性就高了很多。這樣的特性,也使得 SSH 協議迅速被推廣,很多的大型項目中都或多或少的使用到了這個協議。下面本文主要討論 SSH 協議中用戶認證協議層,并且下文中統一將遠程機器稱為服務器(Server),本地機器稱為客戶端 (Client)。
SSH 的認證協議
常見的 SSH 協議認證方式有如下幾種:
- 基于口令的驗證方式(password authentication method),通過輸入用戶名和密碼的方式進行遠程機器的登錄驗證。
- 基于公共密鑰的安全驗證方式(public key authentication method),通過生成一組密鑰(public key/private key)來實現用戶的登錄驗證。
- 基于鍵盤交互的驗證方式(keyboard interactive authentication method),通過服務器向客戶端發送提示信息,然后由客戶端根據相應的信息通過手工輸入的方式發還給服務器端。
SSH 認證協議的工作原理
SSH 的主要工作流程:
圖 3. SSH 登錄工作流程
通過這個張流程圖,我們可以看出,在用戶對遠程機器訪問的時候,首先,是得到了服務器端的一個連接句柄,這里可以理解為是一個 session,然后客戶端可以通過這個句柄取得一些服務器的基本信息,如 SSH 的版本,服務器的版本信息以及一些加密的算法信息等。其次,客戶端可以對這些信息作分析,來匹配當前的客戶端的加密算法、驗證方式是否符合服務器的配置,然后取得彼此可接受的方式,這里可以認為是雙方的協商。最后,當雙方達成一致后,一個安全的信道也就真正建立起來了,此時用戶就可以對遠程機器做想要的操作了。當我們對此有了一定的了解后,就可以初步判斷,在平時工作中,我們通過 SSH 協議去連接一個遠程機器報錯的時候,問題出現在哪個流程上。下面通過具體的 Java 例子來講解用戶驗證方式的原理。
回頁首
常見認證方式的 Java 實現
在開始前,我們要做一些環境的準備工作。
- 一臺本地機器,操作系統是 Windows 用來作為客戶端
- 一臺遠程機器,操作系統是 Linux 用來作為服務器端
- OpenSSH 工具
- Putty 工具
首先,要確保服務器端上已經安裝了 OpenSSH 工具,并且 SSH 的服務已經啟動,可以通過如下命令來進行查看:
查看是否已經安裝了 OpenSSH
清單 1. OpenSSH 版本
# rpm -qa | grep ssh openssh-5.1p1-41.31.36 openssh-askpass-5.1p1-41.31.36查看 SSH 服務是否啟動。
清單 2. SSH 的服務狀態
#/etc/init.d/sshd statusChecking for service sshd running 在 Windows 機器,即客戶端上嘗試使用 Putty 工具連接遠程機器。
圖 4. SSH 連接成功
到目前為止,我們已經可以正常的連接到這臺遠程機器。下面我們就要通過 Java 代碼的方式來實現我們自己的這個遠程登錄的操作。
驗證 service name
在 SSH 協議中定義了一些消息代碼,而 50 至 79 這些代碼是保留給用戶認證協議層使用的,而 80 以上的數字是用于協議運行的,所以如果在用戶認證協議驗證之前,如果我們得到的消息代碼是這個范圍的,SSH 會返回錯誤信息,并斷開連接。例如如以下幾種消息所對應的代碼號:
SSH_MSG_USERAUTH_REQUEST 50:用戶發送一個驗證請求。
SSH_MSG_USERAUTH_FAILURE 51:用戶驗證請求失敗。
SSH_MSG_USERAUTH_SUCCESS 52:用戶驗證請求成功。
那么對于不同的認證方式,又有其各自的消息代碼。
在每次客戶端發送請求的時候,服務器都會檢查當前的 service name 和 username 是否合法,如果當前的 service name 或者 username 不可用,那么服務器端會立刻斷掉請求連接。
下面來實現一個對 service name 驗證的請求,發送數據格式如下:
byte SSH_MSG_SERVICE_REQUEST
string service name in US-ASCIII
具體代碼如下:
清單 3. 類 AuthServiceRequest
package com.my.test.ssh2.auth; import com.my.test.ssh2.common.ProcessTypes; public class AuthServiceRequest { private String serviceName; public AuthServiceRequest(String serviceName){ this.serviceName = serviceName; } /** * 取得指定服務器名稱的認證消息 * @return request – 返回一條十六進制消息 **/ public byte [] getRequestMessage() { byte [] request; ProcessTypes type = new ProcessTypes(); type.asByte(AuthConstant.SSH_MSG_SERVICE_REQUEST); type.asString(serviceName); request = type.getBytes(); return request; }}轉換后發送的消息如下:
[5, 0, 0, 0, 12, 115, 115, 104, 45, 117, 115, 101, 114, 97, 117, 116, 104]
然后再對此進行算法加密,發送到服務器端。
當前協議使用的 service name 是”ssh-userauth”,如果客戶端請求的不是這個 service name,那么服務器會報如下錯誤:
清單 4. service name 異常
Caused by: java.io.IOException: Peer sent DISCONNECT message (reason code 2): bad service request demo-ssh-auth如果客戶端通過了 service name 的驗證后,下一步我們就可以實現具體的認證方式了。流程圖如下所示:
圖 5. Authentication 類圖
TransportManager 類是用來處理傳輸協議層的業務邏輯。在這里主要處理數據的解密、加密、壓縮等操作,這些功能的具體實現主要通過 TransportControl 類來完成,TrasportControl 類會根據客戶端和服務器端協商的數據算法來選擇具體的算法如 sha-1、MD5 等。通過 TransportControl 類處理完的數據會存儲到 Packets 類里,生成一個數據包的列表,為 AuthManager 類提供必要的數據信息。Connect 類是用來處理連接協議層的業務邏輯。主要用于得到一個遠程機器的連接句柄、產生一個安全信道、對 TransportManager 類做數據初始化操作等。AuthManager 類是用來處理認證協議層的業務邏輯。主要是對不同登錄認證方式的請求和從服務器端得到的請求回復做處理,通過客戶端選擇的不同的認證方式調用不同的認證方式實現類,比如 AuthRequestByPassword 類定義了通過密碼認證方式的實現。
圖 6. 認證協議流程圖
首先,開啟一個線程用來接收從服務器發送的加密數據包,然后對這個數據包做算法解密處理并放到一個模型化的堆棧 (Packet List),而另一個線程會監聽當前的 Packet List 里是否有可用的數據包,并對其做解析處理包括對數據包是否合法、是否滿足某種認證算法等。如果數據包所包含的認證方式和當前客戶端請求的認證方式不匹配,那么,客戶端就會失去服務器的連接。反之,如果客戶端請求的認證方式包含在服務器開啟的認證方式,客戶端會返回給服務器一個成功請求,并建立連接會話。
none 認證方式
無認證方式(none authentication),這種認證方式通常是在第一次請求發送的時候使用的,因為通過這個認證方式,我們可以得到當前服務器端支持的所有認證方式的列表,通過這個列表我們就可以驗證我們想要使用的認證方式是否被服務器端所支持。當然,如果遠程目標機器支持這種 none 認證方式,那么客戶端就直接得到了一個會話連接,但是這種認證方式是 SSH 協議里所不推薦使用的。
實現代碼如下:
清單 5. 類 AuthRequestByNone
package com.my.test.ssh2.auth; import com.my.test.ssh2.common.ProcessTypes; public class AuthRequestByNone { private String userName; private String serviceName; public AuthRequestByNone(String serviceName, String user) { this.serviceName = serviceName; this.userName = user; } /** * 取得指定服務器名稱和用戶名的認證消息* @return request - 返回一條十六進制消息* */ public byte [] getRequestMessage() { byte [] request; ProcessTypes type = new ProcessTypes(); type.asByte(AuthConstant.SSH_MSG_USERAUTH_REQUEST); type.asString(userName); type.asString(serviceName); type.asString(AuthConstant.SSH_NONE_AUTHENTICATION_METHOD); request = type.getBytes(); return request; } }從流程圖 6 中可以看出,當我們發送一個 none 認證方式的時候,如果服務器不支持 none 認證,那么客戶端就可以取得服務器端的認證方式列表。首先解析服務器端返回的包信息,例如:
[51, 0, 0, 0, 34, 112, 117, 98, 108, 105, 99, 107, 101, 121, 44, 103, 115, 115, 97, 112, 105, 45, 119, 105, 116, 104, 45, 109, 105, 99, 44, 112, 97, 115, 115, 119, 111, 114, 100, 0]
經過客戶端的算法解析之后,我們可以得到含有如下信息的數據包:( 對于算法原理的具體說明是定義在傳輸層協議里,不是本文所討論的范圍。)
代碼 51,說明用戶驗證請求失敗。
從第 5 位到第 34 位記錄了所當前服務器所支持的認證方法,解析后可得到 publickey, gssapi-with-mic, password 的一個由逗號隔開的認證方式字符串。
最后一位 0 表示,當前的請求失敗,但并不是說整個的連接就斷掉了。
解析數據包的具體算法如下:
清單 6. 解析數據算法
((arr[pos++] & 0xff) << 24) | ((arr[pos++] & 0xff) << 16) | ((arr[pos++] & 0xff) << 8) | (arr[pos++] & 0xff);對于服務器端來說,它要對客戶端的請求作一反饋,從而說明當前的請求是否成功。
數據格式如下:
byte SSH_MSG_USERAUTH_FAILURE name-list authentications that can continue boolean partial success所以,這就正好解釋了上述,從服務器端得到的數據解析結果。
實現部分代碼如下:
清單 7. 初始化函數
public boolean initialize(String userName) throws IOException{ // 預處理服務名稱的請求 AuthServiceRequest serviceRequest = new AuthServiceRequest(AuthConstant.SSH_SERVICE_NAME); IManager transManager = ManagerFactory.getManager(Constant.TRANSPORT_LAYER); transManager.sendMessage(serviceRequest.getRequestMessage()); // 處理無認證方式的消息請求 AuthRequestByNone authNone = new AuthRequestByNone(AuthConstant.SSH_CONN_SERVICE_NAME,userName); transManager.sendMessage(authNone.getRequestMessage()); byte[] message = getMessage(); // 驗證當前的服務名稱是否合法 if(!isAccepted(message)){ return false; } // 取得無認證方式的請求數據包 message = getMessage(); // 驗證當前的請求是否成功 if(isRequestFailed(message)){ return false; } return true; } private boolean isRequestFailed(byte [] messages) throws IOException { if (messages[0] == AuthConstant.SSH_MSG_USERAUTH_SUCCESS){ return true; } if (messages[0] == AuthConstant.SSH_MSG_USERAUTH_FAILURE){ AuthFailure failure = new AuthFailure(messages); authentications = failure.getAuthThatCanContinue(); isPartialSuccess = failure.isPartialSuccess(); return false; } throw new IOException("Unexpected SSH message (type " + messages[0] + ")"); }當客戶端得到了這個 authentications 數組之后,客戶端就可以驗證當前用戶使用的遠程登錄認證方式是否是服務器所支持的。如果是那么再發送一條匹配的認證方式,從而返回登錄認證成功,否則失敗并打印出合理的錯誤信息。下面用 password 的認證方式為例做進一步說明。
password 認證方式
對于 password 認證方式來說,它的數據請求格式如下:
byte SSH_MSG_USERAUTH_REQUEST string user name string service name string "password"boolean FALSE string plaintext password in ISO-10646 UTF-8 encoding具體類的實現方式和 none 認證類的實現類似,只是 getRequestMessage() 方法所有不同。
實現代碼:
清單 8. 生成請求數據函數
public byte [] getRequestMessage() { byte [] request; ProcessTypes type = new ProcessTypes(); type.asByte(AuthConstant.SSH_MSG_USERAUTH_REQUEST); type.asString(userName); type.asString(serviceName); type.asString(AuthConstant.SSH_PASSWORD_AUTHENTICATION_METHOD); type.asString(password); request = type.getBytes(); return request; }在這里我們需要提供給服務器端一個用戶口令,這個口令會在發送給服務器端之前被進行算法加密的處理。調用 password 認證方式的代碼如下:
清單 9. password 認證函數
public boolean passwordAuthentication(String user, String pass) throws IOException{ // 初始化請求 initialize(user); // 驗證指定的認證方式是否是 SSH 服務器所支持的if(verifyAuthenticatonMethods(AuthConstant.SSH_PASSWORD_AUTHENTICATION_METHOD)){ return false; } // 調用密碼認證方式AuthRequestByPassword passwordRequest = new AuthRequestByPassword(AuthConstant.SSH_CONN_SERVICE_NAME,user,pass); // 發送一個消息請求到服務器端 IManager transManager = ManagerFactory.getManager(Constant.TRANSPORT_LAYER); transManager.sendMessage(passwordRequest.getRequestMessage()); // 從服務器端獲取數據包byte[] message = getMessage(); // 驗證當前的請求是否成功 if(isRequestFailed(message)){ return false; } return true; }客戶端首先會做初始化操作,包括數據加密算法的協商、得到服務器端支持的認證方式等。其次客戶端會檢查當前用戶使用的登錄認證方式是否合法,然后再發送一個請求給服務器端,告訴服務器當前使用 password 認證進行遠程登錄。最后,服務器會返回一個數據包,里面包含了對這個請求的回復,如果驗證成功,那么連接就可以開啟一個安全的會話了。至此,password 認證方式的解析就完成了,接下來用戶就可以對遠程機器做操作了,這部分的具體說明是在 SSH 的連接層協議里,不是本文的討論范圍。
回頁首
總結
篇幅所限,本文就以 password 的認證方式為例進行了客戶端遠程登錄的認證方式的討論,對于其他認證方式會在以后的文章中討論。在客戶端用 SSH 協議進行遠程登錄的時候,提供了很多常見的認證方式,每種認證方式發送的數據包的數據結構略有不同,同時也提供了對外擴展接口,可以自定義認證方式。通過對本文的閱讀,可以初步了解到,SSH 協議在用戶認證層的基本原理,希望能對讀者在以后的項目開發中,對 SSH 協議的使用有所幫助。
原文:http://www.ibm.com/developerworks/cn/java/j-lo-sshauthentication/
轉載于:https://www.cnblogs.com/davidwang456/p/4051112.html
總結
以上是生活随笔為你收集整理的Java 实现 SSH 协议的客户端登录认证方式--转载的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用 Java 配置进行 Spring
- 下一篇: Java 日志缓存机制的实现--转载