iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Rust

Rust 後端入門系列 第 24

Day 24 Axum專案加入JWT與驗證

  • 分享至 

  • xImage
  •  

這篇文章將教你:

  • 在登入時簽發 JWT(access token)
  • 用自訂 extractor 驗證 JWT(保護需要授權的 route)
  • 建議的密鑰管理、過期與撤銷(revocation)策略

為什麼要用 JWT

  1. Stateless 驗證效能
    • JWT 把必要資訊放在 token 裡,server 驗證只要驗簽名與 exp,就能授權,減少每次請求查 DB 的需求(除非你需要撤銷/權限即時變更)。
  2. 可擴充性(scalability)
    • 多台服務器只要共享簽名密鑰即可驗證 token,適合分散式系統或 microservice(不同 service 可共用同一個認證 token)。
  3. 自包含(claims)
    • 可以把 role、scope 等放在 claims,service 直接根據 token 做授權檢查,減少每次查 DB。
  4. 簽發/失效策略彈性
    • 透過短期 access + refresh token,兼顧安全與良好使用者體驗(少要求使用者頻繁登入)。

實作

新增 Cargo 依賴

在 Cargo.toml 加:

[dependencies]
jsonwebtoken = "9.3"
chrono = { version = "0.4", features = ["serde"] }
futures = "0.3"

導入 JWT(claims、簽發、驗證)

新增一個檔案 src/auth.rs:

use chrono::{Utc, Duration};
use serde::{Serialize, Deserialize};
use jsonwebtoken::{EncodingKey, Header, DecodingKey, Validation, encode, decode, TokenData, errors::Result as JwtResult};

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,      // subject,例如 user id
    pub username: String, // 放 username 方便前端顯示
    pub exp: i64,         // 失效時間,用UNIX timestamp表示
}

/// 產生 access token
pub fn sign_access_token(secret: &str, user_id: i64, username: &str, minutes: i64) -> anyhow::Result<String> {
    let exp = Utc::now()
        .checked_add_signed(Duration::minutes(minutes))
        .ok_or_else(|| anyhow::anyhow!("invalid exp"))?
        .timestamp();

    let claims = Claims {
        sub: user_id.to_string(),
        username: username.to_string(),
        exp,
    };

    let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
        .map_err(|e| anyhow::anyhow!("token sign error: {}", e))?;
    Ok(token)
}

/// 驗證 token 並回傳 Claims
pub fn validate_token(secret: &str, token: &str) -> JwtResult<TokenData<Claims>> {
    decode::<Claims>(token, &DecodingKey::from_secret(secret.as_bytes()), &Validation::default())
}

在 login handler 裡簽發 token

修改 handlers::login,當驗證密碼成功後簽發 JWT 並回傳給 client(通常回傳 JSON 內含 token):

在 handlers.rs 頂端加上:

use crate::auth::{sign_access_token};
use std::env;

修改 login handler 的最後部分:

// 3. 產生 JWT(access token)
	let jwt_secret = env::var("JWT_SECRET").unwrap_or_else(|_| "set_jwt_secret".to_string());
	// access token 有效 15 分鐘(可用 env 設定)
	let access_token = sign_access_token(&jwt_secret, user.id, &user.username, 15)
		.map_err(|e| internal_err(e))?;

	Ok((StatusCode::OK, Json(json!({"access_token": access_token, "token_type": "bearer", "expires_in": 15*60}))))
  

把 JWT_SECRET 設在 .env(測試或開發環境),生產環境請從安全的地方(KMS / vault / env)讀取。

建立一個自訂 extractor 用於保護路由

新增檔案 src/extractors.rs ,建立一個 FromRequest 的 extractor。

use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use axum::http::StatusCode;
use crate::auth::{validate_token, Claims};
use std::env;
use futures::future::{BoxFuture, FutureExt};

pub struct AuthenticatedUser(pub Claims);

impl<S> FromRequestParts<S> for AuthenticatedUser
where
    S: Send + Sync,
{
    type Rejection = (StatusCode, String);

    fn from_request_parts(parts: &mut Parts, _state: &S) -> BoxFuture<'static, Result<Self, Self::Rejection>> {
        // 同步地從 parts.headers 取出 Authorization header 的 String(或錯誤訊息)
        let token_opt: Result<String, (StatusCode, String)> = parts
            .headers
            .get(axum::http::header::AUTHORIZATION)
            .and_then(|v| v.to_str().ok())
            .ok_or_else(|| (StatusCode::UNAUTHORIZED, "missing authorization".to_string()))
            .and_then(|auth| {
                auth
                    .strip_prefix("Bearer ")
                    .map(|s| s.to_string())
                    .ok_or_else(|| (StatusCode::UNAUTHORIZED, "invalid authorization scheme".to_string()))
            });

        // 讀取 env(也是同步)
        let jwt_secret = env::var("JWT_SECRET").unwrap_or_else(|_| "set_jwt_secret".to_string());

        // 將同步處理的結果移入 async block
        async move {
            let token = match token_opt {
                Ok(t) => t,
                Err(e) => return Err(e),
            };

            match validate_token(&jwt_secret, &token) {
                Ok(data) => Ok(AuthenticatedUser(data.claims)),
                Err(_) => Err((StatusCode::UNAUTHORIZED, "invalid token".to_string())),
            }
        }
        .boxed()
    }
}

使用這個 extractor 來保護 route(例如只允許已登入的 user 取得自己的資料)

在 handlers.rs 新增一個需要授權的 handler:

use crate::extractors::AuthenticatedUser;

pub async fn myid(AuthenticatedUser(claims): AuthenticatedUser) -> Result<impl IntoResponse, AppError> {
    // claims.sub 是 user id(字串)
    Ok((StatusCode::OK, Json(json!({"id": claims.sub, "username": claims.username}))))
}

並在 main.rs 把 route 跟 auth extractors 加上:

mod auth;
mod extractors;

.route("/myid", get(handlers::myid))

這樣呼叫 /myid時必須在 header 帶 Authorization: Bearer ,extractor 會驗證並把 claims 放到 handler。

lib.rs加上:

pub mod auth;
pub mod extractors;

選擇把 token 放哪裡給前端

  • 回傳 JSON:前端在 Authorization header 放 "Bearer ",我們採用的就是這種方式。
  • 設 HttpOnly cookie:可以減少 XSS 竊取(但要注意 CSRF,或把 cookie 設 SameSite=strict 並同時採用 CSRF token)。對 SPA 常見做法是把 access token 存 memory(風險較高)或短命 access + HttpOnly refresh cookie。

Token 撤銷

JWT 本質是 stateless,無法直接把已簽發 token 立刻 invalidated。若要能撤銷(例如登出、修改權限時應立即失效),常見策略:

  • 使用短命 access token(15 分鐘),搭配 refresh token(存在 DB/Redis,若要撤銷就刪除 refresh token)。
  • 將 token 的 jti(唯一 id)或 user 的 session id 記在 Redis(黑名單或白名單),在驗證時檢查是否被撤銷。這會讓驗證變 stateful,但能立即失效。
  • 在密鑰輪替(key rotation)或主動撤銷時,將舊 key 停用。

測試

取得JWT

POST http://127.0.0.1:3000/users/login

{

"username_or_email": "username",
"password": "password"

}

回傳的結果如下:

{
	"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMCIsInVzZXJuYW1lIjoidXNlcm5hbWUiLCJleHAiOjE3NTk4NDMxNTJ9.NHE51qQUryK0stjhdEv15IDr7Wyv-rg55GxE2Y0jAMA",
	"expires_in": 900,
	"token_type": "bearer"
}

現在用取得的JWT來讀取保護路由

GET http://127.0.0.1:3000/myid
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMCIsInVzZXJuYW1lIjoidXNlcm5hbWUiLCJleHAiOjE3NTk4NDMxNTJ9.NHE51qQUryK0stjhdEv15IDr7Wyv-rg55GxE2Y0jAMA

回傳id和用戶名

{
	"id": "10",
	"username": "username"
}

實務建議與注意事項

  • 密鑰管理
    • JWT secret 是關鍵資安元素,不可硬編在程式碼。生產環境建議放在 vault 或至少用 env 變數,並定期輪替(key rotation)。
  • token 有效期限
    • access token 建議短期限(10~30 分鐘),refresh token 較長(幾天到幾個月),refresh token 建議保存在 HttpOnly cookie 或 DB,並可撤銷。
  • 儲存位置與攻擊面
    • 若把 token 存 localStorage,會被 XSS 風險影響;若用 cookie,需防 CSRF(可用 SameSite、CSRF token 等)。
  • 檢查 exp(過期)
    • 驗證時一定要檢查 exp。 jsonwebtoken crate 會自動處理 exp 如果你在 claims 設定了 exp。
  • 角色與授權
    • 若有細緻權限,建議除了驗證 token 外,還要在 handler 裡根據 claims 或 DB 做授權(authorization)。

上一篇
Day 23 Axum 專案整合測試
下一篇
Day 25 Axum 限制只有使用者能修改自己的資料
系列文
Rust 後端入門25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言