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 协议的构成
图 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 登录⼯作流程
图 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 连接成功
图 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
st.ssh2.auth;
st.ssh2mon.ProcessTypes;
public class AuthServiceRequest {
private String serviceName;
public AuthServiceRequest(String serviceName){
this.serviceName = serviceName;
}
/**
* 取得指定服务器名称的认证消息
* @return request – 返回⼀条⼗六进制消息
**/
public byte [] getRequestMessage() {ssh工具安卓下载
byte [] request;
ProcessTypes type = new ProcessTypes();
type.asByte(AuthConstant.SSH_MSG_SERVICE_REQUEST);
type.asString(serviceName);
request = Bytes();
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 类图
图 5. Authentication 类图
TransportManager 类是⽤来处理传输协议层的业务逻辑。在这⾥主要处理数据的解密、加密、压缩等操作,这些功能的具体实现主要通过 TransportControl 类来完成,TrasportControl 类会根据客户端和服务器端协商的数据算法来选择具体的算法如 sha-1、MD5 等。通过 TransportControl 类处理完的数据会存储到 Packets 类⾥,⽣成⼀个数据包的列表,为 AuthManager 类提供必要的数据信息。Connect 类是⽤来处理连接协议层的业务逻辑。主要⽤于得到⼀个远程机器的连接句柄、产⽣⼀个安全信道、对 TransportManager 类做数据初始化操作等。AuthManager 类是⽤来处理认证协议层的业务逻辑。主要是对不同登录认证⽅式的请求和从服务器端得到的请求回复做处理,通过客户端选择的不同的认证⽅式调⽤不同的认证⽅式实现类,⽐如 AuthRequestByPassword 类定义了通过密码认证⽅式的实现。
图 6. 认证协议流程图
图 6. 认证协议流程图
⾸先,开启⼀个线程⽤来接收从服务器发送的加密数据包,然后对这个数据包做算法解密处理并放到⼀个模型化的堆栈 (Packet List),⽽另⼀个线程会监听当前的 Packet List ⾥是否有可⽤的数据包,并对其
做解析处理包括对数据包是否合法、是否满⾜某种认证算法等。如果数据包所包含的认证⽅式和当前客户端请求的认证⽅式不匹配,那么,客户端就会失去服务器的连接。反之,如果客户端请求的认证⽅式包含在服务器开启的认证⽅式,客户端会返回给服务器⼀个成功请求,并建⽴连接会话。
none 认证⽅式
⽆认证⽅式(none authentication),这种认证⽅式通常是在第⼀次请求发送的时候使⽤的,因为通过这个认证⽅式,我们可以得到当前服务器端⽀持的所有认证⽅式的列表,通过这个列表我们就可以验证我们想要使⽤的认证⽅式是否被服务器端所⽀持。当然,如果远程⽬标机器⽀持这种 none 认证⽅式,那么客户端就直接得到了⼀个会话连接,但是这种认证⽅式是 SSH 协议⾥所不推荐使⽤的。
实现代码如下:
清单 5. 类 AuthRequestByNone
st.ssh2.auth;
st.ssh2mon.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 = Bytes();
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 = Manager(Constant.TRANSPORT_LAYER);
transManager.RequestMessage());
// 处理⽆认证⽅式的消息请求
AuthRequestByNone authNone = new
AuthRequestByNone(AuthConstant.SSH_CONN_SERVICE_NAME,userName);
transManager.RequestMessage());
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 = AuthThatCanContinue();
isPartialSuccess = failure.isPartialSuccess();
return false;
}
throw new IOException("Unexpected SSH message (type " + messages[0] + ")");
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论