iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
Rust

Rust 後端入門系列 第 25

Day 25 Axum 限制只有使用者能修改自己的資料

  • 分享至 

  • xImage
  •  

目標

  • 認證(Authentication)由 JWT 與 AuthenticatedUser 負責:確認「你是誰」。
  • 授權(Authorization)要做的事:確認「你可以做什麼」,本範例目標是「只有資源擁有者(user 自己)能更新/刪除自己的資料」。
  • 採用方式:在 handler 開頭比對 JWT claims 的 sub(user id)與路由 path 的 id,若相同才允許,否則回 403。

為什麼要做授權

  • 資料隔離與隱私:避免使用者 A 修改或刪除使用者 B 的個資。
  • 安全與可信任性:任何能修改他人資料的 API 必須受控,否則會造成資料被惡意竄改。
  • 易擴充:Owner check(擁有者檢查)很容易擴充成角色(admin/moderator)或更細緻的 permission(scope)。
  • 可測試與可稽核:明確的授權條件容易寫測試與稽核日誌。

實作步驟

  1. 修改需要授權的 handler(update_user、delete_user 等)讓它們接受 AuthenticatedUser extractor,解析 claims。
  2. 在 handler 開頭做授權檢查:
    • 若 claims.sub 可以 parse 成路由 id -> owner 承認。
    • 否則回 403 Forbidden。
  3. 為避免 TOCTOU,可把 owner 條件也寫在 SQL 的 WHERE 子句,並檢查 rows_affected()。

什麼是 race condition / TOCTOU

  • race condition:多個執行緒或程序同時存取或改變同一資源,執行的相對時序會影響最終結果。當不同步處理時,系統行為會變得不確定或錯誤。
  • TOCTOU:一種常見的競態類型。程式先「檢查(check)」某個條件(例如:呼叫者是資源擁有者嗎?資源是否存在?),之後在另一段時間內「使用(use)」該資源。如果在檢查和使用之間狀態被別的執行緒改變,就會產生錯誤或安全問題。

範例

  • 程式 A 檢查「user X 存在且 caller 是 owner」,然後做刪除。
  • 在檢查到實際刪除之間,程式 B 也改變或刪除了該 user,或把 owner 權限轉交給其他人。A 的刪除操作可能不該被允許,但仍然發生。

後果

  • 資料非法讀寫或刪除(安全漏洞)。
  • 不一致資料(部分更新、快取與 DB 不一致)。
  • 權限繞過或被竄改的商業邏輯。
  • 發生隱蔽、難以重現的錯誤(因為需要特定時序)。

需要修改的地方(具體程式碼範例)

  1. 修改 update_user:接收 AuthenticatedUser 並做 owner 檢查

    use crate::extractors::AuthenticatedUser;
    
    pub async fn update_user(
        Extension(pool): Extension<PgPool>,
        Extension(mut redis): Extension<MultiplexedConnection>,
        AuthenticatedUser(claims): AuthenticatedUser,   // <- 新增
        Path(id): Path<i64>,
        Json(payload): Json<UpdateUser>,
    ) -> Result<impl IntoResponse, AppError> {
        // 1) 授權檢查:只有 owner 可以更新
        let caller_id: i64 = claims.sub.parse().map_err(|_| (StatusCode::UNAUTHORIZED, "invalid token sub".to_string()))?;
    
        if caller_id != id {
            return Err((StatusCode::FORBIDDEN, "forbidden".to_string()));
        }
    
        // 原先更新邏輯(查 existing、hash password、UPDATE ...)
        let existing = sqlx::query_as::<_, User>("SELECT id, username, email, password_hash, created_at, updated_at FROM users WHERE id = $1")
            .bind(id)
            .fetch_optional(&pool)
            .await
            .map_err(|e| internal_err(e))?;
    
    }
    
  2. 修改 delete_user:同樣加入 AuthenticatedUser 與授權檢查

    pub async fn delete_user(
        Extension(pool): Extension<PgPool>,
        Extension(mut redis): Extension<MultiplexedConnection>,
        AuthenticatedUser(claims): AuthenticatedUser,  // <- 新增
        Path(id): Path<i64>,
    ) -> Result<impl IntoResponse, AppError> {
        let caller_id: i64 = claims.sub.parse().map_err(|_| (StatusCode::UNAUTHORIZED, "invalid token sub".to_string()))?;
    
        if caller_id != id {
            return Err((StatusCode::FORBIDDEN, "forbidden".to_string()));
        }
    
        let res = sqlx::query!(
            r#"
            DELETE FROM users
            WHERE id = $1
            "#,
            id
        )
        .execute(&pool)
        .await
        .map_err(|e| internal_err(e))?;
    
    }
    

另外的強化:把 owner 驗證放到 SQL

  • 若擔心 race condition(TOCTOU),可以在 SQL 的 WHERE 同時帶上 caller_id,例如(DELETE ):

    let res = sqlx::query!(
        r#"
        DELETE FROM users
        WHERE id = $1 AND id = $2
        "#,
        id, caller_id
    )
    .execute(&pool)
    .await
    .map_err(|e| internal_err(e))?;
    

測試

  • owner 更新成功(帶有效 token,claims.sub == path id) -> 200
  • 其他使用者更新(claims.sub != path id) -> 403
  • 沒有 token 或 token invalid -> 401
  • token 的 sub 無法 parse -> 401
  • 刪除不存在的 user -> 404

優點總結

  • 資料安全:避免横向越權(horizontal privilege escalation),確保使用者只能操作自己的資源。
  • 可擴充性:容易加上 role(admin)或更細粒度的 permission(scope)。
  • 減少錯誤與漏洞:中心化的授權檢查讓程式碼更容易審查、測試與維護。
  • 適合分散式系統:使用 JWT 做 stateless 驗證,配合 role/claims 可跨服務驗證與授權。

上一篇
Day 24 Axum專案加入JWT與驗證
系列文
Rust 後端入門25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言