fix(auth): send sms verify codes via aliyun

This commit is contained in:
2026-05-16 22:33:29 +08:00
parent 804f1e32be
commit c3ad28577c
8 changed files with 239 additions and 349 deletions

View File

@@ -9,6 +9,7 @@ platform-auth = { workspace = true }
shared-kernel = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
time = { workspace = true, features = ["formatting", "parsing"] }
tracing = { workspace = true }

View File

@@ -18,10 +18,11 @@ use std::{
};
use platform_auth::{
SmsAuthProvider, SmsProviderError, SmsSendCodeRequest, SmsVerifyCodeRequest, hash_password,
SmsAuthProvider, SmsAuthProviderKind, SmsProviderError, SmsSendCodeRequest, hash_password,
verify_password,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use shared_kernel::{
build_prefixed_uuid_id, format_rfc3339 as format_shared_rfc3339, new_uuid_simple_string,
normalize_optional_string, normalize_required_string, parse_rfc3339,
@@ -77,6 +78,7 @@ struct StoredRefreshSession {
struct StoredPhoneCode {
phone_number: String,
scene: PhoneAuthScene,
verify_code_hash: String,
expires_at: String,
last_sent_at: String,
failed_attempts: u32,
@@ -117,6 +119,7 @@ pub struct AuthUserService {
pub struct PhoneAuthService {
store: InMemoryAuthStore,
sms_provider: SmsAuthProvider,
verify_code_salt: String,
}
#[derive(Clone, Debug)]
@@ -431,6 +434,7 @@ impl PhoneAuthService {
Self {
store,
sms_provider,
verify_code_salt: new_uuid_simple_string(),
}
}
@@ -442,6 +446,7 @@ impl PhoneAuthService {
let scene = input.scene.clone();
let normalized_phone = normalize_mainland_china_phone_number(&input.phone_number)?;
let national_phone_number = build_national_phone_number(&normalized_phone.e164)?;
let verify_code = self.generate_phone_verify_code();
info!(
scene = scene.as_str(),
provider = self.sms_provider.kind().as_str(),
@@ -457,12 +462,19 @@ impl PhoneAuthService {
let expires_at = format_rfc3339(expires_at).map_err(|message| {
PhoneAuthError::Store(format!("短信验证码过期时间格式化失败:{message}"))
})?;
let verify_code_hash = hash_phone_verify_code(
&self.verify_code_salt,
&normalized_phone.e164,
&scene,
&verify_code,
);
let provider_result = self
.sms_provider
.send_code(SmsSendCodeRequest {
national_phone_number,
scene: input.scene.as_str().to_string(),
verify_code,
})
.await
.map_err(map_sms_provider_error_to_phone_error)?;
@@ -488,6 +500,7 @@ impl PhoneAuthService {
StoredPhoneCode {
phone_number: normalized_phone.e164.clone(),
scene,
verify_code_hash,
expires_at,
last_sent_at: format_rfc3339(now).map_err(|message| {
PhoneAuthError::Store(format!("短信验证码发送时间格式化失败:{message}"))
@@ -516,28 +529,12 @@ impl PhoneAuthService {
) -> Result<PhoneLoginResult, PhoneAuthError> {
let normalized_phone = normalize_mainland_china_phone_number(&input.phone_number)?;
verify_sms_code_format(&input.verify_code)?;
let provider_out_id = self.store.assert_phone_code_active(
let provider_out_id = self.verify_phone_code(
&normalized_phone.e164,
&PhoneAuthScene::Login,
&input.verify_code,
now,
)?;
match self
.sms_provider
.verify_code(SmsVerifyCodeRequest {
national_phone_number: build_national_phone_number(&normalized_phone.e164)?,
verify_code: input.verify_code.trim().to_string(),
provider_out_id: provider_out_id.clone(),
})
.await
{
Ok(()) => self
.store
.consume_phone_code_success(&normalized_phone.e164, &PhoneAuthScene::Login)?,
Err(SmsProviderError::InvalidVerifyCode) => self
.store
.consume_phone_code_failure(&normalized_phone.e164, &PhoneAuthScene::Login)?,
Err(other) => return Err(map_sms_provider_error_to_phone_error(other)),
}
if let Some(user) = self
.store
@@ -582,30 +579,12 @@ impl PhoneAuthService {
let normalized_phone = normalize_mainland_china_phone_number(&input.phone_number)?;
verify_sms_code_format(&input.verify_code)?;
validate_password(&input.new_password).map_err(map_password_error_to_phone_error)?;
let provider_out_id = self.store.assert_phone_code_active(
let provider_out_id = self.verify_phone_code(
&normalized_phone.e164,
&PhoneAuthScene::ResetPassword,
&input.verify_code,
now,
)?;
match self
.sms_provider
.verify_code(SmsVerifyCodeRequest {
national_phone_number: build_national_phone_number(&normalized_phone.e164)?,
verify_code: input.verify_code.trim().to_string(),
provider_out_id: provider_out_id.clone(),
})
.await
{
Ok(()) => self.store.consume_phone_code_success(
&normalized_phone.e164,
&PhoneAuthScene::ResetPassword,
)?,
Err(SmsProviderError::InvalidVerifyCode) => self.store.consume_phone_code_failure(
&normalized_phone.e164,
&PhoneAuthScene::ResetPassword,
)?,
Err(other) => return Err(map_sms_provider_error_to_phone_error(other)),
}
self.store
.find_by_phone_number(&normalized_phone.e164)?
@@ -632,28 +611,12 @@ impl PhoneAuthService {
) -> Result<BindWechatPhoneResult, PhoneAuthError> {
let normalized_phone = normalize_mainland_china_phone_number(&input.phone_number)?;
verify_sms_code_format(&input.verify_code)?;
let provider_out_id = self.store.assert_phone_code_active(
self.verify_phone_code(
&normalized_phone.e164,
&PhoneAuthScene::BindPhone,
&input.verify_code,
now,
)?;
match self
.sms_provider
.verify_code(SmsVerifyCodeRequest {
national_phone_number: build_national_phone_number(&normalized_phone.e164)?,
verify_code: input.verify_code.trim().to_string(),
provider_out_id,
})
.await
{
Ok(()) => self
.store
.consume_phone_code_success(&normalized_phone.e164, &PhoneAuthScene::BindPhone)?,
Err(SmsProviderError::InvalidVerifyCode) => self
.store
.consume_phone_code_failure(&normalized_phone.e164, &PhoneAuthScene::BindPhone)?,
Err(other) => return Err(map_sms_provider_error_to_phone_error(other)),
}
let current_user = self
.store
@@ -677,6 +640,35 @@ impl PhoneAuthService {
})
}
fn verify_phone_code(
&self,
phone_number: &str,
scene: &PhoneAuthScene,
verify_code: &str,
now: OffsetDateTime,
) -> Result<Option<String>, PhoneAuthError> {
let stored = self.store.get_active_phone_code(phone_number, scene, now)?;
let expected_hash =
hash_phone_verify_code(&self.verify_code_salt, phone_number, scene, verify_code);
if stored.verify_code_hash != expected_hash {
self.store.consume_phone_code_failure(phone_number, scene)?;
return Err(PhoneAuthError::InvalidVerifyCode);
}
self.store.consume_phone_code_success(phone_number, scene)?;
Ok(stored.provider_out_id)
}
fn generate_phone_verify_code(&self) -> String {
match self.sms_provider.kind() {
SmsAuthProviderKind::Mock => self
.sms_provider
.mock_verify_code()
.map(str::to_string)
.unwrap_or_else(|| "123456".to_string()),
SmsAuthProviderKind::Aliyun => generate_random_phone_verify_code(),
}
}
pub async fn bind_wechat_verified_phone(
&self,
input: BindWechatVerifiedPhoneInput,
@@ -1518,12 +1510,12 @@ impl InMemoryAuthStore {
})
}
fn assert_phone_code_active(
fn get_active_phone_code(
&self,
phone_number: &str,
scene: &PhoneAuthScene,
now: OffsetDateTime,
) -> Result<Option<String>, PhoneAuthError> {
) -> Result<StoredPhoneCode, PhoneAuthError> {
let mut state = self
.inner
.lock()
@@ -1543,7 +1535,7 @@ impl InMemoryAuthStore {
state.phone_codes_by_key.remove(&key);
return Err(PhoneAuthError::VerifyCodeExpired);
}
Ok(stored.provider_out_id)
Ok(stored)
}
fn consume_phone_code_success(
@@ -2065,7 +2057,6 @@ impl InMemoryAuthStore {
fn map_sms_provider_error_to_phone_error(error: SmsProviderError) -> PhoneAuthError {
match error {
SmsProviderError::InvalidVerifyCode => PhoneAuthError::InvalidVerifyCode,
SmsProviderError::InvalidConfig(message) => {
PhoneAuthError::SmsProviderInvalidConfig(message)
}
@@ -2139,6 +2130,36 @@ fn build_random_password_seed() -> String {
)
}
fn generate_random_phone_verify_code() -> String {
let digest = Sha256::digest(new_uuid_simple_string().as_bytes());
let mut digits = digest
.iter()
.take(SMS_CODE_LENGTH)
.map(|byte| char::from(b'0' + (*byte % 10)))
.collect::<String>();
while digits.len() < SMS_CODE_LENGTH {
digits.push('0');
}
digits
}
fn hash_phone_verify_code(
salt: &str,
phone_number: &str,
scene: &PhoneAuthScene,
verify_code: &str,
) -> String {
let content = format!(
"{}:{}:{}:{}",
salt,
phone_number.trim(),
scene.as_str(),
verify_code.trim()
);
let digest = Sha256::digest(content.as_bytes());
digest.iter().map(|byte| format!("{byte:02x}")).collect()
}
fn format_rfc3339(value: OffsetDateTime) -> Result<String, String> {
format_shared_rfc3339(value)
}
@@ -2655,6 +2676,14 @@ mod tests {
assert!(bind_result.await.is_ok());
}
#[test]
fn random_phone_verify_code_is_six_digits() {
let code = generate_random_phone_verify_code();
assert_eq!(code.len(), SMS_CODE_LENGTH);
assert!(code.chars().all(|character| character.is_ascii_digit()));
}
#[tokio::test]
async fn phone_login_expires_code_after_too_many_wrong_attempts() {
let service = build_phone_service(build_store());