把昨天包裝好的JWTHandler
加入AppState
後,就要來完成Login API了,首先我們要建立一張新的表用來放管理員的帳號密碼:
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Users::Table)
.if_not_exists()
.col(ColumnDef::new(Users::Id).uuid().not_null().primary_key())
.col(
ColumnDef::new(Users::UserName)
.string()
.not_null()
.unique_key(),
)
.col(ColumnDef::new(Users::Password).string())
.col(
ColumnDef::new(Users::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await
}
接下來就開始完成handler,首先要用user_name
找到管理員的資訊:
pub async fn login(
state: State<AppState>,
Json(user): Json<User>,
) -> Result<StatusCode, StatusCode> {
let user_model = users::Entity::find()
.filter(users::Column::UserName.eq(user.user_name))
.one(&state.database)
.await?
.ok_or(StatusCode::NOT_FOUND)?;
todo!();
}
接下來要進行密碼驗證,為了安全起見我們通常不將密碼以明碼的方式存在資料庫中,而是使用雜湊過後的密碼進行比對,先安裝套件:
cargo add bcrypt
使用上也十分簡單,只要使用verify
方法即可:
pub async fn login(
state: State<AppState>,
Json(user): Json<User>,
) -> Result<StatusCode, StatusCode> {
let user_model = users::Entity::find()
.filter(users::Column::UserName.eq(user.user_name))
.one(&state.database)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
let valid = verify(user.password, &user_model.password.unwrap())
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
todo!();
}
valid
是一個bool,表示是否驗證成功,如果驗證成功的話,我們就要產生token並透過cookie的方式傳到client端:
pub async fn login(
state: State<AppState>,
Json(user): Json<User>,
) -> Result<(StatusCode, HeaderMap), StatusCode> {
let user_model = users::Entity::find()
.filter(users::Column::UserName.eq(user.user_name))
.one(&state.database)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
let valid = verify(user.password, &user_model.password.unwrap())
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if valid {
let token = state
.jwt_handler
.clone()
.create_token(&user_model.user_name);
let cookie = Cookie::build("token", token.to_owned())
.http_only(true)
.finish();
let mut headers = HeaderMap::new();
headers.insert(SET_COOKIE, cookie.to_string().parse().unwrap());
Ok((StatusCode::OK, headers))
} else {
Err(StatusCode::BAD_REQUEST)
}
}
可以用AppState
的方式傳遞jwt_handler,生成token後再用Axum充提供的Cookie Builder來產生Cookie的內容,最後再Axum中如果要傳遞Cookie的話,則是要將內容以HeaderMap作為Handler的回傳值,所以最後需要將這個Handler的回傳值改成Result<(StatusCode, HeaderMap), StatusCode>
。
最後再把這隻API加入路由即可:
//// 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))
.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
}