1 添加用户密码加密、验证
build / build-rust (push) Successful in 3m7s Details

2 添加用户名、邮箱等是否已经存在接口
This commit is contained in:
soul-walker 2024-11-07 23:09:36 +08:00
parent 25a63b1fb7
commit 61de18077e
9 changed files with 287 additions and 184 deletions

35
Cargo.lock generated
View File

@ -125,6 +125,18 @@ version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
[[package]]
name = "argon2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"password-hash",
]
[[package]]
name = "arraydeque"
version = "0.5.1"
@ -602,6 +614,15 @@ dependencies = [
"serde",
]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -1907,6 +1928,17 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
@ -2417,7 +2449,10 @@ name = "rtsa_db"
version = "0.1.0"
dependencies = [
"anyhow",
"argon2",
"lazy_static",
"md-5",
"rand_core",
"regex",
"rtsa_dto",
"rtsa_log",

View File

@ -19,6 +19,9 @@ sqlx = { workspace = true, features = [
thiserror = { workspace = true }
lazy_static = { workspace = true }
regex = { workspace = true }
argon2 = "0.5.3"
rand_core = { version = "0.6.4", features = ["std"] }
md-5 = "0.10.6"
rtsa_dto = { path = "../rtsa_dto" }
rtsa_log = { path = "../rtsa_log" }

View File

@ -3,7 +3,6 @@ use sqlx::Postgres;
use super::RtsaDbAccessor;
use super::{OrgAccessor, RegisterUser, UserAccessor};
use crate::password_util::verify_password;
use crate::{
common::{PageData, PageQuery, TableColumn},
model::{OrganizationUserColumn, OrganizationUserModel, UserModel},
@ -48,14 +47,6 @@ pub trait OrgUserAccessor {
info: Value,
updater_id: i32,
) -> Result<OrganizationUserModel, DbAccessError>;
/// 查询组织用户登陆
/// username: 学工号/用户名/邮箱/手机号
async fn query_org_user_login(
&self,
org_id: i32,
username: &str,
password: &str,
) -> Result<(OrganizationUserModel, UserModel), DbAccessError>;
/// 获取组织用户
async fn query_org_user(
&self,
@ -383,35 +374,6 @@ impl OrgUserAccessor for RtsaDbAccessor {
Ok(update)
}
async fn query_org_user_login(
&self,
org_id: i32,
username: &str,
password: &str,
) -> Result<(OrganizationUserModel, UserModel), DbAccessError> {
// 查询用户登陆
let user = self.query_user_login(username, password).await;
match user {
Err(_) => {
// 用户不存在,查询组织用户学工号+用户密码
// 通过组织id和学工号查询组织用户
let org_user = self.query_org_user_by_student_id(org_id, username).await?;
let user = self.query_user(org_user.user_id).await?;
// 检查用户密码
if verify_password(password, &user.password) {
Ok((org_user, user))
} else {
Err(DbAccessError::InvalidArgument("密码不匹配".to_string()))
}
}
Ok(user) => {
// 用户存在,查询组织用户
let org_user = self.query_org_user(org_id, user.id).await?;
Ok((org_user, user))
}
}
}
async fn query_org_user(
&self,
org_id: i32,
@ -594,43 +556,6 @@ mod tests {
Ok(())
}
#[sqlx::test(migrator = "crate::MIGRATOR")]
async fn test_query_org_user_login(pool: PgPool) -> Result<(), DbAccessError> {
let accessor = RtsaDbAccessor::new(pool);
let (creator, org) = init_default_org_and_user(&accessor).await?;
let create = CreateOrgUser::new(
&org.code.clone().unwrap(),
"test_student_id",
"test_name",
"test_password",
creator.id,
);
accessor.create_org_user(create.clone()).await?;
// Attempt to log in with student ID
let (org_user, user) = accessor
.query_org_user_login(org.id, "test_student_id", "test_password")
.await?;
assert_eq!(org_user.organization_id, org.id);
assert_eq!(user.nickname, create.nickname());
// Attempt to log in with username
let (org_user, user) = accessor
.query_org_user_login(org.id, &create.build_username(), "test_password")
.await?;
assert_eq!(org_user.organization_id, org.id);
assert_eq!(user.nickname, create.nickname());
// Attempt to log in with wrong password
let result = accessor
.query_org_user_login(org.id, "test_student_id", "wrong_password")
.await;
assert!(matches!(result, Err(DbAccessError::InvalidArgument(_))));
// Attempt to log in with wrong username
let result = accessor
.query_org_user_login(org.id, "wrong_student_id", "test_password")
.await;
assert!(matches!(result, Err(DbAccessError::SqlxError(_))));
Ok(())
}
#[sqlx::test(migrator = "crate::MIGRATOR")]
async fn test_query_org_user_by_student_id(pool: PgPool) -> Result<(), DbAccessError> {
let accessor = RtsaDbAccessor::new(pool);

View File

@ -4,7 +4,7 @@ use sqlx::Postgres;
use crate::{
common::{PageData, PageQuery, TableColumn},
model::{UserColumn, UserModel},
password_util::verify_password,
password_util::{self, verify_password},
username_util::{is_email, is_mobile},
DbAccessError,
};
@ -48,6 +48,10 @@ pub trait UserAccessor {
async fn query_user(&self, id: i32) -> Result<UserModel, DbAccessError>;
/// 根据username查询用户数据
async fn query_user_by_username(&self, username: &str) -> Result<UserModel, DbAccessError>;
/// 根据email查询用户数据
async fn query_user_by_email(&self, email: &str) -> Result<UserModel, DbAccessError>;
/// 根据mobile查询用户数据
async fn query_user_by_mobile(&self, mobile: &str) -> Result<UserModel, DbAccessError>;
/// 是否用户名已经存在
async fn is_user_name_exist(&self, name: &str) -> Result<bool, DbAccessError>;
/// 是否用户email已经存在
@ -296,7 +300,8 @@ impl UserAccessor for RtsaDbAccessor {
Ok(PageData::new(total, rows))
}
async fn register_user(&self, user: RegisterUser) -> Result<UserModel, DbAccessError> {
async fn register_user(&self, mut user: RegisterUser) -> Result<UserModel, DbAccessError> {
user.password = password_util::hash_password(&user.password)?;
self.insert_user(user, &self.pool).await
}
@ -305,87 +310,33 @@ impl UserAccessor for RtsaDbAccessor {
username: &str,
password: &str,
) -> Result<UserModel, DbAccessError> {
let table = UserColumn::Table.name();
let username_column = UserColumn::Username.name();
let email_column = UserColumn::Email.name();
let mobile_column = UserColumn::Mobile.name();
let user: UserModel;
if is_email(username) {
let query_clause = format!(
"SELECT * FROM {table} WHERE {email_column} = $1 LIMIT 1",
table = table,
email_column = email_column,
);
let user: Result<UserModel, sqlx::Error> = sqlx::query_as(&query_clause)
.bind(username)
.fetch_one(&self.pool)
.await;
match user {
Ok(user) => {
if verify_password(password, &user.password) {
Ok(user)
let query_result = self.query_user_by_email(username).await;
if query_result.is_ok() {
user = query_result.unwrap();
} else {
Err(DbAccessError::InvalidArgument("密码不匹配".to_string()))
}
}
Err(sqlx::Error::RowNotFound) => Err(DbAccessError::InvalidArgument(
"用户不存在(email)".to_string(),
)),
Err(e) => Err(DbAccessError::SqlxError(e)),
return Err(DbAccessError::UserNotExist("email".to_string()));
}
} else if is_mobile(username) {
let query_clause = format!(
"SELECT * FROM {table} WHERE {mobile_column} = $1 LIMIT 1",
table = table,
mobile_column = mobile_column,
);
let user: Result<UserModel, sqlx::Error> = sqlx::query_as(&query_clause)
.bind(username)
.fetch_one(&self.pool)
.await;
match user {
Ok(user) => {
if verify_password(password, &user.password) {
return Ok(user);
let query_result = self.query_user_by_mobile(username).await;
if query_result.is_ok() {
user = query_result.unwrap();
} else {
return Err(DbAccessError::InvalidArgument("密码不匹配".to_string()));
}
}
Err(sqlx::Error::RowNotFound) => {
return Err(DbAccessError::InvalidArgument(
"用户不存在(mobile)".to_string(),
));
}
Err(e) => {
return Err(DbAccessError::SqlxError(e));
}
return Err(DbAccessError::UserNotExist("mobile".to_string()));
}
} else {
let query_clause = format!(
"SELECT * FROM {table} WHERE {username_column} = $1 LIMIT 1",
table = table,
username_column = username_column,
);
let user: Result<UserModel, sqlx::Error> = sqlx::query_as(&query_clause)
.bind(username)
.fetch_one(&self.pool)
.await;
match user {
Ok(user) => {
if verify_password(password, &user.password) {
return Ok(user);
let query_result = self.query_user_by_username(username).await;
if query_result.is_ok() {
user = query_result.unwrap();
} else {
return Err(DbAccessError::InvalidArgument("密码不匹配".to_string()));
}
}
Err(sqlx::Error::RowNotFound) => {
return Err(DbAccessError::InvalidArgument(
"用户不存在(username)".to_string(),
));
}
Err(e) => {
return Err(DbAccessError::SqlxError(e));
return Err(DbAccessError::UserNotExist("username".to_string()));
}
}
if verify_password(password, &user.password).is_ok() {
Ok(user)
} else {
Err(DbAccessError::PasswordNotMatch)
}
}
@ -415,6 +366,36 @@ impl UserAccessor for RtsaDbAccessor {
Ok(user)
}
async fn query_user_by_email(&self, email: &str) -> Result<UserModel, DbAccessError> {
let table = UserColumn::Table.name();
let email_column = UserColumn::Email.name();
let query_clause = format!(
"SELECT * FROM {table} WHERE {email_column} = $1 LIMIT 1",
table = table,
email_column = email_column,
);
let user = sqlx::query_as(&query_clause)
.bind(email)
.fetch_one(&self.pool)
.await?;
Ok(user)
}
async fn query_user_by_mobile(&self, mobile: &str) -> Result<UserModel, DbAccessError> {
let table = UserColumn::Table.name();
let mobile_column = UserColumn::Mobile.name();
let query_clause = format!(
"SELECT * FROM {table} WHERE {mobile_column} = $1 LIMIT 1",
table = table,
mobile_column = mobile_column,
);
let user = sqlx::query_as(&query_clause)
.bind(mobile)
.fetch_one(&self.pool)
.await?;
Ok(user)
}
async fn update_user_info(&self, id: i32, info: Value) -> Result<UserModel, DbAccessError> {
let table = UserColumn::Table.name();
let id_column = UserColumn::Id.name();
@ -451,15 +432,16 @@ impl UserAccessor for RtsaDbAccessor {
"new_password is too long".to_string(),
));
}
let hashed = password_util::hash_password(new_password)?;
let table = UserColumn::Table.name();
let id_column = UserColumn::Id.name();
let password_column = UserColumn::Password.name();
let updated_at_column = UserColumn::UpdatedAt.name();
let update_clause = format!(
"UPDATE {table} SET {password_column} = '{new_password}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *",
"UPDATE {table} SET {password_column} = '{hashed}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *",
table = table,
password_column = password_column,
new_password = new_password,
hashed = hashed,
id_column = id_column,
id = id
);
@ -609,12 +591,14 @@ impl UserAccessor for RtsaDbAccessor {
#[cfg(test)]
mod tests {
use rtsa_dto::common::Role;
use rtsa_log::tracing::info;
use sqlx::PgPool;
use super::*;
#[sqlx::test(migrator = "crate::MIGRATOR")]
async fn test_register_user(pool: PgPool) -> Result<(), DbAccessError> {
rtsa_log::Logging::default().init();
let accessor = RtsaDbAccessor { pool };
let new_user = RegisterUser::new("test_user", "test_password")
@ -627,6 +611,7 @@ mod tests {
let queried_user = accessor
.query_user_login("test_user@example.com", "test_password")
.await?;
info!("queried_user: {:?}", queried_user);
assert_eq!(queried_user.username, "test_user");
assert_eq!(
queried_user.email,
@ -781,6 +766,42 @@ mod tests {
Ok(())
}
#[sqlx::test(migrator = "crate::MIGRATOR")]
async fn test_query_user_by_email(pool: PgPool) -> Result<(), DbAccessError> {
let accessor = RtsaDbAccessor { pool };
// 准备测试数据
let new_user = RegisterUser::new("test_user", "test_password").with_email("test@test.cn");
let um = accessor.register_user(new_user).await?;
// Assume a user with email exists
let user = accessor.query_user_by_email("test@test.cn").await?;
assert_eq!(user.id, um.id);
// Assume a user with email does not exist
let result = accessor.query_user_by_email("dev@test.cn").await;
assert!(result.is_err());
Ok(())
}
#[sqlx::test(migrator = "crate::MIGRATOR")]
async fn test_query_user_by_mobile(pool: PgPool) -> Result<(), DbAccessError> {
let accessor = RtsaDbAccessor { pool };
// 准备测试数据
let new_user = RegisterUser::new("test_user", "test_password").with_mobile("13345678901");
let um = accessor.register_user(new_user).await?;
// Assume a user with mobile exists
let user = accessor.query_user_by_mobile("13345678901").await?;
assert_eq!(user.id, um.id);
// Assume a user with mobile does not exist
let result = accessor.query_user_by_mobile("13345678902").await;
assert!(result.is_err());
Ok(())
}
#[sqlx::test(migrator = "crate::MIGRATOR")]
async fn test_query_user_login(pool: PgPool) -> Result<(), DbAccessError> {
let accessor = RtsaDbAccessor { pool };
@ -809,7 +830,7 @@ mod tests {
// Test with incorrect credentials
let result = accessor
.query_user_login("test_user@example.com", "wrongpassword")
.query_user_login("test_user@example.com", "wrong_password")
.await;
assert!(result.is_err());
Ok(())

View File

@ -14,4 +14,8 @@ pub enum DbAccessError {
DataError(String),
#[error("非法参数:{0}")]
InvalidArgument(String),
#[error("用户不存在:{0}")]
UserNotExist(String),
#[error("密码不正确")]
PasswordNotMatch,
}

View File

@ -1,4 +1,69 @@
use argon2::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use rand_core::OsRng;
use md5::{Digest, Md5};
use crate::DbAccessError;
/// 验证密码是否正确
pub fn verify_password(password: &str, hash: &str) -> bool {
password == hash
pub fn verify_password(password: &str, hash: &str) -> Result<(), DbAccessError> {
// Verify password against PHC string.
// NOTE: hash params from `parsed_hash` are used instead of what is configured in the
// `Argon2` instance.
let parsed_hash = PasswordHash::new(hash);
match parsed_hash {
Ok(parsed_hash) => Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.map_err(|e| DbAccessError::InvalidArgument(format!("password verify error: {}", e))),
Err(e) => Err(DbAccessError::InvalidArgument(format!(
"password hash error: {}",
e
))),
}
}
pub fn hash_password(password: &str) -> Result<String, DbAccessError> {
let salt = SaltString::generate(&mut OsRng);
// Argon2 with default params (Argon2id v19)
let argon2 = Argon2::default();
// Hash password to PHC string ($argon2id$v=19$...)
let password_hash = argon2.hash_password(password.as_bytes(), &salt);
match password_hash {
Ok(ph) => Ok(ph.to_string()),
Err(e) => Err(DbAccessError::InvalidArgument(format!(
"password hash error: {}",
e
))),
}
}
pub fn md5(input: &str) -> String {
let mut hasher = Md5::new();
hasher.update(input);
let result = hasher.finalize();
format!("{:x}", result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_password() {
let password = "password";
let hash = hash_password(password).unwrap();
assert!(verify_password(password, &hash).is_ok());
}
#[test]
fn test_md5() {
let input = "hello";
let output = md5(input);
assert_eq!(output, "5d41402abc4b2a76b9719d911017c592");
}
}

View File

@ -41,6 +41,35 @@ impl UserQuery {
Ok(user.into())
}
/// 用户名是否存在
async fn username_exists(
&self,
ctx: &Context<'_>,
username: String,
) -> async_graphql::Result<bool> {
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
let exist = db_accessor.is_user_name_exist(&username).await?;
Ok(exist)
}
/// 邮箱是否存在
async fn email_exists(&self, ctx: &Context<'_>, email: String) -> async_graphql::Result<bool> {
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
let exist = db_accessor.is_user_email_exist(&email).await?;
Ok(exist)
}
/// 手机号是否存在
async fn mobile_exists(
&self,
ctx: &Context<'_>,
mobile: String,
) -> async_graphql::Result<bool> {
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
let exist = db_accessor.is_user_mobile_exist(&mobile).await?;
Ok(exist)
}
/// 分页查询用户(系统管理)
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
async fn user_paging(

View File

@ -6,7 +6,7 @@ use rtsa_log::tracing::{debug, error, info};
use serde_json::json;
pub const ADMIN_USER_NAME: &str = "_jl_admin_";
pub const ADMIN_USER_PASSWORD: &str = "joylink0503";
pub const ADMIN_USER_PASSWORD: &str = "4a6d74126bfd06d69406fcccb7e7d5d9";
pub const DEFAULT_ORG_CODE: &str = "default";
const DEFAULT_ORG_NAME: &str = "默认机构";
@ -64,7 +64,6 @@ mod tests {
let db_accessor = rtsa_db::get_default_db_accessor();
let user = db_accessor.query_user_by_username(ADMIN_USER_NAME).await?;
assert_eq!(user.username, ADMIN_USER_NAME);
assert_eq!(user.password, ADMIN_USER_PASSWORD);
assert_eq!(user.nickname, ADMIN_USER_NAME);
assert_eq!(user.roles, json!([Role::Admin]));

View File

@ -1,7 +1,7 @@
use async_graphql::Guard;
use rtsa_db::prelude::*;
use rtsa_dto::common::Role;
use rtsa_log::tracing::warn;
use rtsa_log::tracing::{info, warn};
mod jwt_auth;
use crate::{apis::UserLoginDto, error::BusinessError, sys_init::DEFAULT_ORG_CODE};
@ -53,10 +53,10 @@ impl Guard for RoleGuard {
/// 处理用户登录
pub(crate) async fn handle_login(
db_accessor: &RtsaDbAccessor,
info: UserLoginDto,
user_login_dto: UserLoginDto,
) -> Result<Jwt, BusinessError> {
let org_id;
if let Some(oid) = info.org_id {
if let Some(oid) = user_login_dto.org_id {
org_id = oid;
} else {
let default_org = db_accessor.query_org_by_code(DEFAULT_ORG_CODE).await?;
@ -64,21 +64,30 @@ pub(crate) async fn handle_login(
}
// 查询用户登陆
let user = db_accessor
.query_user_login(&info.username, &info.password)
.query_user_login(&user_login_dto.username, &user_login_dto.password)
.await;
match user {
Ok(user) => {
// 用户存在
Ok(Jwt::build_from(Claims::new(user.id, org_id))?)
}
Err(_) => {
Err(e) => {
match e {
DbAccessError::UserNotExist(e) => {
info!(
"用户不存在: {}, 尝试查询组织学工号用户: username={}, org_id={}",
e, user_login_dto.username, org_id
);
// 用户不存在,查询组织用户学工号+用户密码
// 通过组织id和学工号查询组织用户
let org_user = db_accessor
.query_org_user_by_student_id(org_id, &info.username)
.query_org_user_by_student_id(org_id, &user_login_dto.username)
.await;
if org_user.is_err() {
warn!("用户不存在: username={}, org_id={}", info.username, org_id);
warn!(
"用户不存在: username={}, org_id={}",
user_login_dto.username, org_id
);
return Err(BusinessError::AuthError(
"用户不存在或密码不正确".to_string(),
));
@ -86,15 +95,28 @@ pub(crate) async fn handle_login(
let org_user = org_user.unwrap();
let user = db_accessor.query_user(org_user.user_id).await?;
// 检查用户密码
if rtsa_db::password_util::verify_password(&info.password, &user.password) {
if rtsa_db::password_util::verify_password(
&user_login_dto.password,
&user.password,
)
.is_ok()
{
Ok(Jwt::build_from(Claims::new(user.id, org_id))?)
} else {
warn!("密码不匹配: username={}, org_id={}", info.username, org_id);
warn!(
"密码不匹配: username={}, org_id={}",
user_login_dto.username, org_id
);
Err(BusinessError::AuthError(
"用户不存在或密码不正确".to_string(),
))
}
}
_ => Err(BusinessError::AuthError(
"用户不存在或密码不正确".to_string(),
)),
}
}
}
}