Rust中實現JWT身份驗證

banq發表於2024-02-24

我們將討論如何在 Rust 中使用 JSON Web Tokens (JWT) 實現身份驗證。

什麼是 JWT?
JSON Web 令牌 (JWT) 是一種緊湊、URL 安全的方式,用於透過 Web 在兩方之間傳輸資料(“宣告”)。資料通常使用 JSON Web 簽名進行編碼或作為 JSON Web 加密 (JWE) 結構的一部分,並且可以加密(和簽名!)。

在決定身份驗證策略時,JWT 是一個流行的選擇。客戶端透過 JWT 儲存所有資訊,從而實現無狀態 API。在某些情況下,這可以使使用者身份驗證變得更加容易。雖然未加密和未簽名的 JWT 可以被操縱,但只要用於建立 JWT 的金鑰保持秘密,伺服器就很容易忽略被操縱的 JWT。

依賴
首先,讓我們使用 初始化一個專案cargo shuttle init,確保選擇 Axum 作為框架。

然後我們將新增我們的依賴項:


cargo add axum-extra@0.9.2 -F typed-header
cargo add chrono@0.4.34 -F serde,clock
cargo add jsonwebtoken@9.2.0
cargo add once_cell@1.19.0
cargo add serde@1.0.196 -F derive
cargo add serde-json@1.0.113


設定金鑰
首先,我們需要宣告一個儲存解碼和編碼金鑰的結構體,並使用一個可以接收 &[u8] (u8 片)的方法來生成結構體:

use jsonwebtoken::{DecodingKey, EncodingKey};

struct Keys {
    encoding: EncodingKey,
    decoding: DecodingKey,
}

impl Keys {
    fn new(secret: &[u8]) -> Self {
        Self {
            encoding: EncodingKey::from_secret(secret),
            decoding: DecodingKey::from_secret(secret),
        }
    }
}

該結構需要從秘鑰生成,因為我們將用它來生成 JWT。在本例中,我們將從字串隨機生成位元組,然後將其轉化為位元組。然後,它將儲存在 once_cell::LazyCell 中,可以在我們的應用程式中全域性訪問:

use once_cell::sync::Lazy;

static KEYS: Lazy<Keys> = Lazy::new(|| {
    let secret = Alphanumeric.sample_string(&mut rand::thread_rng(), 60);
    Keys::new(secret.as_bytes())
});

請注意,生成位元組陣列的來源有很多種,生成隨機字串並將其轉化為位元組只是其中之一。對於生產用途,您可能還需要使用加密安全的演算法。

編寫我們的 JWT 宣告
下一步是實現我們的請求。聲稱(在 JWT 上下文中)是 JWT 傳輸的資料,由伺服器進行編碼或解碼。我們可以建立一個結構來儲存使用者名稱和到期日期,然後為該結構實現 FromRequestParts 特性(來自 Axum),從而編寫自己的索賠實現。這樣,我們就可以將其用作 Axum 提取器,而無需實現任何中介軟體!

不過,在編寫實際實現之前,FromRequestParts 要求我們有一個自定義錯誤型別。我們可以編寫一個表示 JWT 失敗的錯誤型別,併為其實現 IntoResponse - 這樣我們就可以在實現中使用它了。

use axum::response::{ IntoResponse, Response };
use axum::http::StatusCode;
use serde_json::json;

pub enum AuthError {
    InvalidToken,
    WrongCredentials,
    TokenCreation,
    MissingCredentials,
}

impl IntoResponse for AuthError {
    fn into_response(self) -> Response {
        let (status, error_message) = match self {
            AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, <font>"Wrong credentials"),
            AuthError::MissingCredentials => (StatusCode::BAD_REQUEST,
"Missing credentials"),
            AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR,
"Token creation error"),
            AuthError::InvalidToken => (StatusCode::BAD_REQUEST,
"Invalid token"),
        };
        let body = Json(json!({
           
"error": error_message,
        }));
        (status, body).into_response()
    }
}

為 AuthError 實現 IntoResponse 可將其用作 FromRequestParts 特質中的拒絕型別。請注意,為了能在 FromPartsRequest 特質中返回 AuthError,我們使用了 map_err,將錯誤型別轉化為 AuthError,以便它能被傳播。我們在這裡還使用了去結構化技術,從 TypedHeader<Authorization<Bearer>> 型別中提取 bearer 結構,因為它更易於訪問。

use serde::{ Serialize, Deserialize };
use axum::{ http::{ request::Parts }, extract::FromRequestParts, RequestPartsExt };

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    username: String,
    exp: usize,
}

#[async_trait]
impl<S> FromRequestParts<S> for Claims where S: Send + Sync {
    type Rejection = AuthError;

    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
        <font>// 從授權標頭提取令牌<i>
        let TypedHeader(Authorization(bearer)) = parts
            .extract::<TypedHeader<Authorization<Bearer>>>().await
            .map_err(|_| AuthError::InvalidToken)?;
       
// 解碼使用者資料<i>
        let token_data = decode::<Claims>(
            bearer.token(),
            &KEYS.decoding,
            &Validation::default()
        ).map_err(|_| AuthError::InvalidToken)?;

        Ok(token_data.claims)
    }
}

現在,我們已經完成了 JWT 執行方式的模板設定,接下來就可以建立路由了!

建立路由
下一步是在授權使用者時編寫端點--在返回令牌時,我們將建立一個新的 AuthBody,其中包含令牌和令牌型別。稍後我們將使用它:

#[derive(Debug, Serialize)]
struct AuthBody {
    access_token: String,
    token_type: String,
}

impl AuthBody {
    fn new(access_token: String) -> Self {
        Self {
            access_token,
            token_type: <font>"Bearer".to_string(),
        }
    }
}

現在我們已經建立了 AuthBody,可以建立一個端點,它將接收客戶端 ID 和密文並進行驗證。然後,它會建立一個請求,對其進行編碼並以 JSON 格式返回。

use axum::Json;
use chrono::Utc;

#[derive(Debug, Deserialize)]
struct AuthPayload {
    client_id: String,
    client_secret: String,
}

async fn authorize(Json(payload): Json<AuthPayload>) -> Result<Json<AuthBody>, AuthError> {
    <font>//檢查使用者是否傳送了證書<i>
    if payload.client_id.is_empty() || payload.client_secret.is_empty() {
        return Err(AuthError::MissingCredentials);
    }
   
// 這裡使用的是基本驗證,但通常會使用資料庫<i>
    if &payload.client_id !=
"foo" || &payload.client_secret != "bar" {
        return Err(AuthError::WrongCredentials);
    }

   
// 為過期時間建立時間戳--此處的過期時間為 1 天<i>
   
// 在生產中,您可能不希望 JWT 的有效期如此之長<i>
    let exp = (Utc::now().naive_utc() + chrono::naive::Days::new(1)).timestamp() as usize;
    let claims = Claims {
        username: payload.client_id,
        exp,
    };
   
// 建立授權令牌<i>
    let token = encode(&Header::default(), &claims, &KEYS.encoding).map_err(
        |_| AuthError::TokenCreation
    )?;

   
// 傳送授權令牌<i>
    Ok(Json(AuthBody::new(token)))
}

現在我們有了生成 JWT 的路由,可以建立一個路由來試用我們的令牌了!

async fn protected(claims: Claims) -> String {
    <font>// 向使用者傳送受保護的資料<i>
    format!(
"Welcome to the protected area, {}!", claims.username)
}

現在,所有路由都已編寫完畢,我們可以將其連線到主函式:

use axum::{Router, routing::{get, post}};

#[shuttle_runtime::main]
async fn main() -> shuttle_axum::ShuttleAxum {
    let router = Router::new()
        .route(<font>"/", get(hello_world))
        .route(
"/protected", get(protected))
        .route(
"/login", post(authorize));

    Ok(router.into())
}

部署
現在我們已經編寫了整個專案,可以進行部署了!鍵入 cargo shuttle deploy 並按Enter鍵(如果在髒 Git 分支上,則新增--ad 標誌,以允許髒部署)。完成後,我們的終端應該會列印出有關部署和專案的所有資料,以及一個指向實時專案的連結!

有興趣擴充套件這個專案嗎?這裡有幾個想法:

  • 使用 Postgres 儲存使用者登入資訊。
  • 嘗試使用加密和簽名來加強 JWT,並將其儲存在 cookie 中。
  • 嘗試新增整合測試,這樣就不需要手動測試了!


 

相關文章