前言#
昨天我们实现了token相关的内容,今天我们开始来写路由。
项目地址:
https://github.com/1714080902120/rust_rocket_crud_demo由于我是先实现完再写的这篇文章,如果有些地方无法运行,可以看下我项目里的代码。
目前我还在往全栈的方向学习,所以如果看的不顺眼,请多多包涵。如果觉得那里可以改进,麻烦评论区说下,谢谢~
register#
实现#
第一步自然是实现注册的路由,回到我们的auth文件夹中,新建route.rs文件
#[post("/register", data = "<register_data>")]
pub async fn register(
db: BlogDBC,
validator: &State<ValidateData>,
register_data: Form<RegisterData>,
) -> Result<RtData<RtDataType>, Status> {
// ...
}这个是整体框架,我们需要用到db,所以这里需要引入,另外我们还需要数据类型,这里发送类型是POST,所以我创建了一个新的类型RegisterData,放在auth/mod.rs中
use rocket::{
FromForm
};
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, FromForm)]
pub struct RegisterData {
#[field(name = "name")]
name: String,
#[field(name = "pwd")]
pwd: String,
#[field(name = "email")]
email: String,
#[field(name = "phone")]
phone: String,
#[field(name = "desc")]
desc: String,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub struct UserExisted(());
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub enum RtDataType {
Exist(UserExisted),
Success(LoginSuccessData),
}
impl<'r> Responder<'r, 'static> for RtData<RtDataType> {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
let mut res = Response::build();
res.header(ContentType::JSON);
req.local_cache(|| AuthMsg {
is_valid_token: true,
});
if let RtDataType::Success(login_data) = &self.data {
let (token_field, token) = set_token(req, login_data.user_id.as_str());
res.raw_header(token_field, token);
};
let data = self.to_string();
res.sized_body(data.len(), Cursor::new(data)).ok()
}
}我们直接derive一个FromForm[1]即可。
其中field里面的name指的是POST传过来的数据里的form字段名字
另外我们再实现一个枚举类型,用于组装RtData作为response(实际上可以和前面实现的DataType放到一起,我这里写的时候还没这个想法,所以是分开的)
这里还有一步set_token,这个函数我们在上篇文章中实现了,注册之后就没必要让用户再去登录了,直接就给他token即可。
然后回到我们的路由这里,我们来填充逻辑,我们需要实现以下部分:
validate_register_data:这个我们还没实现,用来校验数据是否正确的,比如email格式等try_register_user:这个也还没实现,我们准备在这个函数中和数据库对接,执行SQL语句。
我们先来实现try_register_user,在auth文件夹下创建一个db_service文件,这个文件不用在mod.rs中pub出去,只需要引入即可,只在route.rs中使用。
use crate::{db::{BlogDBC, SqlxError, DbQueryResult}, types::LoginSuccessData};
use jsonwebtoken::get_current_timestamp;
use rocket_db_pools::sqlx::{self, postgres::PgRow};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::RegisterData;
pub async fn try_register_user(
mut db: BlogDBC,
register_data: RegisterData,
) -> DbQueryResult<RegisterRtType> {
let (name, pwd, email, phone, desc) = register_data.into();
let email_c = email.as_str();
let phone_c = phone.as_str();
let sql =
format!("SELECT * FROM public.user WHERE email = '{email_c}' OR phone = {phone_c} LIMIT 1");
let res = if let Err(db_err) = sqlx::query(&sql).fetch_one(&mut *db).await {
match db_err {
SqlxError::RowNotFound => {
register_user(db, (name, pwd, email, phone, desc)).await?
}
_ => return Err(db_err),
}
} else {
return Ok(RegisterRtType::Exist(String::from(
"email or phone had been registried !",
)));
};
Ok(res)
}
pub async fn register_user(
mut db: BlogDBC,
(name, pwd, email, phone, desc): (String, String, String, String, String),
) -> DbQueryResult<RegisterRtType> {
let id = Uuid::new_v4();
let pwd = format!("{:x}", md5::compute(pwd));
let create_time = get_current_timestamp();
let desc = if desc.is_empty() {
String::from("这人很懒,什么都没留下~")
} else {
desc
};
let insert_key = format!("id, name, pwd, email, phone, description, create_time");
let insert_values =
format!("'{id}', '{name}', '{pwd}', '{email}', {phone}, '{desc}', {create_time}");
let sql = format!("INSERT INTO public.user ({insert_key}) VALUES ({insert_values})");
dbg!(&sql);
match sqlx::query(&sql).fetch_one(&mut *db).await {
Ok(_) => Ok(RegisterRtType::Success(LoginSuccessData {
name,
desc,
user_id: id.to_string(),
})),
Err(err) => {
if let SqlxError::RowNotFound = err {
return Ok(RegisterRtType::Success(LoginSuccessData {
name,
desc,
user_id: id.to_string(),
}));
}
return Err(err);
}
}
} 这里的逻辑很简单,先通过SELECT语句查询是否已经被注册了,如果没有再执行INSERT逻辑。
至于SQL语句我这里就不多说了
注意:这里如果是字符串,你需要加上'',不然就无法通过解析。
然后我们再来实现validate这块。
我们在auth文件夹下创建一个validate.rs文件
use regex::Regex;
use rocket::form::Form;
use rocket::{http::Status, State};
use super::LoginData;
#[derive(Debug, Clone)]
pub struct ValidateData {
email_reg: Regex,
phone_reg: Regex,
}
const EMAIL_REG_RULE: &'static str = r"^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$";
const PHONE_REG_RULE: &'static str = r"^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$";
impl ValidateData {
pub fn new() -> Result<Self, Status> {
let email_reg = match Regex::new(EMAIL_REG_RULE) {
Ok(reg) => reg,
Err(_) => return Err(Status::InternalServerError),
};
let phone_reg = match Regex::new(PHONE_REG_RULE) {
Ok(reg) => reg,
Err(_) => return Err(Status::InternalServerError),
};
Ok(Self {
email_reg,
phone_reg,
})
}
pub fn is_empty(&self, target: Vec<&str>) -> bool {
for item in target.iter() {
if item.is_empty() {
return true;
}
}
return false;
}
pub fn validate_email(&self, target: &str) -> bool {
match self.email_reg.captures(target) {
Some(_) => true,
None => {
dbg!("validate email fail");
false
}
}
}
pub fn validate_phone(&self, target: &str) -> bool {
match self.phone_reg.captures(target) {
Some(_) => true,
None => {
dbg!("validate phone fail");
false
}
}
}
pub fn validate_name(&self, target: &str) -> bool {
let len = target.len();
if len <= 0 || len > 255 {
return false;
}
return true;
}
pub fn validate_email_phone_name(&self, email: &str, phone: &str, name: &str) -> bool {
return self.validate_email(email)
&& self.validate_phone(phone)
&& self.validate_name(name);
}
}
pub fn validate_login_data(
form_data: &mut Form<LoginData>,
validator: &State<ValidateData>,
) -> Result<bool, Status> {
let user_login_key = form_data.user_login_key.to_owned();
let pwd = form_data.pwd.to_owned();
dbg!(&form_data);
if user_login_key.is_empty() || pwd.is_empty() {
return Err(Status::BadRequest);
}
// vailidate
let mut is_email = false;
let mut is_fail = false;
if user_login_key.contains("@") {
is_email = true;
is_fail = !validator.validate_email(&user_login_key);
} else {
is_fail = !validator.validate_phone(&user_login_key);
};
if is_fail {
return Err(Status::BadRequest);
}
Ok(is_email)
}
pub fn validate_register_data(
(name, pwd, email, phone, desc): (String, String, String, String, String),
validator: &State<ValidateData>,
) -> Result<(), Status> {
if validator.is_empty(vec![&name, &pwd, &email, &phone])
|| !validator.validate_email_phone_name(&email, &phone, &name)
{
return Err(Status::BadRequest);
}
Ok(())
}
代码稍微有些长,但是很好理解,就是用来校验数据的。
这里用到了Regex的正则表达式,它来自于我们前面引入的regex[4]这个crate
这个validate在登录和注册都需要使用到,所以我还是把它放到全局state去了,在state/mod.rs中
use crate::auth::{UserToken, validate::ValidateData};
use rocket::http::Status;
// ...
pub fn init_validate_instace () -> Result<ValidateData, Status> {
let instance = ValidateData::new()?;
Ok(instance)
} 然后别忘了在main.rs中引入
use state::{get_default_user_token, init_validate_instace};
// ...
#[launch]
fn rocket() -> _ {
rocket::custom(get_custom_figment())
// ...
.manage(init_validate_instace().unwrap())
// ...
}那么这一块就完成了。
完整的register代码
#[post("/register", data = "<register_data>")]
pub async fn register(
db: BlogDBC,
validator: &State<ValidateData>,
register_data: Form<RegisterData>,
) -> Result<RtData<RtDataType>, Status> {
validate_register_data(register_data.clone().into(), &validator)?;
let res = try_register_user(db, register_data.into_inner()).await;
match res {
Ok(rt_type) => {
dbg!(&rt_type);
let mut success = true;
let mut rt = Rt::Success;
let mut msg = String::from("registry success !");
let data = match rt_type {
RegisterRtType::Exist(r_msg) => {
success = false;
rt = Rt::Fail;
msg = r_msg;
RtDataType::Exist(UserExisted(()))
}
RegisterRtType::Success(login_success_data) => {
RtDataType::Success(login_success_data)
}
};
Ok(RtData {
success,
rt,
msg,
data,
})
}
Err(err) => {
dbg!(err);
return Err(Status::InternalServerError);
}
}
} 第一步我们显示校验数据,错误直接去400。
然后我们在query邮箱/手机号是否已被注册,是的话返回success:false。
如果没有被注册就帮他注册,如果注册过程中发生问题,则返回500。
最后我们在给RegisterData实现的Responder中插入token到header中,那么这个路由就完成了。
最后别忘了在main.rs中引入
rocket::custom(get_custom_figment())
// ...
.mount("/user", routes![register])测试#
官方提供了我们单元测试和集成测试的场景,但是我这里还是不这么做了。就我个人而言,写一些公共库/组件的时候用集成测试和单元测试确实会比较好一些,但是对于业务来说,这种反而作用不大。
所以我还是走传统的方式,通过第三方工具来测试接口。
我这里选择的是ApiFox的谷歌/edge浏览器插件:Apifox Browser Extension - Microsoft Edge Addons
我偶然发现的,本来打算看下有没有postman的,结果看到了这个。
试了下发现挺不错的,数据是远程的,也就是说换台电脑依旧是这些接口,非常方便。

我这里简单的测试下即可,其它几种场景我开发的时候已经测试过了。

这里的user_id忘了去掉了。。。。。
这个时候我们在header里就能拿到token了。

别忘了配置开发环境的基础路径

login#
实现#
登录的逻辑和注册的差不多。
我们先去type/mod.rs中新增一个类型
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct LoginSuccessData {
pub name: String,
pub desc: String,
pub user_id: String,
}
impl RtData<LoginSuccessData> {
fn hide_user_id (&mut self) {
self.data.user_id = String::from("-");
}
}
impl<'r> Responder<'r, 'static> for RtData<LoginSuccessData> {
fn respond_to(mut self, req: &'r Request<'_>) -> response::Result<'static> {
let user_id = self.data.user_id.as_str().to_owned();
self.hide_user_id();
let data = self.to_string();
req.local_cache(|| AuthMsg {
is_valid_token: true,
});
let (token_field, token) = set_token(req, user_id.as_str());
Response::build()
.header(ContentType::JSON)
.raw_header(token_field.to_string(), token)
.sized_body(data.len(), Cursor::new(data))
.ok()
}
} 我们在这里插入token,并且将is_valid_token置为true,这样避免fairing那边on_reponse造成的误伤。
这里我还做了对user_id的数据隐藏,register是之前写的,忘了加上了。。。
use crate::types::rt_type::Rt;
use rocket::http::Status;
use rocket::{form::Form, post, State};
use rocket_db_pools::sqlx::Row;
use crate::auth::{db_service::{RegisterRtType, get_user_msg}, LoginData};
use crate::db::{BlogDBC, SqlxError};
use crate::types::{LoginSuccessData, RtData};
use super::db_service::try_register_user;
use super::validate::{validate_login_data, validate_register_data, ValidateData};
use super::{RegisterData, RtDataType, UserExisted};
#[post("/login", data = "<login_data>")]
pub async fn login(
db: BlogDBC,
validator: &State<ValidateData>,
mut login_data: Form<LoginData>,
) -> Result<RtData<LoginSuccessData>, Status> {
let user_login_key = login_data.user_login_key.to_owned();
let pwd = login_data.pwd.to_owned();
let is_email = validate_login_data(&mut login_data, &validator)?;
let result = get_user_msg((user_login_key, pwd, is_email), db).await;
let user_msg = match result {
Ok(row) => {
let user_id = row.get::<sqlx::types::Uuid, usize>(2).to_string();
LoginSuccessData {
name: row.get(0),
desc: row.get(1),
user_id,
}
}
Err(err) => {
match err {
SqlxError::RowNotFound => {
dbg!("row not found");
return Err(Status::BadRequest);
}
_ => {
let db_err = err.into_database_error().expect("is not db err");
dbg!(db_err.message());
return Err(Status::InternalServerError);
}
};
}
};
Ok(RtData {
success: true,
msg: String::from("login success"),
rt: Rt::Success,
data: user_msg,
})
}row.get:::由于我们数据库那边id是uuid类型的,所以拿出来的也是uuid,而我们传给前端的id是String的,如果不做转换在runtime的时候就会有问题。
这里逻辑很简单,直接根据email/phone + 密码来查询是否存在对应信息,没有直接返回Status 400扔到捕获器那边处理。
查询过程中发生了问题,这就属于服务器内部出错,这个时候走500的场景。
我们回到auth/db_service.rs里面实现这个get_user_msg
use crate::{db::{BlogDBC, SqlxError, DbQueryResult}, types::LoginSuccessData};
use jsonwebtoken::get_current_timestamp;
use rocket_db_pools::sqlx::{self, postgres::PgRow};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub async fn get_user_msg(
(login_key, pwd, is_email): (String, String, bool),
mut db: BlogDBC,
) -> DbQueryResult<PgRow> {
let condition = if is_email {
format!("email = '{login_key}'")
} else {
format!("phone = {login_key}")
};
let pwd = format!("{:x}", md5::compute(pwd));
let sql = format!(
"SELECT name, description, id FROM public.user WHERE pwd = '{pwd}' AND {condition}"
);
dbg!(&sql);
sqlx::query(&sql).fetch_one(&mut *db).await
} 注意这里做了加密处理,因为我们注册的时候插入到数据库里的密码是md5之后的了,所以这里也需要md5加密下。
最后别忘了在main.rs中注册
// ...
.mount("/user", routes![login, register]) 测试#

这个是我们刚注册的用户,可以看到登录成功了。
token也有正常拿到

总结#
至于logout就不在这实现了,由前端移除_token即可。
而注销我这偷懒觉得没必要,你也可以自己实现一个。
下一章我们来实现文章的增删改查
参考#
- ^FromForm https://api.rocket.rs/v0.5-rc/rocket/derive.FromForm.html
- ^uuid https://crates.io/crates/uuid
- ^md5 https://crates.io/crates/md5
- ^regex https://crates.io/crates/regex
发布于 2023-06-07 18:01・IP 属地广东
