並發型服務器
博客展示的登錄系統的服務器端,將實現重復型服務器。
Client–server model
客戶端-服務器模型(Client–server model)簡稱C/S結構,是一種網絡架構。大部分網絡應用程序在編寫時都假設一端是客戶,另一端是服務器,其目的是為了讓服務器為客戶提供一些特定的服務。可以將這種服務分為兩種類型:重復型或並發型。重復型服務器通過以下步驟進行交互:
- 等待一個客戶請求的到來。
- 處理客戶請求。
- 發送響應給發送請求的客戶。
- 返回第 1 步。
並發型服務器采用以下步驟:
- 等待一個客戶請求的到來。
- 啟動一個新的服務器來處理這個客戶的請求。在這期間可能生成一個新的進程、任務或線程,並依賴底層操作系統的支持。這個步驟如何進行取決於操作系統。生成的新服務器對客戶的全部請求進行處理。處理結束后,終止這個新服務器。
- 返回第 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——增、刪、改