透過昨天建立的Login API,使用者登入後,我們需要在特定的API驗證cookie中JWT token的有效性,這一點需要靠中介軟體來達到。通常情況下我們不用真的從頭實做一個中介層,歸功於與tower
生態系的整合,可以找找看有沒有符合需求的元件來使用,我們需要開啟tower-http
的validate-request
:
cargo add tower-http --features validate-request
根據文件中的說明,我們可以使用ValidateRequestHeaderLayer
,並帶入自定義的驗證,帶入的驗證需要實做ValidateRequest
trait:
pub trait ValidateRequest<B> {
type ResponseBody;
// Required method
fn validate(
&mut self,
request: &mut Request<B>
) -> Result<(), Response<Self::ResponseBody>>;
}
這邊在嘗試的時候遇到一點小問題,ResponseBody的關聯型別需要使用axum::body::BoxBody
。接下來我們先建立驗證器的struct:
#[derive(Clone)]
pub struct Authorization {
pub jwt_handler: JwtHandler,
}
因為驗證的時候會需要用到前面建立的JwtHandler,所以驗證器需要包含它。
impl<B> ValidateRequest<B> for Authorization {
type ResponseBody = BoxBody;
fn validate(&mut self, request: &mut Request<B>) -> Result<(), Response<Self::ResponseBody>> {
request
.headers()
.get(header::COOKIE)
.and_then(|cookie| cookie.to_str().ok())
.and_then(|cookie| cookie.split(';').find(|cookie| cookie.contains("token")))
.and_then(|cookie| cookie.split('=').nth(1))
//// 驗證token
.map(|token| self.jwt_handler.clone().decode_token(token.to_string()))
.map(|result| match result {
Ok(_) => Ok(()),
Err(e) => {
if e.kind().eq(&ErrorKind::ExpiredSignature) {
Err((
StatusCode::UNAUTHORIZED,
"Token expired, please login again",
)
.into_response())
} else {
Err(StatusCode::UNAUTHORIZED.into_response())
}
}
})
.unwrap_or_else(|| {
warn!("Missing authorization header");
Err(StatusCode::UNAUTHORIZED.into_response())
})
}
}
每次的HTTP請求會以request傳遞進入方法中,這個參數是可變的借用,但在驗證過程中我們不會去修改它。接下來我們要從request中取出header,並連續使用幾個map
來取得token。使用jwt_handler驗證token後再針對不同的情境顯示處裡,這裡可以看到jwtbeartoken
回傳的錯誤型別其實是一個enum,可以表示不同驗證失敗的原因。
最後就是把middleware加到路由裡面了:
//// application.rs
pub fn build() -> Router {
let database = get_database(&config.database).await.unwrap();
let email_client = get_email_client(&config.email_client);
let jwt_handler = get_jwt_handler(&config.jwt_handler);
let app = Router::new()
.route("/", get(health_check))
//// 加入權限驗證的中介層
.layer(ValidateRequestHeaderLayer::custom(auth))
.route("/subscriptions", post(subscribe))
.route("/subscriptions/confirm/:token", get(confirm))
.route("/login", post(login))
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
.with_state(AppState {
database,
email_client,
jwt_handler,
});;
app
}
前面有提到middleware的運作模式會由下而上,所以需要權限驗證的API要放在ValidateRequestHeaderLayer
之上,這邊可以簡單的用health_check做登入的測試。
專案到這邊就告一段落了,原本預計要再做一隻API用來發布新文章通知,因為這是給部落格作者使用的強大API,需要權限控管,也才有了這三篇關於權限驗證的內容,不過發布通知的內容其實跟前面訂閱API需要知道的東西大同小異,舊不多做介紹了。寫到這邊我覺得我的介紹近乎流水帳的把自己side project的內容貼過來,明天我想要聊聊對於rust進行Web API開發的想法,剩下的四篇文章在想想要寫些什麼額外的東西吧!