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 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 协议的使用有所帮助。
原文:http://www.ibm.com/developerworks/cn/java/j-lo-sshauthentication/
Java 实现 SSH 协议的客户端登录认证方式--转载的更多相关文章
- 【项目实践】一文带你搞定Session和JWT的登录认证方式
以项目驱动学习,以实践检验真知 前言 登录认证,估计是所有系统中最常见的功能了,并且也是最基础.最重要的功能.为了做好这一块而诞生了许多安全框架,比如最常见的Shiro.Spring Security ...
- oracle修改登录认证方式
通过配置sqlnet.ora文件,我们可以修改oracle登录认证方式. SQLNET.AUTHENTICATION_SERVICES=(NTS);基于操作系统的认证 SQLNET.AUTHENTIC ...
- oracle数据库启动流程及登录认证方式详解
转自:https://www.2cto.com/database/201803/726644.html ■ oracle启动流程-windows下 1) lsnrctl start (启动监听) ...
- oracle改动登录认证方式
通过配置sqlnet.ora文件.我们能够改动oracle登录认证方式. SQLNET.AUTHENTICATION_SERVICES=(NTS);基于操作系统的认证 SQLNET.AUTHENTIC ...
- 09 SSH原理与远程登录实现方式
一.什么是SSH? SSH是一种网络协议,用于计算机之间的加密登录.使用SSH协议登录另一台远程计算机,我们就可以认为,这种登录是安全的,即使被中途截获,密码也不会泄露. 二.LInux下的基本用法 ...
- 这些OAuth2客户端的认证方式你未必了解
OAuth2客户端按照它们与授权服务器进行安全认证的能力可以分为机密类型(Confidential)和公共类型(Public). 机密类型的自身会有个密码凭据,比如Web服务器后端程序:而公共类型则没 ...
- 通过ssh协议实现用户key认证登录
author:JevonWei 版权声明:原创作品 用户实现key认证登录 主机A 192.168.198,134 主机B 192.168.198,131 主机C 192.168.198,136 创建 ...
- 关于KeePass实现ssh协议的自动登录
本文主要介绍一下,在keepass中如何实现linux主机的ssh方式的自动登录 keepass版本:KeePass 2.45 在keepass的URL中,其实默认也是内置了ssh的,其原理是调用pu ...
- Java Tomcat SSL 服务端/客户端双向认证
借花献佛:http://www.blogjava.net/icewee/archive/2012/06/04/379947.html
随机推荐
- Excel等外部程序点击链接会带上IE信息的bug
今天碰到一个问题,在Excel内点击链接到默认浏览器Chrome打开,奇怪的是服务端收到的Session一直对不上. 查了很久发现这个Excel到Chrome的跳转竟然带上了IE的Cookie 和 U ...
- C缩写
STL:Standard Template Library,标准模板库
- delphi中DLL编程详解
10.1 Windows的动态链接库原理 动态链接库(DLLs)是从C语言函数库和Pascal库单元的概念发展而来的.所有的C语言标准库函数都存放在某一函数库中,同时用户也可以用LIB程序创建自己的函 ...
- Obj-C的hello,world 1
不得不说,Obj-C所谓的中缀表达式真的蛮奇怪的,当无参或者只有一个参数时看起来还不错: //无参数的方法 -(void) say; [employee say]; //只有一个参数的方法 -(voi ...
- 转:使用Mongo Connector和Elasticsearch实现模糊匹配
原文来自于:http://www.csdn.net/article/2014-09-01/2821485-how-to-perform-fuzzy-matching-with-mongo-connec ...
- 设计模式——如何避免在OO设计中违反依赖倒置原则
1 变量不可以包含具体类的引用.一旦new,就对具体类产生依赖,用工厂模式来避开. 2 类不要派生至具体类.用派生抽象类避开. 3 不要覆盖基类已经实现的方法.基类中已实现的方法应该由所有子类共享.
- shell命令记录一些
ps aux | sort -k 5n|tail -5 找到内存最对的进程 ps aux 是找出全部的进程 sort -k 5n 表示第5个参数进行排序 tail -5 表示最后5个 ps -e - ...
- Swordfish
zoj1203:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=1203 题意:给定平面上N个城市的位置,计算连接这N个城市所 ...
- left join 、right join 、inner join和 full join的区别
内连接 INNER JOIN(等值连接):只显示两个表中联结字段相等的行.这个和用select查询多表是一样的效果,所以很少用到: 外连接:LEFT JOIN :以左表为基础,显示左表中的所 ...
- Lucky and Good Months by Gregorian Calendar(模拟)
http://poj.org/problem?id=3393 好大的一道模拟题,直接当阅读理解看了.下面是大神写的题意,解释的好详细. 定义: Goog month : 该月第一个工作日为星期一的月份 ...