fix: restrict password login to existing phone accounts

This commit is contained in:
2026-04-26 01:11:45 +08:00
parent c4b9b8173f
commit 0a0f3f1bd8
33 changed files with 489 additions and 778 deletions

View File

@@ -18,8 +18,6 @@ use shared_kernel::{
use time::{Duration, OffsetDateTime};
use tracing::{info, warn};
const USERNAME_MIN_LENGTH: usize = 3;
const USERNAME_MAX_LENGTH: usize = 24;
const PASSWORD_MIN_LENGTH: usize = 6;
const PASSWORD_MAX_LENGTH: usize = 128;
const SMS_CODE_LENGTH: usize = 6;
@@ -65,7 +63,7 @@ pub struct PublicUserSearchResult {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PasswordEntryInput {
pub username: String,
pub phone_number: String,
pub password: String,
}
@@ -315,7 +313,7 @@ pub struct AuthStoreSnapshotProcedureResult {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PasswordEntryError {
InvalidUsername,
InvalidPhoneNumber,
InvalidPasswordLength,
InvalidPublicUserCode,
InvalidCredentials,
@@ -476,27 +474,16 @@ impl PasswordEntryService {
input: PasswordEntryInput,
) -> Result<PasswordEntryResult, PasswordEntryError> {
validate_password(&input.password)?;
let normalized_phone = normalize_mainland_china_phone_number(&input.phone_number)
.map_err(|_| PasswordEntryError::InvalidPhoneNumber)?;
let Some(existing_user) = self
.store
.find_by_phone_number_for_password(&normalized_phone.e164)?
else {
return Err(PasswordEntryError::InvalidCredentials);
};
// 登录面板现在固定使用手机号作为密码登录标识;先走手机号索引,
// 再保留历史用户名路径给开发游客和旧测试数据使用。
if let Ok(normalized_phone) = normalize_mainland_china_phone_number(&input.username) {
let Some(existing_user) = self
.store
.find_by_phone_number_for_password(&normalized_phone.e164)?
else {
return Err(PasswordEntryError::InvalidCredentials);
};
return verify_stored_password_user(existing_user, &input.password).await;
}
let username = normalize_username(&input.username)?;
if let Some(existing_user) = self.store.find_by_username(&username)? {
return verify_stored_password_user(existing_user, &input.password).await;
}
Err(PasswordEntryError::InvalidCredentials)
verify_stored_password_user(existing_user, &input.password).await
}
pub fn get_user_by_id(
@@ -1232,17 +1219,6 @@ impl InMemoryAuthStore {
.map_err(RefreshSessionError::Store)
}
fn find_by_username(
&self,
username: &str,
) -> Result<Option<StoredPasswordUser>, PasswordEntryError> {
let state = self
.inner
.lock()
.map_err(|_| PasswordEntryError::Store("用户仓储锁已中毒".to_string()))?;
Ok(state.users_by_username.get(username).cloned())
}
fn find_by_user_id(
&self,
user_id: &str,
@@ -2087,10 +2063,10 @@ impl AuthBindingStatus {
impl fmt::Display for PasswordEntryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidUsername => f.write_str("用户名只允许 3 到 24 位字母、数字、下划线"),
Self::InvalidPhoneNumber => f.write_str("手机号格式不正确"),
Self::InvalidPasswordLength => f.write_str("密码长度需要在 6 到 128 位之间"),
Self::InvalidPublicUserCode => f.write_str("叙世号格式不正确"),
Self::InvalidCredentials => f.write_str("用户名或密码错误"),
Self::InvalidCredentials => f.write_str("手机号或密码错误"),
Self::UserNotFound => f.write_str("用户不存在"),
Self::Store(message) | Self::PasswordHash(message) => f.write_str(message),
}
@@ -2161,7 +2137,7 @@ impl Error for LogoutError {}
fn map_password_store_error(error: PasswordEntryError) -> RefreshSessionError {
match error {
PasswordEntryError::Store(message) => RefreshSessionError::Store(message),
PasswordEntryError::InvalidUsername
PasswordEntryError::InvalidPhoneNumber
| PasswordEntryError::InvalidPasswordLength
| PasswordEntryError::InvalidPublicUserCode
| PasswordEntryError::InvalidCredentials
@@ -2176,7 +2152,7 @@ fn map_password_error_to_phone_error(error: PasswordEntryError) -> PhoneAuthErro
match error {
PasswordEntryError::Store(message) => PhoneAuthError::Store(message),
PasswordEntryError::PasswordHash(message) => PhoneAuthError::PasswordHash(message),
PasswordEntryError::InvalidUsername
PasswordEntryError::InvalidPhoneNumber
| PasswordEntryError::InvalidPasswordLength
| PasswordEntryError::InvalidPublicUserCode
| PasswordEntryError::InvalidCredentials
@@ -2187,7 +2163,7 @@ fn map_password_error_to_phone_error(error: PasswordEntryError) -> PhoneAuthErro
fn map_password_error_to_logout_error(error: PasswordEntryError) -> LogoutError {
match error {
PasswordEntryError::Store(message) => LogoutError::Store(message),
PasswordEntryError::InvalidUsername
PasswordEntryError::InvalidPhoneNumber
| PasswordEntryError::InvalidPasswordLength
| PasswordEntryError::InvalidPublicUserCode
| PasswordEntryError::InvalidCredentials
@@ -2215,21 +2191,6 @@ fn map_refresh_error_to_logout_error(error: RefreshSessionError) -> LogoutError
}
}
fn normalize_username(raw_username: &str) -> Result<String, PasswordEntryError> {
let username = normalize_required_string(raw_username).unwrap_or_default();
let valid_length =
(USERNAME_MIN_LENGTH..=USERNAME_MAX_LENGTH).contains(&username.chars().count());
let valid_chars = username
.chars()
.all(|character| character.is_ascii_alphanumeric() || character == '_');
if !valid_length || !valid_chars {
return Err(PasswordEntryError::InvalidUsername);
}
Ok(username)
}
fn validate_password(password: &str) -> Result<(), PasswordEntryError> {
let length = password.chars().count();
if !(PASSWORD_MIN_LENGTH..=PASSWORD_MAX_LENGTH).contains(&length) {
@@ -2255,7 +2216,10 @@ async fn verify_stored_password_user(
}
Ok(PasswordEntryResult {
user: existing_user.user,
user: AuthUser {
login_method: AuthLoginMethod::Password,
..existing_user.user
},
created: false,
})
}
@@ -2501,7 +2465,7 @@ mod tests {
let error = service
.execute(PasswordEntryInput {
username: "guest_001".to_string(),
phone_number: "13800138000".to_string(),
password: "secret123".to_string(),
})
.await
@@ -2516,7 +2480,7 @@ mod tests {
let user = create_phone_login_user(store.clone(), "13800138000").await;
let service = build_password_service(store);
let changed = service
service
.change_password(ChangePasswordInput {
user_id: user.id.clone(),
current_password: None,
@@ -2526,7 +2490,7 @@ mod tests {
.expect("phone user should set first password");
let result = service
.execute(PasswordEntryInput {
username: changed.user.username.clone(),
phone_number: "13800138000".to_string(),
password: "secret123".to_string(),
})
.await
@@ -2534,7 +2498,7 @@ mod tests {
assert!(!result.created);
assert_eq!(result.user.id, user.id);
assert_eq!(result.user.login_method, AuthLoginMethod::Phone);
assert_eq!(result.user.login_method, AuthLoginMethod::Password);
}
#[tokio::test]
@@ -2553,7 +2517,7 @@ mod tests {
let error = service
.execute(PasswordEntryInput {
username: user.username,
phone_number: "13800138001".to_string(),
password: "secret999".to_string(),
})
.await
@@ -2651,18 +2615,18 @@ mod tests {
}
#[tokio::test]
async fn invalid_username_returns_bad_request_error() {
async fn password_entry_rejects_email_or_username_identifier() {
let service = build_password_service(build_store());
let error = service
.execute(PasswordEntryInput {
username: "坏用户名".to_string(),
phone_number: "user@example.com".to_string(),
password: "secret123".to_string(),
})
.await
.expect_err("invalid username should fail");
.expect_err("email should fail");
assert_eq!(error, PasswordEntryError::InvalidUsername);
assert_eq!(error, PasswordEntryError::InvalidPhoneNumber);
}
#[tokio::test]