Java 程序设计——登录系统

并发型服务器

博客展示的登录系统的服务器端,将实现重复型服务器。

Client–server model

客户端-服务器模型(Client–server model)简称C/S结构,是一种网络架构。大部分网络应用程序在编写时都假设一端是客户,另一端是服务器,其目的是为了让服务器为客户提供一些特定的服务。可以将这种服务分为两种类型:重复型或并发型。重复型服务器通过以下步骤进行交互:

  1. 等待一个客户请求的到来。
  2. 处理客户请求。
  3. 发送响应给发送请求的客户。
  4. 返回第 1 步。

并发型服务器采用以下步骤:

  1. 等待一个客户请求的到来。
  2. 启动一个新的服务器来处理这个客户的请求。在这期间可能生成一个新的进程、任务或线程,并依赖底层操作系统的支持。这个步骤如何进行取决于操作系统。生成的新服务器对客户的全部请求进行处理。处理结束后,终止这个新服务器。
  3. 返回第 1 步。

并发服务器的优点在于利用多线程来处理客户的请求,如果操作系统允许多任务,那么就可以同时为多个客户服务,本博客将实现并发型服务器。

Request/Response model

请求/响应(Request/Response)模型一种通用的网络模型架构,该模型下永远都由客户端发起请求,由服务器进行响应并发送回响应报文。如果没有客户端进行请求或曾经请求过,那么服务器是无法将消息推送到客户端的。

三层架构

本系统将基于三层架构实现,由表示层、逻辑层和存储层 3 层组成。表示层是应用的最高层,负责与用户进行交互,并将通信信息发送给逻辑层。逻辑层执行细节处理来控制应用的功能,连接到存储层。存储层则往往是数据库,用于检索并维护数据。

表示层向逻辑层发送请求,逻辑层对存储层完成检索和更新数据等操作,最后逻辑层根据存储层的数据对表示层进行响应。从概念上看,三层架构是一种线性关系

登录系统设计

登录系统由客户端和服务器 2 个部分组成。

客户端

客户端需要接收用户输入的用户名和密码,然后将这 2 个信息发送给服务器。发送完毕之后等待服务器的响应信息,若在一定的时间内收到了服务器的响应,并且响应“操作成功”,则完成用户的登录操作。除了支持用户的登录操作,客户端还应该支持其他基于用户的操作,例如注册用户、密码修改和注销用户等。

  • 修改密码和注销用户的操作,不能在登录前被调用,只能在登录之后。

服务器

无论客户端是否启动,服务器应当保持启动的状态,并且持续监听分配给服务器的端口。当服务器收到客户端发送的数据时,服务器对数据进行处理并执行对应的操作。根据服务器的执行情况,若操作成功则向客户端通告操作成功,否则通告失败,接着继续 监听客户端发送的数据。

数据库连接

用户名和密码信息将保存在数据库中,当服务器接收到客户端的数据时,将按照客户端请求的操作与数据库进行交互。根据客户端请求的方式不同,服务器与数据库交互时需要执行的操作为:

用户请求 数据库操作
注册 INSERT INTO
登录 SELECT
密码修改 UPDATE
用户注销 DELETE

客户端实现

UserBehaviorDAO 接口

由于客户端和服务器的交互方式不仅仅只有三层架构的实现方式,还可以实现多层架构等其他方式。此时服务器及其它层的动作对于客户端来说是不可见的,因此定义 UserBehaviorDAO 接口支持客户端和服务器的交互方式的更换。

/**
* UserBehaviorDAO 接口指定了针对用户的行为
* @author 乌漆WhiteMoon 
* @version 1.0
*/
public interface UserBehaviorDAO {
	
	/**
	   * 这个方法将实现用户的注册操作
	   * @param username 用户名,String
	   * @param password 密码,String
	   * @return 操作是否成功,boolean
	   */
	  public static boolean registerUser(String username, String password) {
		return false;
	  }
	
	/**
	   * 这个方法将实现用户的改密码操作
	   * @param username 用户名,String
	   * @param password 密码,String
	   * @param new_password 新密码,String
	   * @return 操作是否成功,boolean
	   */
          public static boolean changePassword(String username, String password, String new_password) {
		return false;
	  }
    
       /**
	   * 这个方法将实现用户的登录操作
	   * @param username 用户名,String
	   * @param password 密码,String
	   * @return 操作是否成功,boolean
	   */
          public static boolean signIn(String username, String password) {
		return false;
	  }
    
        /**
	   * 这个方法将实现用户的销户操作
	   * @param username 用户名,String
	   * @param password 密码,String
	   * @return 操作是否成功,boolean
	   */
          public static boolean cancelUser(String username,String password) {
		return false;
	  }
}

UserBehavior 类

UserBehavior 类是静态类,实现了 UserBehaviorDAO 接口,该类的 4 个方法将把数据传输给套接字进行和服务器的通信。

服务器实现

UserDao 类

由于服务器需要与数据库进行交互,而数据库应该作为一个可替换组件存在,因此定义 UserDao 接口指定了与数据库的交互行为。

/**
* SqlActionDao 接口指定了与数据库的交互行为
* @author 乌漆WhiteMoon 
* @version 1.0
*/
public interface UserDao {
	
	/**
	   * 查找用户名是否已存在,用户注册时用
	   * @param username 被查找的用户名
	   * @return true为用户名不存在,false为用户名已存在
	 * @throws SQLException 数据库异常
	   */
	public static boolean selectUsername(String username) throws SQLException{
		return false;
	}
	
	/**
	   * 核对用户名和密码是否存在且匹配,用户登录和其他增删改操作时使用
	   * @param username 用户名
	   * @param password 密码
	   * @return true为用户名和密码存在且匹配,false为用户名或密码错误
	 * @throws SQLException 数据库异常
	   */
	public static boolean checkUser(String username, String password) throws SQLException{
		return false;
	}
	
	/**
	   * 向数据库插入一个uesr记录,注册操作时用,调用该方法前应使用selectUsername()方法检查
	   * @param username 用户名
	   * @param password 密码
	   * @return true为记录插入成功,false为插入失败
	 * @throws SQLException 数据库异常
	   */
	public static boolean insertUser(String username, String password) throws SQLException{
		return false;
	}
	
	/**
	   * 删除一条记录,注销用户时用,调用该方法前应使用checkUser()方法检查
	   * @param username 用户名
	   * @return true为删除成功,false删除失败
	 * @throws SQLException 数据库异常
	   */
	public static boolean deleteUser(String username) throws SQLException{
		return false;
	}
	
	/**
	   * 更新一个uesr的password字段,改密码操作时用,调用该方法前应使用checkUser()方法检查
	   * @param username 用户名
	   * @param new_password 新密码,用于替换原有的条目
	   * @return true为更换成功,false更换失败
	 * @throws SQLException 数据库异常
	   */
	public static boolean updateUserPasswd(String username, String new_password) throws SQLException{
		return false;
	}
}

UserImpl 类

UserImpl 类是静态类,实现了 UserDao 接口,该类将连接到 MySQL 数据库进行对数据的操作。为了支持对数据库的连接,还需要 MysqlConnect 类完成连接操作。

数据封装

操作码和分隔符

客户端发送的有效载荷为一个字符串,该字符串由操作码、用户名和密码 3 个部分组成,3 个部分之间用 “+” 连接。

服务器接收到数据之后,将数据按照分隔符 “+” 进行分割。

//分割明文,执行对应的操作
String result_set[] = result_decode.split("+");

通过操作码执行对应的操作,操作码和用户的请求的关系如下:

用户请求 数据库操作
1 注册
2 登录
3 密码修改
4 用户注销
  • 改密码操作还需要传输新密码,因此“密码”部分的内容为 “password '+' newpassword”。

数据加密

MD5Util 类

MD5Util 类只有 getMD5Str() 方法,用于对传入的字符串进行 MD5 加密。MD5Util 类会被 UserBehavior 类调用,传输的用户名和密码都会进行 MD5 加密,从而保证这 2 者的安全性。

例如对用户名“张三”和密码“123456”调用 getMD5Str() 方法进行加密,输出结果为:

615db57aa314529aaa0fbe95b3e95bd3
e10adc3949ba59abbe56e057f20f883e

服务器的日志文件如下所示:

Base 64 加密

即使对用户名和密码进行加密,攻击者仍然可能截取密文进行提交,为了保证安全性需要对整个有效载荷进行加密。UserClient 类的 sendRequest 方法使用 base64 加密,加密的代码为:

//明文写入操作码,跟着用户名即密码
String Plaintext = actionCode + " " + username + " " + password;
//对明文进行 base64加密
String ciphertext = Base64.getEncoder().encodeToString(Plaintext.getBytes("utf-8"));

Base 64 解密

服务器接收到数据之后要先对数据进行 Base 64 解密,解密的代码如下:

DataInputStream in = new DataInputStream(server.getInputStream());
//将客户端传来的密文转成明文
String result_decode = new String(Base64.getDecoder().decode(in.readUTF()));

MySQL 数据库

users 表

数据库中的 users 表用于存储 MD5 加密后的用户名和密码,users 表的字段有。

下图是存储了 3 个用户信息的 users 表。

SQL 查询语句

selectUsername() 方法

这个方法将基于select查找用户名是否已存在,用户注册时用。

SELECT username FROM users WHERE binary username = '%s';

checkUser() 方法

这个方法将基于 select 核对用户名和密码是否存在且匹配,用户登录和其他增删改操作时使用.

SELECT username,password FROM users WHERE binary username = '%s' AND password = '%s';

insertUser() 方法

这个方法将基于 insert 向数据库插入一个 uesr 记录,注册操作时用,调用该方法前应使用
selectUsername() 方法检查。

INSERT INTO users(username, password) values('%s','%s');

deleteUser() 方法

这个方法将基于 delete 删除一条记录,注销用户时用,调用该方法前应使用 checkUser() 方法检查。

"DELETE FROM users WHERE username = '%s';"

updateUserPasswd() 方法

这个方法将基于 update 向更新一个 uesr 的 password 字段,改密码操作时用,调用该方法前应使用 checkUser() 方法检查。

UPDATE users SET password='%s' WHERE username = '%s';

Socket 实现

Socket

不同端系统的进程是通过彼此之间向套接字发送报文来实现通信,套接字就好比是门禁,想要和应用程序进行通信需要先通过门禁的验证。同理也不是什么报文都能随意出门的,必须是得到允许的报文才会被送出门去。
为了连接主机,我们需要目标主机的 IP 地址,这样才能知道要发给哪个端系统,就像送信就一定要有收件人。但是由于一台主机上可能运行着好多个进程,需要指定一个端口号,令指定的进程接收分组。需要强调的是,我们自己写的端口需要避开 RFC 定义的协议,例如 HTTP 协议的端口号 80。

Client-Socket

UserClient 类

UserClient 类是基于请求响应模型的客户端套接字,该类应该在客户端被调用,只有 sendRequest() 发送报文一个方法。

Server-Socket

Response 类

本类的方法将接受套接字收到的数据,调用 SqlActionDao 接口执行对客户端请求的操作,操作完成后进行响应。

UserServer 类

UserServer 类将继承 Thread 类,run() 方法将保持对分配给该进程的端口的监听,若接收到数据则调用 Response 类中的方法进行操作。

Customer 类

用户登录之后,将会把登录的用户信息实例化一个 Customer 类。注意 Customer 类不会保存用户的密码,安全的做法是让用户执行改密码和注销操作时都额外提供一次密码。

public class Customer {
	private final String username;
	private final String username_md5;
	private LinkedList<Emails> Inbox;    //收件箱
	private LinkedList<Emails> Outbox;    //发件箱
	
	/**
	   * 这个方法是customer对象的构造器
	   * @param username 用户名,String
	   * @return customer对象
	   */
	public Customer(String username) {
		this.username = username;
		this.username_md5 = MD5Util.getMD5Str(username);
	}
}

GUI 设计

登录界面


注册用户界面


密码修改界面

销户界面

参考资料

《计算机网络(第七版)》 谢希仁 著,电子工业出版社
《TCP/IP 详解 卷1:协议》[美]W.Richard Stevens 著,范建华 胥光辉 张涛 等译,谢希仁 校,机械工业出版社
《SQL注入攻击与防御(第2版)》 [美]Justin Clarke 著,施宏斌 叶愫 译,清华大学出版社
计算机网络:协议栈分层
应用层:HTTP 协议
HTTP请求/响应模型
Java DAO 模式
应用层:UDP 套接字编程
应用层:TCP 套接字编程
MySQL——SELECT
MySQL——增、删、改

posted @ 2020-12-08 23:55  乌漆WhiteMoon  阅读(1580)  评论(0编辑  收藏  举报