Skip to content
Clone
main
83 lines 2.5 KB Rust
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<CodeAttempt> {
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(())
}
}
}