首页 > 其他分享 > 使用axum构建博客系统 - 鉴权与登录

使用axum构建博客系统 - 鉴权与登录

时间:2023-10-21 20:35:04浏览次数:41  
标签:axum rs AppError pub let 博客 cookie 鉴权 fn

本章实现后台管理的鉴权,以及管理员的登录、注销功能。涉及的知识点有: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');

为了方便使用,我们插入一条初始数据作为默认管理员:

数据模型

// 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::BcryptErrorAppError的转换
  • 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():从请求头获取Cookie
  • set_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

相关文章

  • 使用axum构建博客系统 - 网站首页
    后台管理完成后,我们开始进入前台功能的开发。本章我们将完成博客首页的开发。母模板templates/frontend/base.html是时候对前台母模板进行数据填充和块的定义了:<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="utf-8"/><metaname="viewport"c......
  • 使用axum构建博客系统 - 分类文章列表
    本章将实现博客的分类文章列表功能。模板请参见代码仓库的templates/frontend/topic_list.html视图类请参见代码仓库的src/view/frontend/topic.rshandler//src/handler/frontend/topic.rspubasyncfnlist(Extension(state):Extension<Arc<AppState>>,Path(......
  • 使用axum构建博客系统 - 文章详情
    本章将实现博客文章的详情显示功能。数据库视图CREATEVIEWv_topic_cat_detailASSELECTt.id,title,html,hit,dateline,category_id,t.is_del,c.nameAScategory_nameFROMtopicsAStINNERJOINcategoriesAScONt.cate......
  • 使用axum构建博客系统 - 存档文章列表
    本章将实现存档文章列表功能。注意,本章涉及较多PostgreSQL知识,如果你对相关知识不熟悉,可以先让代码跑起来,再去了解相关知识。模板本功能模板文件是templates/frontend/topic_arch.html。视图类本功能视图类定义在src/view/frontend/topic.rs文件。handler//src/handler/fro......
  • 每日博客
    1.Hive是由Facebook公司开发的一个构建在Hadoop之上的数据仓库工具,在某种程度上可以看作是用户编程接口,其本身并不存储和处理数据2.Hive一般依赖于分布式文件系统HDFS,而传统数据库则依赖于本地文件系统,Hive和传统关系数据库都支持分区,传统关系数据库很难实现横向扩展,Hive具......
  • 博客园主题美化
    一、首页主题预览二、主题部署1.开通js权限2.css代码禁用css模板点击查看代码#loading{bottom:0;left:0;position:fixed;right:0;top:0;z-index:9999;background-color:#f4f5f5;pointer-events:none;}.loader-inner{will-change:transform;width:40px;height:40px;positi......
  • 模拟集成电路设计系列博客——3.1.3 稳压电路
    3.1.3稳压电路稳压器的目标是产生一个低噪声并能提供电流的电压源。他们一般来说用于这种情节:当一个关键模拟电路必须和其他的电路工作在同一个电源供电下时。如下图所示,其他的电路向共用的电源中引入了很大的噪声,使用稳压器可以为关键电路提供一个更加干净的电源。数字电路一般......
  • 模拟集成电路设计系列博客——3.1.2 参考电路
    3.1.2参考电路已知绝对值的电压和电流在集成电路的交互处,或者是集成电路和其他分立部件之间是最有用的。例如,两个集成电路需要交互时,规定通过一伏摆幅的信号来进行。参考电压或者电流优势从电源中分配而出,但电源并不重组有着充足的控制精度,这种情况下参考电压或者参考电流就需要......
  • 从零用VitePress搭建博客教程(4) – 如何自定义首页布局和主题样式修改?
    接上一节:从零用VitePress搭建博客教程(3)-VitePress页脚、标题logo、最后更新时间等相关细节配置六、首页样式修改有时候觉得自带的样式不好看,想自定义,首先我们在docs/.vitePress新建一个theme文件夹,用来存放自定义布局和主题修改的相关文件,如下所示theme下再新建custom.css......
  • 【玩转 EdgeOne】边缘安全加速平台EO给自己的技术博客插上“翅膀”
    作为一个技术博客爱好者,不知不觉已经在程序员行业将近十年了,写技术博客也有将近七年的时间,其中我也搭建了一个自己的技术博客,因为服务器购买的是比较低配的云服务器,技术博客的访问速度不是很理想。近期发现腾讯云推出的边缘安全加速平台可以起到访问加速和安全防护的作用,我这边非常......