這篇文章將教你:
在 Cargo.toml 加:
[dependencies]
jsonwebtoken = "9.3"
chrono = { version = "0.4", features = ["serde"] }
futures = "0.3"
新增一個檔案 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())
}
修改 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)讀取。
新增檔案 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;
JWT 本質是 stateless,無法直接把已簽發 token 立刻 invalidated。若要能撤銷(例如登出、修改權限時應立即失效),常見策略:
取得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"
}