use std::time::Duration; use crate::cache::Cache; use crate::errors::{AuthError, AuthResult}; use crate::share::crypto; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CodeAttempt { Missing, Exceeded, Allowed(i64), } pub fn attempts_key(key: &str) -> String { format!("{key}:attempts") } /// 验证码只存哈希:Redis 里的明文泄露不等于验证码泄露。 #[tracing::instrument(skip_all, fields(request_id = %envelope::context::resolve_request_id(_request_id)))] pub async fn store_email_code( cache: &Cache, key: &str, code: &str, ttl_secs: u64, _request_id: Option<&str>, ) -> AuthResult<()> { let digest = crypto::sha256_hex(code.trim().as_bytes()); cache .set_ex(key, digest.as_str(), Duration::from_secs(ttl_secs.max(1))) .await?; cache.delete(&attempts_key(key)).await?; Ok(()) } #[tracing::instrument(skip_all, fields(request_id = %envelope::context::resolve_request_id(_request_id)))] pub async fn take_code_attempt( cache: &Cache, key: &str, max_attempts: i64, _request_id: Option<&str>, ) -> AuthResult { if !cache.exists(key).await? { return Ok(CodeAttempt::Missing); } let ttl = cache .ttl(key) .await? .unwrap_or_else(|| Duration::from_secs(1)); let attempts = cache.scripts().incr_ex(&attempts_key(key), ttl).await?; if attempts > max_attempts { cache.delete(key).await?; cache.delete(&attempts_key(key)).await?; return Ok(CodeAttempt::Exceeded); } Ok(CodeAttempt::Allowed(attempts)) } #[tracing::instrument(skip_all, fields(request_id = %envelope::context::resolve_request_id(_request_id)))] pub async fn consume_email_code( cache: &Cache, key: &str, code: &str, max_attempts: i64, _request_id: Option<&str>, ) -> AuthResult<()> { match take_code_attempt(cache, key, max_attempts, _request_id).await? { CodeAttempt::Missing => Err(AuthError::InvalidEmailCode), CodeAttempt::Exceeded => Err(AuthError::EmailCodeAttemptsExceeded), CodeAttempt::Allowed(_) => { let digest = crypto::sha256_hex(code.trim().as_bytes()); // 原子比较并删除:并发提交同一个码时只有一方能消费成功。 let consumed = cache .scripts() .consume_matching(key, digest.as_bytes()) .await?; if !consumed { return Err(AuthError::InvalidEmailCode); } Ok(()) } } }