程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java實現 SSH 協議的客戶端登錄認證方式

Java實現 SSH 協議的客戶端登錄認證方式

編輯:關於JAVA

背景

在開篇之前,讓我們先對 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 status

Checking 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 協議的使用有所幫助。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved