本章实现后台管理的鉴权,以及管理员的登录、注销功能。涉及的知识点有:cookie及中间件等。
数据库结构
CREATE TABLE admins (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
is_del BOOLEAN NOT NULL DEFAULT FALSE
);
字段 | 说明 |
---|---|
id |
主键,唯一标识,自动编号 |
email |
管理员邮箱 |
password |
加密后的管理员密码 |
is_del |
是否删除 |
初始数据
INSERT INTO admins(email,password) VALUES('[email protected]', '$2b$12$OljS3FqwxaYXESzu6F0ZRevgBrt9ueY.7NNzdsMOaJk0YoGD5aTii');
为了方便使用,我们插入一条初始数据作为默认管理员:
- 邮箱:
[email protected]
- 密码:
axum.rs
数据模型
// src/model.rs
#[derive(PostgresMapper, Serialize)]
#[pg_mapper(table="admins")]
pub struct Admin {
pub id:i32,
pub email:String,
pub password:String,
pub is_del:bool,
}
该数据模型的字段与数据表结构一一对应。
数据库操作
// src/db/admin.rs
pub async fn find(client:&Client, email: &str) -> Result<Admin> {
super::query_row(client, "SELECT id,email,password,is_del FROM admins WHERE email=$1 AND is_del=false", &[&email]).await
}
find()
:通过邮箱查找对应的管理员
模板
新加的模板位于templates/backend/admin。未涉及新知识,请自行在源码仓库查看。
视图类
新加的视图类位于src/view/auth.rs。未涉及新知识,请自动在源码仓库查看。
handler
// src/handler/auth.rs
pub async fn login_ui() -> Result<HtmlView> {
let handler_name = "auth/login_ui";
let tmpl = Login {};
render(tmpl).map_err(log_error(handler_name))
}
pub async fn login(
Extension(state): Extension<Arc<AppState>>,
Form(frm): Form<AdminLogin>,
) -> Result<RedirectView> {
let handler_name = "auth/login";
tracing::debug!("{}", password::hash("axum.rs")?);
let client = get_client(&state).await.map_err(log_error(handler_name))?;
let admin_info = admin::find(&client, &frm.email)
.await
.map_err(|err| match err.types {
AppErrorType::Notfound => AppError::incorrect_login(),
_ => err,
})
.map_err(log_error(handler_name))?;
let verify =
password::verify(&frm.password, &admin_info.password).map_err(log_error(handler_name))?;
if !verify {
return Err(AppError::incorrect_login());
}
redirect_with_cookie("/admin", Some(&admin_info.email))
}
pub async fn logout() -> Result<RedirectView> {
redirect_with_cookie("/auth", Some(""))
}
login_ui()
:渲染登录页面login()
:处理登录逻辑- 调用了
password::verify()
对密码进行验证。有关新增的password
模块,请查看下文的“密码加密与验证”部分。 - 调用了
redirect_with_cookie()
进行带cookie的跳转。该函数将在下文的Cookie
部分进行说明。
- 调用了
logout()
:注销登录。实质是将cookie设置为空字符串。
路由
// src/handler/frontend/mod.rs
pub fn router()->Router {
Router::new().route("/", get(index::index))
.route("/auth", get(login_ui).post(login))
.route("/logout", get(logout))
}
注意,基于以下两个原因,需要将登录的路由注册到前台路由上:
- 因为前台路由的前缀是
/
,只有这样,登录之后设置Cookie才有效 - 因为登录是不需要鉴权的,所以不能注册到后台路由上
中间件
// src/middleware.rs
pub struct Auth(pub String) ;
#[async_trait]
impl<B> FromRequest<B> for Auth
where
B: Send,
{
type Rejection = AppError;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
let headers = req.headers().unwrap();
let cookie = cookie::get_cookie(headers);
let auth = cookie.unwrap_or("".to_string());
if auth.is_empty() {
return Err(AppError::forbidden());
}
Ok(Auth(auth))
}
}
- 从请求头中获取cookie
- 如果没有我们设置的cookie或者该cookie的值为空,返回
AppError::forbidden()
,这种错误会导致浏览器重新定向到登录页面。实现原理参见下文的AppError
部分 cookie
模块请看下文的“Cookie”部分
应用中间件
定义好中间件好,需要将它应用到后台路由上,以便保护后台管理:
// main.rs
let backend_routers = backend::router().layer(extractor_middleware::<middleware::Auth>());
密码加密与验证
// src/password.rs
pub fn hash(pwd: &str) -> Result<String> {
bcrypt::hash(pwd, DEFAULT_COST).map_err(AppError::from)
}
pub fn verify(pwd: &str, hashed_pwd: &str) -> Result<bool> {
bcrypt::verify(pwd, hashed_pwd).map_err(AppError::from)
}
AppError
为了处理管理员登录和鉴权,对AppError
进行了大量改动。
// src/error.rs
#[derive(Debug)]
pub enum AppErrorType {
//...
Crypt,
IncorrectLogin,
Forbidden,
}
impl AppError {
// ...
pub fn incorrect_login() -> Self {
Self::from_str("错误的邮箱或密码", AppErrorType::IncorrectLogin)
}
pub fn forbidden() -> Self {
Self::from_str("无权访问", AppErrorType::Forbidden)
}
pub fn response(self) -> axum::response::Response {
match self.types {
AppErrorType::Forbidden => {
let mut hm = HeaderMap::new();
hm.insert(header::LOCATION, "/auth".parse().unwrap());
(StatusCode::FOUND, hm, ()).into_response()
}
_ => self
.message
.to_owned()
.unwrap_or("有错误发生".to_string())
.into_response(),
}
}
}
// ...
impl From<bcrypt::BcryptError> for AppError {
fn from(err: bcrypt::BcryptError) -> Self {
Self::from_err(Box::new(err), AppErrorType::Crypt)
}
}
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
self.response()
}
}
AppErrorType::Crypt
:密码加密/验证失败AppErrorType::IncorrectLogin
:错误的邮箱/密码AppErrorType::Forbidden
:禁止访问,请先登录response(self)
:根据不同的错误类型作出不同响应。其中,如果是AppErrorType::Forbidden
(即:未登录),跳转到登录页面;其它情况直接输出错误信息。impl From<bcrypt::BcryptError> for AppError
:实现bcrypt::BcryptError
到AppError
的转换impl IntoResponse for AppError
:改由response(self)
方法提供
Cookie
cookie
模块
// src/cookie.rs
const COOKIE_NAME: &str = "axum_rs_blog_admin";
pub fn get_cookie(headers: &HeaderMap) -> Option<String> {
let cookie = headers
.get(axum::http::header::COOKIE)
.and_then(|value| value.to_str().ok())
.map(|value| value.to_string());
match cookie {
Some(cookie) => {
let cookie = cookie.as_str();
let cs: Vec<&str> = cookie.split(';').collect();
for item in cs {
let item: Vec<&str> = item.split('=').collect();
if item.len() != 2 {
continue;
}
let key = item[0];
let val = item[1];
let key = key.trim();
let val = val.trim();
if key == COOKIE_NAME {
return Some(val.to_string());
}
}
None
}
None => None,
}
}
pub fn set_cookie(value: &str) -> HeaderMap {
let c = format!("{}={}", COOKIE_NAME, value);
let mut hm = HeaderMap::new();
hm.insert(axum::http::header::SET_COOKIE, (&c).parse().unwrap());
hm
}
COOKIE_NAME
:本项目使用的Cookie的名称get_cookie()
:从请求头获取Cookieset_cookie()
:设置Cookie,并将带有cookie的响应头返回
redirect_with_cookie()
// src/handler/mod.rs
fn redirect_with_cookie(url: &str, c:Option<&str>) -> Result<RedirectView> {
let mut hm = match c {
Some(s) => cookie::set_cookie(s),
None => HeaderMap::new(),
};
hm.insert(header::LOCATION, url.parse().unwrap());
Ok((StatusCode::FOUND, hm, ()))
}
通过参数c:Option<&str>
判断是否需要设置Cookie。如果需要,则调用cookie::set_cookie(s)
来获得一个带cookie的响应头;如果不需要,则调用HeaderMap::new()
生成一个空的响应头。
最后,在响应头里设置跳转。
相应的,之前的redirect()
可以改为调用redirect_with_cookie()
来实现。
// src/handler/mod.rs
fn redirect(url: &str) -> Result<RedirectView> {
redirect_with_cookie(url, None)
}
标签:axum,rs,AppError,pub,let,博客,cookie,鉴权,fn
From: https://www.cnblogs.com/pythonClub/p/17779456.html