Rust axum 中使用 jwt 進行認證

jwt 是 JSON Web Token 的縮寫,它是目前最流行的跨域認證解決方案。jwt 是一個很長的字符串,它由 3 部分組成,分別爲頭部 Header、負載 Payload、簽名 Signature,它們通過. 連接起來。其中,Header 部分是一個 JSON 對象,描述 jwt 的元數據,常用的屬性有:alg 屬性表示簽名的算法,默認是 HS256,即 HMAC SHA256,typ 屬性表示這個令牌的類型,jwt 令牌統一固定爲 JWT。Payload 部分也是一個 JSON 對象,用來存放需要傳遞的數據。JWT 規定了 7 個官方字段,供選用。這 7 個字段爲:iss 簽發人、exp 過期時間、sub 主題、 aud 受衆、 nbf 生效時間 、iat 簽發時間、 jti 編號,除了這些字段外,你還可以定義私有字段,但是,請注意,jwt 是不加密的,任何人都可以讀到,所以不要把祕密信息放在這個部分。Signature 部分是對前兩部分的簽名,防止數據篡改。由此可見,從 jwt 的定義中可見 jwt 本身自包含認證信息及自身識別信息,可以理解爲一張憑證。

我們如何使用 jwt 呢?首先,我們需要一個密鑰,這個密鑰不能泄露給用戶,然後,定義這個 token 攜帶的數據,這個需要定義在 Payload 部分,然後使用 Header 部分指定的簽名算法簽名,將生成的字符串發給用戶的客戶端,客戶端收到服務器返回的 jwt,可以存儲在 cookie 裏面,也可以存儲在 localstorage。使用接口 API 模式下,推薦的做法是放在 HTTP 請求的頭信息 Authorization 字段裏面。還可以將 jwt 放在 post 請求的數據體裏面。無論哪種方式都可以,客戶端每次請求都需要攜帶這個 jwt token,服務端可以根據這個 token 識別客戶端信息,因爲服務端有密鑰,所以可驗證這個 token 是否有效。

我們以 A 大學的學生張三爲例,假設 A 大學給張三一個電子簽名的 jwt token 字符串,張三將這個字符串以二維碼的形式打印,這個 token 中包含張三的姓名、學號、專業等信息。當張三去校圖書館時,管理員可以掃碼不用聯網直接算出張三的姓名、學號等信息,但無法判斷這個信息的真僞,如果要判斷信息的真僞,還是需要連接校園網進行驗證,這是我假想的一個場景,可以充分說明 jwt 的一個憑證性質。這也說明了 jwt 的幾個特點,jwt 默認不加密,你可以在生成原始 token 以後,使用密鑰加密一次,客戶端需對應的進行解密。jwt 不僅可以用於認證,也可以交換信息,有效使用 jwt,可以降低服務器查詢數據庫的次數。jwt 一旦簽發,在到期之前會始終有效。jwt 本身包含認證信息,爲了減少盜用,jwt 的有效期應該設置的比較短,應該使用 https 協議傳輸。對於一些重要的權限,使用時可再次對用戶進行認證。

下面,我們將進入編碼環節,將實現一個只有認證用戶訪問受保護信息,無權用戶無法訪問,使用 jwt 來完成。

在 axum 中,我們先定義一個受保護的 handler,參數爲 claims,Claims 爲自定義的提取器,它能從請求中提取 jwt token 包含的負載,如果提取失敗,將返回認證錯誤。

async fn protected(claims: Claims) -> Result<String, AuthError> {
    // Send the protected data to the user
    Ok(format!(
        "Welcome to the protected area :)\nYour data:\n{}",
        claims
    ))
}

claims 爲自定義的憑證,它包含了暴漏給用戶的信息,我們還需要爲 claims 實現自定義的提取器,這裏不需要 body,所以實現了 from_request_parts,當需要請求 body 內容時,請實現 from_request,將請求中的內容直接提取出來並返回給參數。

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    company: 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> {
        // Extract the token from the authorization header
        let TypedHeader(Authorization(bearer)) = parts
            .extract::<TypedHeader<Authorization<Bearer>>>()
            .await
            .map_err(|_| AuthError::InvalidToken)?;
        // Decode the user data
        let token_data = decode::<Claims>(bearer.token(), &KEYS.decoding, &Validation::default())
            .map_err(|_| AuthError::InvalidToken)?;
        Ok(token_data.claims)
    }
}

我們將返回的錯誤統一定義爲認證錯誤,認證錯誤包含認證無效、缺失、已到期等。我們還需要實現 into response,這樣返回的錯誤,可以直接在 axum 響應體中使用。

#[derive(Debug)]
enum AuthError {
    WrongCredentials,
    MissingCredentials,
    TokenCreation,
    InvalidToken,
}
impl IntoResponse for AuthError {
    fn into_response(self) -> Response {
        let (status, error_message) = match self {
            AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "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()
    }
}

我們定義獲取 jwt 認證憑證的入口,只有通過鑑權後的用戶才能獲取 jwt 認證憑證,authPayload 包含用戶名和密碼,authBody 包含 jwt token 和類型。

#[derive(Debug, Deserialize)]
struct AuthPayload {
    client_id: String,
    client_secret: String,
}
#[derive(Debug, Serialize)]
struct AuthBody {
    access_token: String,
    token_type: String,
}
async fn authorize(Json(payload): Json<AuthPayload>) -> Result<Json<AuthBody>, AuthError> {
    // Check if the user sent the credentials
    if payload.client_id.is_empty() || payload.client_secret.is_empty() {
        return Err(AuthError::MissingCredentials);
    }
    // Here you can check the user credentials from a database
    if payload.client_id != "foo" || payload.client_secret != "bar" {
        return Err(AuthError::WrongCredentials);
    }
    let claims = Claims {
        sub: "b@b.com".to_owned(),
        company: "ACME".to_owned(),
        // Mandatory expiry time as UTC timestamp
        exp: 2000000000, // May 2033
    };
    // Create the authorization token
    let token = encode(&Header::default(), &claims, &KEYS.encoding)
        .map_err(|_| AuthError::TokenCreation)?;
    // Send the authorized token
    Ok(Json(AuthBody::new(token)))
}

總結,jwt 的介紹以及在 axum 中如何使用,上面已經講解完畢,我們需要根據自己的業務場景來合適的使用。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2HPoPErryZBazg_vEYzmLQ