本章我们将实现管理员服务。
首先创建项目并加入到 workspace 中:
cargo new admin-srv
密码处理
为了保证存储在数据库中的密码安全,我们创建一个 blog-utils
crate:
cargo new --lib blog-utils
加上依赖:
pub fn hash(pwd: &str) -> Result<String, String> {
bcrypt::hash(pwd, bcrypt::DEFAULT_COST).map_err(|err| err.to_string())
}
pub fn verify(pwd: &str, hashed_pwd: &str) -> Result<bool, String> {
bcrypt::verify(pwd, hashed_pwd).map_err(|err| err.to_string())
}
hash
:将明文密码进行hashverify
:对明文密码和给定的hash过的密码进行验证
管理员是否存在 admin_exists
的实现
根据传入的条件,通过email或id判断管理员是否存在。
获取管理员 get_admin
的实现
获取管理员的几种场景:
- 通过email和密码进行获取,如果email和密码正确,则返回结果。通常用于管理员登录
- 通过id获取。通常用于查看管理员信息
async fn get_admin(
&self,
request: tonic::Request<GetAdminRequest>,
) -> Result<tonic::Response<GetAdminReply>, tonic::Status> {
let GetAdminRequest { condition } = request.into_inner();
let condition = match condition {
Some(condition) => condition,
None => return Err(tonic::Status::invalid_argument("请指定条件")),
};
let reply = match condition {
blog_proto::get_admin_request::Condition::ByAuth(ba) => {
let row = sqlx::query("SELECT id,email,is_del,password FROM admins WHERE email=$1")
.bind(ba.email)
.fetch_optional(&*self.pool)
.await
.map_err(|err| tonic::Status::internal(err.to_string()))?;
if let Some(row) = row {
let hashed_pwd: String = row.get("password");
let is_verify = password::verify(&ba.password, &hashed_pwd)
.map_err(tonic::Status::internal)?;
if !is_verify {
return Err(tonic::Status::invalid_argument("用户名/密码错误2"));
} else {
GetAdminReply {
admin: Some(blog_proto::Admin {
id: row.get("id"),
email: row.get("email"),
password: None,
is_del: row.get("is_del"),
}),
}
}
} else {
return Err(tonic::Status::invalid_argument("用户名/密码错误"));
}
}
blog_proto::get_admin_request::Condition::ById(bi) => {
let row = match bi.is_del {
Some(is_del) => {
sqlx::query("SELECT id,email,is_del FROM admins WHERE id=$1 AND is_del=$2")
.bind(bi.id)
.bind(is_del)
}
None => {
sqlx::query("SELECT id,email,is_del FROM admins WHERE id=$1").bind(bi.id)
}
}
.fetch_optional(&*self.pool)
.await
.map_err(|err| tonic::Status::internal(err.to_string()))?;
if let Some(row) = row {
GetAdminReply {
admin: Some(blog_proto::Admin {
id: row.get("id"),
email: row.get("email"),
password: None,
is_del: row.get("is_del"),
}),
}
} else {
return Err(tonic::Status::not_found("不存在的用户"));
}
}
};
Ok(tonic::Response::new(reply))
}
我们重点来看通过email和密码来获取:
blog_proto::get_admin_request::Condition::ByAuth(ba) => {
let row = sqlx::query("SELECT id,email,is_del,password FROM admins WHERE email=$1")
.bind(ba.email)
.fetch_optional(&*self.pool)
.await
.map_err(|err| tonic::Status::internal(err.to_string()))?;
if let Some(row) = row {
let hashed_pwd: String = row.get("password");
let is_verify = password::verify(&ba.password, &hashed_pwd)
.map_err(tonic::Status::internal)?;
if !is_verify {
return Err(tonic::Status::invalid_argument("用户名/密码错误2"));
} else {
GetAdminReply {
admin: Some(blog_proto::Admin {
id: row.get("id"),
email: row.get("email"),
password: None,
is_del: row.get("is_del"),
}),
}
}
} else {
return Err(tonic::Status::invalid_argument("用户名/密码错误"));
}
}
- 通过email从数据库中查找记录
- 如果有记录,通过
password::verify
判断密码是否正确 - 如果密码正确返回结果
作业:
在上面的代码中,无论管理员是否被删除,都可以进行登录,请修改代码,实现只有未删除的管理员才能登录。
修改密码 edit_admin
的实现
async fn edit_admin(
&self,
request: tonic::Request<EditAdminRequest>,
) -> Result<tonic::Response<EditAdminReply>, tonic::Status> {
let EditAdminRequest {
id,
email,
password,
new_password,
} = request.into_inner();
let new_password = match new_password {
Some(n) => n,
None => return Err(tonic::Status::invalid_argument("请设定新密码")),
};
let row = sqlx::query("SELECT password FROM admins WHERE id=$1 AND email=$2")
.bind(id)
.bind(&email)
.fetch_optional(&*self.pool)
.await
.map_err(|err| tonic::Status::internal(err.to_string()))?;
let pwd_in_db: String = match row {
Some(row) => row.get("password"),
None => return Err(tonic::Status::not_found("不存在的用户")),
};
let is_verify = password::verify(&password, &pwd_in_db).map_err(tonic::Status::internal)?;
if !is_verify {
return Err(tonic::Status::invalid_argument("密码错误"));
}
let hashed_new_pwd = password::hash(&new_password).map_err(tonic::Status::internal)?;
let rows_affected = sqlx::query("UPDATE admins SET password=$1 WHERE id=$2 AND email=$3")
.bind(hashed_new_pwd)
.bind(id)
.bind(&email)
.execute(&*self.pool)
.await
.map_err(|err| tonic::Status::internal(err.to_string()))?
.rows_affected();
Ok(tonic::Response::new(EditAdminReply {
id: id,
ok: rows_affected > 0,
}))
}
管理员列表 list_admin
的实现
async fn list_admin(
&self,
request: tonic::Request<ListAdminRequest>,
) -> Result<tonic::Response<ListAdminReply>, tonic::Status> {
let ListAdminRequest { email, is_del } = request.into_inner();
let rows = sqlx::query(
r#"
SELECT
id,email,is_del
FROM
admins
WHERE 1=1
AND ($1::text IS NULL OR email ILIKE CONCAT('%',$1::text,'%'))
AND ($2::boolean IS NULL OR is_del=$2::boolean)
"#,
)
.bind(email)
.bind(is_del)
.fetch_all(&*self.pool)
.await
.map_err(|err| tonic::Status::internal(err.to_string()))?;
let mut admins = Vec::with_capacity(rows.len());
for row in rows {
let a = blog_proto::Admin {
id: row.get("id"),
email: row.get("email"),
is_del: row.get("is_del"),
password: None,
};
admins.push(a);
}
Ok(tonic::Response::new(ListAdminReply { admins }))
}
创建管理员 create_admin
的实现
async fn create_admin(
&self,
request: tonic::Request<CreateAdminRequest>,
) -> Result<tonic::Response<CreateAdminReply>, tonic::Status> {
let request = request.into_inner();
let AdminExistsReply { exists } = self
.admin_exists(tonic::Request::new(AdminExistsRequest {
condition: Some(blog_proto::admin_exists_request::Condition::Email(
request.email.clone(),
)),
}))
.await?
.into_inner();
if exists {
return Err(tonic::Status::already_exists("管理员已存在"));
}
let pwd = password::hash(&request.password).map_err(tonic::Status::internal)?;
let row = sqlx::query("INSERT INTO admins (email,password) VALUES ($1,$2) RETURNING id")
.bind(request.email)
.bind(pwd)
.fetch_one(&*self.pool)
.await
.map_err(|err| tonic::Status::internal(err.to_string()))?;
Ok(tonic::Response::new(CreateAdminReply { id: row.get(0) }))
}
删除/恢复管理员 toggle_admin
的实现
async fn toggle_admin(
&self,
request: tonic::Request<ToggleAdminRequest>,
) -> Result<tonic::Response<ToggleAdminReply>, tonic::Status> {
let ToggleAdminRequest { id } = request.into_inner();
let row = sqlx::query("UPDATE admins SET is_del=(NOT is_del) WHERE id=$1 RETURNING is_del")
.bind(id)
.fetch_one(&*self.pool)
.await
.map_err(|err| tonic::Status::internal(err.to_string()))?;
Ok(tonic::Response::new(ToggleAdminReply {
id: id,
is_del: row.get(0),
}))
}
运行和测试
运行、测试方法和分类服务、文章服务相似,请参考之前的章节
本章代码位于05/实现管理员服务分支。