在任何需要使用者認證的 Web 應用中,一個安全、穩健的註冊流程都是不可或缺的基本。本文將深入探討如何使用 Rust 的 Actix-web 框架,結合 PostgreSQL 資料庫,從零開始建構一個完整且安全的使用者註冊功能。我們將重點關注後端的核心邏輯,包括資料庫設計、伺服器端驗證,以及至關重要的密碼安全儲存。

步驟一:資料庫準備

我們的第一步是設計並建立用於儲存使用者認證資訊的資料庫表。

1. 資料庫 Schema 設計

我們需要一個資料表來儲存使用者的帳號資訊。關鍵在於,絕不能以明文形式儲存密碼。我們將只儲存經過密碼雜湊演算法處理後的密碼摘要。

-- dbscripts/user.sql
-- 如果存在舊表,先刪除
DROP TABLE IF EXISTS ezyweb_user;

-- 建立使用者資料表
CREATE TABLE ezyweb_user (
    username VARCHAR(30) PRIMARY KEY,
    tutor_id INT NOT NULL,
    user_password CHAR(97) NOT NULL -- Argon2 雜湊後的長度
);

設計解說

  • username: 使用者名稱,設為主鍵以確保其唯一性。
  • tutor_id: 關聯到另一個(假設存在的)導師資料表的外部索引鍵。
  • user_password: 用於儲存密碼的雜湊值。我們選擇 CHAR(97),因為 Argon2 演算法產生的標準雜湊字串長度固定為 97 個字元。

2. 初始化資料庫

假設您已安裝 psql 命令列工具,可以使用以下指令來初始化資料庫和資料表:

# 建立資料庫和使用者
psql -U postgres -c "CREATE DATABASE ezytutor_web_ssr;"
psql -U postgres -c "CREATE USER ssruser WITH PASSWORD 'mypassword';"
psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE ezytutor_web_ssr TO ssruser;"

# 執行 SQL 指令碼建立資料表
psql -U ssruser -d ezytutor_web_ssr -f dbscripts/user.sql

步驟二:後端註冊 Handler 實現

現在,我們來編寫處理 POST /register 請求的 Actix-web handler。這個 handler 將負責接收表單資料、進行驗證、雜湊密碼,並將新使用者存入資料庫。

1. 專案依賴

確保您的 Cargo.toml 包含處理資料庫、密碼雜湊和 HTTP 請求所需的依賴:

[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.6", features = ["postgres", "runtime-tokio-native-tls", "macros"] }
argon2 = "0.4"
rand = "0.8"
# awc 用於向其他服務發送 HTTP 請求
awc = "3.0" 

2. 註冊 Handler 核心邏輯

use actix_web::{web, HttpResponse, Responder};
use argon2::{self, Config};
use rand::Rng;
// ... 引入其他模組 ...

// 用於接收註冊表單資料的結構體
#[derive(serde::Deserialize)]
pub struct RegisterTutor {
    pub name: String,
    pub username: String,
    pub password: String,
    pub confirmation: String,
    // ... 其他欄位 ...
}

pub async fn handle_register(
    pool: web::Data<PgPool>, // 資料庫連線池
    // ... 其他依賴 ...
    new_tutor: web::Form<RegisterTutor>,
) -> impl Responder {
    // 1. 伺服器端驗證
    if new_tutor.password != new_tutor.confirmation {
        return HttpResponse::BadRequest().body("密碼與確認密碼不符");
    }

    // 2. 密碼雜湊 (安全性核心)
    let salt = rand::thread_rng().gen::<[u8; 32]>();
    let config = Config::default();
    let hashed_password = match argon2::hash_encoded(new_tutor.password.as_bytes(), &salt, &config) {
        Ok(h) => h,
        Err(_) => return HttpResponse::InternalServerError().body("密碼雜湊失敗"),
    };

    // 3. (可選) 呼叫外部服務建立導師基本資料
    // let tutor_id = call_tutor_service(&new_tutor.name).await;

    // 4. 將使用者認證資料存入資料庫
    let result = sqlx::query!(
        "INSERT INTO ezyweb_user (username, tutor_id, user_password) VALUES ($1, $2, $3)",
        new_tutor.username,
        1, // 假設從外部服務獲取的 tutor_id
        hashed_password
    )
    .execute(pool.get_ref())
    .await;

    match result {
        Ok(_) => HttpResponse::Ok().body("註冊成功"),
        Err(_) => HttpResponse::InternalServerError().body("註冊失敗,請稍後再試"),
    }
}

程式碼解說

  • 伺服器端驗證: 這是必要的安全防線,即使前端已有驗證,也不能信任任何來自客戶端的資料。
  • 密碼雜湊: 我們使用 argon2 crate 來處理密碼。
    • rand::thread_rng().gen::<[u8; 32]>(): 為每個密碼生成一個獨一無二的隨機「鹽」(salt)。這確保了即使兩個使用者使用相同的密碼,他們儲存的雜湊值也完全不同。
    • argon2::hash_encoded(...): 執行雜湊計算,產生一個包含所有必要資訊(演算法、版本、鹽、雜湊值)的標準化字串,可以直接存入資料庫。

步驟三:前端表單設計

雖然本文專注於後端,但一個完整流程離不開前端。以下是一個基礎的 HTML 註冊表單,它的 action 指向我們剛才建立的後端 API。

<!-- static/register.html -->
<!DOCTYPE html>
<html>
<head>
    <title>導師註冊</title>
</head>
<body>
    <h1>導師註冊</h1>
    <form action="/register" method="POST">
        <label>使用者名稱: <input type="text" name="username" required></label><br>
        <label>導師名稱: <input type="text" name="name" required></label><br>
        <label>密碼: <input type="password" name="password" required></label><br>
        <label>確認密碼: <input type="password" name="confirmation" required></label><br>
        <button type="submit">註冊</button>
    </form>
</body>
</html>

圖表解說:安全註冊流程

此循序圖詳細展示了從使用者提交表單到完成安全註冊的完整後端處理流程。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 16
title 安全註冊流程循序圖

actor User
participant Browser
participant "Actix-web Handler" as Handler
participant "Argon2" as Hasher
participant "Database" as DB

User -> Browser : 提交註冊表單
Browser -> Handler : POST /register (含表單資料)
Handler -> Handler : 驗證密碼是否相符
Handler -> Hasher : 產生隨機鹽 (salt)
Handler -> Hasher : 呼叫 hash_encoded(password, salt)
Hasher --> Handler : 回傳雜湊後的密碼字串
Handler -> DB : 執行 INSERT SQL (含 username, hashed_password)
DB --> Handler : 回傳執行結果
Handler --> Browser : 回應 200 OK 或錯誤訊息
Browser -> User : 顯示註冊結果
@enduml

透過遵循以上步驟,我們便建立了一個包含伺服器端驗證和工業級密碼雜湊的安全註冊功能。這不僅保護了使用者的帳號安全,也為整個應用程式的安全性奠定了堅實的基礎。