返回文章列表

Rust網頁應用程式使用者註冊登入實作

本文示範如何使用 Rust 的 Actix Web 框架和 Tera 範本引擎,構建一個具備使用者註冊和登入功能的網頁應用程式。文章涵蓋了資料函式庫互動、密碼雜湊、表單處理和路由設定等關鍵步驟,並提供程式碼範例和流程圖解說,幫助讀者理解 Rust 網頁應用程式的開發流程。

Web 開發 Rust

本篇延續先前章節,更探討 Rust 網頁應用程式的使用者認證機制,包含資料函式庫的讀寫操作、密碼安全處理、表單驗證與錯誤處理。示範如何使用 Actix Web 框架處理 HTTP 請求、路由設定,以及如何運用 Tera 範本引擎渲染動態網頁內容。同時,也說明瞭如何與後端服務互動,整合前後端流程,並提供程式碼實作細節與流程圖,以利讀者快速上手 Rust 網頁應用程式開發。

資料庫存取功能實作與註冊處理器開發

資料庫存取函式實作

$PROJECT_ROOT/src/iter5/dbaccess.rs 檔案中新增以下程式碼,來實作使用者資料的存取。

程式碼實作

use crate::errors::EzyTutorError;
use crate::model::*;
use sqlx::postgres::PgPool;

pub async fn get_user_record(pool: &PgPool, username: String) -> Result<User, EzyTutorError> {
    let user_row = sqlx::query_as!(
        User,
        "SELECT * FROM ezyweb_user where username = $1",
        username
    )
    .fetch_optional(pool)
    .await?;
    if let Some(user) = user_row {
        Ok(user)
    } else {
        Err(EzyTutorError::NotFound("User name not found".into()))
    }
}

pub async fn post_new_user(pool: &PgPool, new_user: User) -> Result<User, EzyTutorError> {
    let user_row = sqlx::query_as!(
        User,
        "insert into ezyweb_user (username, tutor_id, user_password) values ($1, $2, $3) returning username, tutor_id, user_password",
        new_user.username, new_user.tutor_id, new_user.user_password
    )
    .fetch_one(pool)
    .await?;
    Ok(user_row)
}

內容解密:

  1. get_user_record 函式用於從資料函式庫中檢索使用者記錄。它接受一個 Postgres 連線池和使用者名稱(主鍵)作為引數。
  2. post_new_user 函式用於建立新使用者。它接受一個 Postgres 連線池和一個 User 型別的物件,包含使用者名稱、導師 ID 和雜湊密碼。
  3. 這兩個函式都使用 sqlx 函式庫與 Postgres 資料函式庫互動,並處理可能的錯誤。

註冊處理器實作

$PROJECT_ROOT/src/iter5/handler.rs 檔案中更新 handle_register 函式,以處理登入檔單的提交。

程式碼實作

use crate::dbaccess::{get_user_record, post_new_user};
use serde_json::json;

pub async fn handle_register(
    tmpl: web::Data<tera::Tera>,
    app_state: web::Data<AppState>,
    params: web::Form<TutorRegisterForm>,
) -> Result<HttpResponse, Error> {
    let mut ctx = tera::Context::new();
    let s;
    let username = params.username.clone();
    let user = get_user_record(&app_state.db, username.to_string()).await;
    let user_not_found: bool = user.is_err();

    if user_not_found {
        if params.password != params.confirmation {
            ctx.insert("error", "Passwords do not match");
            s = tmpl.render("register.html", &ctx).map_err(|_| EzyTutorError::TeraError("Template error".to_string()))?;
        } else {
            // 建立新導師並儲存到資料函式庫
            let new_tutor = json!({
                "tutor_name": params.username,
            });
            let awc_client = awc::Client::default();
            let res = awc_client.post("http://localhost:3000/tutors/").send_json(&new_tutor).await.unwrap().body().await?;
            let tutor_response: TutorResponse = serde_json::from_str(&std::str::from_utf8(&res)?)?;

            // 雜湊密碼並儲存使用者資料
            let salt = b"somerandomsalt";
            let config = Config::default();
            let hash = argon2::hash_encoded(params.password.clone().as_bytes(), salt, &config).unwrap();
            let user = User {
                username: params.username.clone(),
                tutor_id: tutor_response.tutor_id,
                user_password: hash,
            };
            let _tutor_created = post_new_user(&app_state.db, user).await?;
            s = format!("Congratulations. You have been successfully registered with EzyTutor and your tutor id is: {}", tutor_response.tutor_id);
        }
    } else {
        ctx.insert("error", "User Id already exists");
        s = tmpl.render("register.html", &ctx).map_err(|_| EzyTutorError::TeraError("Template error".to_string()))?;
    }
    Ok(HttpResponse::Ok().content_type("text/html").body(s))
}

內容解密:

  1. handle_register 函式處理登入檔單的提交,驗證使用者輸入的資料,並將新使用者儲存到資料函式庫中。
  2. 如果使用者名稱已存在或密碼與確認密碼不符,則傳回錯誤訊息。
  3. 使用 argon2 函式庫對密碼進行雜湊處理,以確保密碼安全。
  4. 將新使用者的資料儲存到 Postgres 資料函式庫中。

測試與驗證

  1. 啟動後端導師 Web 服務:cargo run --bin iter5
  2. 啟動 Web 應用程式:cargo run --bin iter5-ssr
  3. 在瀏覽器中存取 localhost:8080/,填寫登入檔單並點選註冊按鈕。
  4. 如果註冊成功,將顯示成功訊息和導師 ID。

使用者介面改進

目前的實作存在兩個問題:

  1. 在錯誤情況下,需要重複建立表單。
  2. 如果使用者將註冊端點加入書籤,可能會得到空白頁面。

建議將註冊成功後的頁面重新導向到首頁,以改善使用者經驗。

使用者註冊與登入系統的設計與實作

本章節將延續前一章的內容,探討如何使用Rust語言建立一個完整的伺服器端網頁應用程式。我們將涵蓋使用者認證、路由HTTP請求、建立、更新和刪除資源等主題。

使用者註冊功能回顧

在前一章中,我們已經學習瞭如何定義範本、顯示登入檔單、進行瀏覽器端和伺服器端的驗證、傳送HTTP請求到後端網頁服務以及將使用者儲存在本地資料函式庫中。我們自定義了一個錯誤型別來統一錯誤處理,並且學習瞭如何在將密碼儲存到資料函式庫之前對其進行雜湊處理以確保安全性。

改善方向

雖然本章的實作範例是為了教學目的而簡化了許多功能,但對於一個實際的生產環境應用程式來說,還有很多可以改進的地方。不過,本文的重點在於展示如何使用正確的Rust套件來快速啟動這類別應用程式。

使用者輸入驗證

  • 表單中的使用者輸入驗證可以在瀏覽器端或伺服器處理函式中進行。簡單的驗證,如欄位長度檢查,通常由瀏覽器完成,而更複雜的驗證(如使用者名稱是否已經註冊)則在伺服器處理函式中完成。
  • 當使用者提交表單時,瀏覽器會向Actix網頁伺服器上的指定路由傳送一個包含表單資料的POST HTTP請求。

自定義錯誤型別與資料儲存

  • 可以定義自定義錯誤型別來統一網頁應用程式中的錯誤處理。在使用者輸入的表單資料出現錯誤的情況下,處理函式會重新渲染對應的Tera表單範本,並將其連同適當的錯誤訊息一起傳送到瀏覽器。
  • 與使用者管理相關的資料(如使用者名稱和密碼)儲存在網頁應用程式內的本地資料儲存中(本章使用了Postgres資料函式庫)。出於安全考慮,密碼以雜湊形式儲存,而不是明文。

第9章:課程維護表單的實作

本章將涵蓋設計和實作使用者認證、路由HTTP請求、建立、更新和刪除資源等主題。在前一章中,我們探討瞭如何註冊導師。當使用者註冊為導師時,導師的資訊被儲存在兩個資料函式庫中。導師的個人資料詳情,如姓名、影像和專業領域,維護在後端導師網頁服務內的資料函式庫中。使用者註冊詳情,如使用者ID和密碼,則儲存在本地的網頁應用程式內的資料函式庫中。

實作導師登入功能

本章將根據前一章的程式碼繼續開發。我們將撰寫一個Rust前端網頁應用程式,允許使用者登入應用程式,與本地資料函式庫互動,並與後端網頁服務進行通訊。我們的重點將放在撰寫構成網頁應用程式的其他元件上,包括路由、請求處理函式和資料模型,並學習如何呼叫後端網頁服務上的API。

設計使用者認證流程

對於導師登入,我們將接受兩個欄位:使用者名稱和密碼,並使用它們來對導師進行網頁應用程式的認證。圖9.1展示了導師登入表單。

導師登入流程

圖9.2展示了導師登入的工作流程。圖中的Actix網頁伺服器是前端網頁應用程式伺服器,而不是後端導師網頁服務:

  1. 使用者存取首頁URL,顯示導師登入表單。
  2. 使用HTML功能在表單內對使用者名稱和密碼進行基本驗證,無需向Actix網頁伺服器傳送請求。
  3. 如果驗證過程中出現錯誤,向使用者提供反饋。
  4. 使用者提交登入表單。向Actix網頁伺服器上的登入路由傳送POST請求,然後路由請求到相應的路由處理函式。
  5. 路由處理函式透過從本地資料函式庫檢索使用者憑據來驗證使用者名稱和密碼。
  6. 如果認證不成功,則重新顯示登入表單給使用者,並附上適當的錯誤訊息。錯誤訊息的例子包括不正確的使用者名稱或密碼。
  7. 如果使用者成功透過認證,則將其導向導師網頁應用程式的首頁。
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Rust網頁應用程式使用者註冊登入實作

package "Rust 記憶體管理" {
    package "所有權系統" {
        component [Owner] as owner
        component [Borrower &T] as borrow
        component [Mutable &mut T] as mutborrow
    }

    package "生命週期" {
        component [Lifetime 'a] as lifetime
        component [Static 'static] as static_lt
    }

    package "智慧指標" {
        component [Box<T>] as box
        component [Rc<T>] as rc
        component [Arc<T>] as arc
        component [RefCell<T>] as refcell
    }
}

package "記憶體區域" {
    component [Stack] as stack
    component [Heap] as heap
}

owner --> borrow : 不可變借用
owner --> mutborrow : 可變借用
owner --> lifetime : 生命週期標註
box --> heap : 堆積分配
rc --> heap : 引用計數
arc --> heap : 原子引用計數
stack --> owner : 棧上分配

note right of owner
  每個值只有一個所有者
  所有者離開作用域時值被釋放
end note

@enduml

圖表翻譯: 此圖示展示了導師登入的工作流程,從使用者存取首頁到最終登入成功的整個過程。首先,使用者存取首頁並看到登入表單,接著輸入帳號和密碼。系統會對輸入的資訊進行基本驗證,如果驗證失敗則顯示錯誤訊息。如果驗證成功,使用者提交登入表單,伺服器端會再次驗證帳號和密碼。若驗證失敗,同樣顯示錯誤訊息;若驗證成功,則將使用者導向首頁。

9.2 專案結構設定

首先,從第八章節複製 ezytutors 儲存函式庫。然後設定 PROJECT_ROOT 環境變數至 /path-to-folder/ezytutors/tutor-web-app-ssr。往後,我們將此資料夾稱為 $PROJECT_ROOT

讓我們按照以下方式組織專案根目錄下的程式碼:

  1. 複製 $PROJECT_ROOT/src/iter5 資料夾,並重新命名為 $PROJECT_ROOT/src/iter6
  2. 複製 $PROJECT_ROOT/static/iter5 資料夾,並重新命名為 $PROJECT_ROOT/static/iter6。此資料夾將包含 HTML 和 Tera 樣板。
  3. 複製 $PROJECT_ROOT/src/bin/iter5-ssr.rs 檔案,並重新命名為 $PROJECT_ROOT/src/bin/iter6-ssr.rs。此檔案包含 main() 函式,用於組態和啟動 Actix 網頁伺服器(提供我們正在建立的網頁應用程式)。在 iter6-ssr.rs 中,將所有對 iter5 的參照替換為 iter6

同時,請確保 $PROJECT_ROOT 中的 .env 檔案正確組態了 HOST_PORTDATABASE_URL 環境變數。

路由定義

我們準備好開始編寫程式碼。讓我們從 $PROJECT_ROOT/src/iter6/routes.rs 中的路由定義開始:

use crate::handler::{handle_register, show_register_form, show_signin_form, handle_signin};
use actix_files as fs;
use actix_web::web;

pub fn app_config(config: &mut web::ServiceConfig) {
    config.service(
        web::scope("")
            .service(fs::Files::new("/static", "./static").show_files_listing())
            .service(web::resource("/").route(web::get().to(show_register_form)))
            .service(web::resource("/signinform").route(web::get().to(show_signin_form)))
            .service(web::resource("/signin").route(web::post().to(handle_signin)))
            .service(web::resource("/register").route(web::post().to(handle_register))),
    );
}

內容解密:

  • 新增了對 show_signin_formhandle_signin 的匯入。目前尚未編寫這些處理函式。
  • 新增了 /signinform 路由,用於在使用者造訪登入頁面時顯示登入表單。show_signin_form 處理函式(尚未編寫)將向使用者顯示 HTML 表單。
  • 新增了 /signin 路由,用於處理使用者的登入請求。當使用者輸入使用者名稱和密碼並提交登入表單時,將觸發對此路由的 POST HTTP 請求。

模型定義

接下來,讓我們在 $PROJECT_ROOT/src/iter6/model.rs 中定義模型。新增 TutorSigninForm 資料結構至 model.rs

// 用於啟用導師登入的表單
#[derive(Serialize, Deserialize, Debug)]
pub struct TutorSigninForm {
    pub username: String,
    pub password: String,
}

內容解密:

  • 這是一個 Rust 結構體,用於捕捉使用者輸入的使用者名稱和密碼,並使其在處理函式中可用於處理。