feat(auth): 添加异步密码哈希和验证函数
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

refactor(relay): 复用HTTP客户端和请求体序列化结果

feat(kernel): 添加获取单个审批记录的方法

fix(store): 改进SaaS连接错误分类和降级处理

docs: 更新审计文档和系统架构文档

refactor(prompt): 优化SQL查询参数化绑定

refactor(migration): 使用静态SQL和COALESCE更新配置项

feat(commands): 添加审批执行状态追踪和事件通知

chore: 更新启动脚本以支持Admin后台

fix(auth-guard): 优化授权状态管理和错误处理

refactor(db): 使用异步密码哈希函数

refactor(totp): 使用异步密码验证函数

style: 清理无用文件和注释

docs: 更新功能全景和审计文档

refactor(service): 优化HTTP客户端重用和请求处理

fix(connection): 改进SaaS不可用时的降级处理

refactor(handlers): 使用异步密码验证函数

chore: 更新依赖和工具链配置
This commit is contained in:
iven
2026-03-29 21:45:29 +08:00
parent b7ec317d2c
commit 7de294375b
34 changed files with 2041 additions and 894 deletions

View File

@@ -8,7 +8,7 @@ use crate::error::{SaasError, SaasResult};
use crate::models::{AccountAuthRow, AccountLoginRow};
use super::{
jwt::{create_token, create_refresh_token, verify_token, verify_token_skip_expiry},
password::{hash_password, verify_password},
password::{hash_password_async, verify_password_async},
types::{AuthContext, LoginRequest, LoginResponse, RegisterRequest, ChangePasswordRequest, AccountPublic, RefreshRequest},
};
@@ -25,7 +25,8 @@ pub async fn register(
if req.username.len() > 32 {
return Err(SaasError::InvalidInput("用户名最多 32 个字符".into()));
}
let username_re = regex::Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap();
static USERNAME_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
let username_re = USERNAME_RE.get_or_init(|| regex::Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap());
if !username_re.is_match(&req.username) {
return Err(SaasError::InvalidInput("用户名只能包含字母、数字、下划线和连字符".into()));
}
@@ -56,7 +57,7 @@ pub async fn register(
return Err(SaasError::AlreadyExists("用户名或邮箱已存在".into()));
}
let password_hash = hash_password(&req.password)?;
let password_hash = hash_password_async(req.password.clone()).await?;
let account_id = uuid::Uuid::new_v4().to_string();
let role = "user".to_string(); // 注册固定为普通用户,角色由管理员分配
let display_name = req.display_name.unwrap_or_default();
@@ -138,7 +139,7 @@ pub async fn login(
return Err(SaasError::Forbidden(format!("账号已{},请联系管理员", r.status)));
}
if !verify_password(&req.password, &r.password_hash)? {
if !verify_password_async(req.password.clone(), r.password_hash.clone()).await? {
return Err(SaasError::AuthError("用户名或密码错误".into()));
}
@@ -328,12 +329,12 @@ pub async fn change_password(
.await?;
// 验证旧密码
if !verify_password(&req.old_password, &password_hash)? {
if !verify_password_async(req.old_password.clone(), password_hash.clone()).await? {
return Err(SaasError::AuthError("旧密码错误".into()));
}
// 更新密码
let new_hash = hash_password(&req.new_password)?;
let new_hash = hash_password_async(req.new_password.clone()).await?;
let now = chrono::Utc::now().to_rfc3339();
sqlx::query("UPDATE accounts SET password_hash = $1, updated_at = $2 WHERE id = $3")
.bind(&new_hash)

View File

@@ -1,4 +1,8 @@
//! 密码哈希 (Argon2id)
//!
//! Argon2 是 CPU 密集型操作(~100-500ms不能在 tokio worker 线程上直接执行,
//! 否则会阻塞整个异步运行时。所有 async 上下文必须使用 `hash_password_async`
//! 和 `verify_password_async`。
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
@@ -7,7 +11,7 @@ use argon2::{
use crate::error::{SaasError, SaasResult};
/// 哈希密码
/// 哈希密码(同步版本,仅用于测试和启动时 seed
pub fn hash_password(password: &str) -> SaasResult<String> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
@@ -17,7 +21,7 @@ pub fn hash_password(password: &str) -> SaasResult<String> {
Ok(hash.to_string())
}
/// 验证密码
/// 验证密码(同步版本,仅用于测试)
pub fn verify_password(password: &str, hash: &str) -> SaasResult<bool> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|e| SaasError::PasswordHash(e.to_string()))?;
@@ -26,6 +30,20 @@ pub fn verify_password(password: &str, hash: &str) -> SaasResult<bool> {
.is_ok())
}
/// 异步哈希密码 — 在 spawn_blocking 线程池中执行 Argon2
pub async fn hash_password_async(password: String) -> SaasResult<String> {
tokio::task::spawn_blocking(move || hash_password(&password))
.await
.map_err(|e| SaasError::Internal(format!("spawn_blocking error: {e}")))?
}
/// 异步验证密码 — 在 spawn_blocking 线程池中执行 Argon2
pub async fn verify_password_async(password: String, hash: String) -> SaasResult<bool> {
tokio::task::spawn_blocking(move || verify_password(&password, &hash))
.await
.map_err(|e| SaasError::Internal(format!("spawn_blocking error: {e}")))?
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -212,7 +212,7 @@ pub async fn disable_totp(
.fetch_one(&state.db)
.await?;
if !crate::auth::password::verify_password(&req.password, &password_hash)? {
if !crate::auth::password::verify_password_async(req.password.clone(), password_hash.clone()).await? {
return Err(SaasError::AuthError("密码错误".into()));
}