fix(auth): reject placeholder mobile numbers

This commit is contained in:
2026-09-01 19:27:46 +08:00
parent caffcb90a9
commit 405d6b94d7
11 changed files with 72 additions and 22 deletions
@@ -22,7 +22,7 @@ public final class AccountManagementDtos {
String newPassword) { } String newPassword) { }
public record CreateAccountRequest(@NotNull Long accountId, public record CreateAccountRequest(@NotNull Long accountId,
@NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username, @NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
@Pattern(regexp = "^1\\d{10}$") String phone, @Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@NotBlank @Size(min = 8, max = 64) @NotBlank @Size(min = 8, max = 64)
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password, @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
@NotBlank String displayName, @NotNull Long tenantId, @NotBlank String displayName, @NotNull Long tenantId,
@@ -42,10 +42,10 @@ public final class AccountManagementDtos {
} }
public record ChangeLoginIdentifiersRequest( public record ChangeLoginIdentifiersRequest(
@NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username, @NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
@NotBlank @Pattern(regexp = "^1\\d{10}$") String phone, @NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@NotBlank String currentPassword) { } @NotBlank String currentPassword) { }
public record UpdateAccountPhoneRequest( public record UpdateAccountPhoneRequest(
@NotBlank @Pattern(regexp = "^1\\d{10}$") String phone) { } @NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String phone) { }
public record UpdateAccountProfileRequest(@NotBlank @Size(max = 64) String displayName) { } public record UpdateAccountProfileRequest(@NotBlank @Size(max = 64) String displayName) { }
public record UpdateAccountStatusRequest(@NotBlank String status) { } public record UpdateAccountStatusRequest(@NotBlank String status) { }
public record RevokeAccountSessionsRequest( public record RevokeAccountSessionsRequest(
@@ -13,7 +13,7 @@ public record MerchantRegisterRequest(
@NotBlank @Size(max = 64) String displayName, @NotBlank @Size(max = 64) String displayName,
@NotBlank @Size(max = 128) String companyName, @NotBlank @Size(max = 128) String companyName,
@NotBlank @Size(max = 64) String legalPerson, @NotBlank @Size(max = 64) String legalPerson,
@NotBlank @Pattern(regexp = "^1\\d{10}$") String legalPhone, @NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String legalPhone,
@Email @Size(max = 128) String contactEmail, @Email @Size(max = 128) String contactEmail,
@Size(min = 2, max = 2) String countryCode, @Size(min = 2, max = 2) String countryCode,
@Size(max = 255) String address @Size(max = 255) String address
@@ -61,7 +61,7 @@ public class AuthService {
description = "User login / 用户登录", captureAfter = false) description = "User login / 用户登录", captureAfter = false)
public TokenResponse login(LoginRequest request) { public TokenResponse login(LoginRequest request) {
String loginId = request.loginId().trim(); String loginId = request.loginId().trim();
AuthAccount account = loginId.matches("^1\\d{10}$") AuthAccount account = loginId.matches("^1[3-9]\\d{9}$")
? accountRepository.findByPhone(loginId) ? accountRepository.findByPhone(loginId)
: accountRepository.findByUsername(loginId); : accountRepository.findByUsername(loginId);
if (account == null) throw invalidCredentials(); if (account == null) throw invalidCredentials();
@@ -23,14 +23,14 @@ BEGIN
IF EXISTS ( IF EXISTS (
SELECT user.phone SELECT user.phone
FROM baiyue_iam.iam_user user FROM baiyue_iam.iam_user user
WHERE user.deleted = 0 AND user.phone REGEXP '^1[0-9]{10}$' WHERE user.deleted = 0 AND user.phone REGEXP '^1[3-9][0-9]{9}$'
GROUP BY user.phone HAVING COUNT(DISTINCT user.auth_user_id) > 1) THEN GROUP BY user.phone HAVING COUNT(DISTINCT user.auth_user_id) > 1) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'IAM 存在重复手机号,禁止建立 Auth 唯一登录索引'; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'IAM 存在重复手机号,禁止建立 Auth 唯一登录索引';
END IF; END IF;
UPDATE auth_account account UPDATE auth_account account
JOIN baiyue_iam.iam_user user ON user.auth_user_id = account.id AND user.deleted = 0 JOIN baiyue_iam.iam_user user ON user.auth_user_id = account.id AND user.deleted = 0
SET account.phone = user.phone SET account.phone = user.phone
WHERE account.phone IS NULL AND user.phone REGEXP '^1[0-9]{10}$'; WHERE account.phone IS NULL AND user.phone REGEXP '^1[3-9][0-9]{9}$';
IF NOT EXISTS ( IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics SELECT 1 FROM information_schema.statistics
WHERE table_schema = 'baiyue_auth' AND table_name = 'auth_account' WHERE table_schema = 'baiyue_auth' AND table_name = 'auth_account'
@@ -0,0 +1,38 @@
package com.baiyue.auth.repository;
import com.baiyue.auth.dto.AccountManagementDtos.ChangeLoginIdentifiersRequest;
import com.baiyue.auth.dto.MerchantRegisterRequest;
import jakarta.validation.Validation;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import static org.assertj.core.api.Assertions.assertThat;
class LoginIdentifierContractTest {
@Test
void mainlandMobileRuleRejectsPlaceholderNumbersAcrossJavaAndSql() throws Exception {
try (var factory = Validation.buildDefaultValidatorFactory()) {
var validator = factory.getValidator();
var invalidRegistration = new MerchantRegisterRequest(
"merchant.user", "Password1", "测试用户", "测试企业", "张三",
"11111111111", null, "CN", null);
var validChange = new ChangeLoginIdentifiersRequest(
"merchant.user", "13800138000", "Password1");
var invalidChange = new ChangeLoginIdentifiersRequest(
"merchant.user", "11111111111", "Password1");
assertThat(validator.validate(invalidRegistration)).isNotEmpty();
assertThat(validator.validate(validChange)).isEmpty();
assertThat(validator.validate(invalidChange)).isNotEmpty();
}
String sql = new ClassPathResource("db/auth-login-identifiers.sql")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(sql)
.contains("REGEXP '^1[3-9][0-9]{9}$'")
.doesNotContain("REGEXP '^1[0-9]{10}$'");
}
}
@@ -128,6 +128,18 @@ class AuthServiceSmallJwtTest {
verify(repository, never()).findByUsername("13800138000"); verify(repository, never()).findByUsername("13800138000");
} }
@Test
void placeholderNumberIsNeverResolvedAsAMobileLoginIdentifier() {
when(repository.findByUsername("11111111111")).thenReturn(null);
assertThatThrownBy(() -> service.login(new LoginRequest("11111111111", "secret")))
.isInstanceOf(com.baiyue.common.core.exception.BizException.class)
.hasMessageContaining("登录标识或密码错误");
verify(repository).findByUsername("11111111111");
verify(repository, never()).findByPhone("11111111111");
}
@Test @Test
void refreshRotatesDomesticAndOverseasTokensFromOneAuthorizationSnapshot() { void refreshRotatesDomesticAndOverseasTokensFromOneAuthorizationSnapshot() {
AuthAccount account = new AuthAccount(); AuthAccount account = new AuthAccount();
@@ -18,7 +18,7 @@ public final class MemberDtos {
@NotBlank @Size(min = 8, max = 64) @NotBlank @Size(min = 8, max = 64)
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password, @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
@NotBlank @Size(max = 64) String displayName, @NotBlank @Size(max = 64) String displayName,
@Pattern(regexp = "^1\\d{10}$") String phone, @Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@Email @Size(max = 128) String email, @Email @Size(max = 128) String email,
@NotBlank String memberType, @NotBlank String memberType,
@Size(max = 64) String memberNo, @Size(max = 64) String memberNo,
@@ -30,7 +30,7 @@ public final class MemberDtos {
public record UpdateMemberRequest( public record UpdateMemberRequest(
@NotBlank @Size(max = 64) String displayName, @NotBlank @Size(max = 64) String displayName,
@Pattern(regexp = "^1\\d{10}$") String phone, @Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@Email @Size(max = 128) String email, @Email @Size(max = 128) String email,
@NotBlank String memberType, @NotBlank String memberType,
@Size(max = 64) String memberNo, @Size(max = 64) String memberNo,
@@ -20,7 +20,7 @@ public final class PlatformInternalUserDtos {
Long authUserId, Long authUserId,
String userType, String userType,
@NotBlank @Size(max = 64) String displayName, @NotBlank @Size(max = 64) String displayName,
@Pattern(regexp = "^1\\d{10}$") String phone, @Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@Email @Size(max = 128) String email, @Email @Size(max = 128) String email,
@Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username, @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
@Size(min = 8, max = 64) @Size(min = 8, max = 64)
@@ -31,7 +31,7 @@ public final class PlatformInternalUserDtos {
public record UpdateInternalUserRequest( public record UpdateInternalUserRequest(
@NotBlank @Size(max = 64) String displayName, @NotBlank @Size(max = 64) String displayName,
@Pattern(regexp = "^1\\d{10}$") String phone, @Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@Email @Size(max = 128) String email, @Email @Size(max = 128) String email,
@NotBlank @Size(max = 64) String memberNo, @NotBlank @Size(max = 64) String memberNo,
@NotNull List<Long> roleIds) { } @NotNull List<Long> roleIds) { }
@@ -16,7 +16,7 @@ public final class PlatformUserManagementDtos {
public record CreateUserRequest( public record CreateUserRequest(
@NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username, @NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
@NotBlank @Size(max = 64) String displayName, @NotBlank @Size(max = 64) String displayName,
@NotBlank @Pattern(regexp = "^1\\d{10}$", message = "手机号格式不正确") String phone, @NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") String phone,
@Email @Size(max = 128) String email, @Email @Size(max = 128) String email,
@NotNull Long tenantId, @NotNull Long tenantId,
@NotNull List<Long> roleIds, @NotNull List<Long> roleIds,
@@ -32,7 +32,7 @@ public final class PlatformUserManagementDtos {
public record UpdateUserRequest( public record UpdateUserRequest(
@NotBlank @Size(max = 64) String displayName, @NotBlank @Size(max = 64) String displayName,
@NotBlank @Pattern(regexp = "^1\\d{10}$", message = "手机号格式不正确") String phone, @NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") String phone,
@Email @Size(max = 128) String email, @Email @Size(max = 128) String email,
@NotNull Long tenantId, @NotNull Long tenantId,
@NotNull List<Long> roleIds, @NotNull List<Long> roleIds,
@@ -17,7 +17,7 @@ public final class ProfileDtos {
public record ChangeLoginIdentifiersRequest( public record ChangeLoginIdentifiersRequest(
@NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username, @NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
@NotBlank @Pattern(regexp = "^1\\d{10}$") String phone, @NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@NotBlank String currentPassword) { } @NotBlank String currentPassword) { }
public record AddressRequest(@NotBlank String addressType, String countryCode, String province, public record AddressRequest(@NotBlank String addressType, String countryCode, String province,
+8 -8
View File
@@ -982,7 +982,7 @@ components:
authUserId: {type: string, pattern: '^\d+$'} authUserId: {type: string, pattern: '^\d+$'}
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'} username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
displayName: {type: string, maxLength: 64} displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1\d{10}$'} phone: {type: string, pattern: '^1[3-9]\d{9}$'}
email: {type: string, format: email, nullable: true} email: {type: string, format: email, nullable: true}
UpdatePersonalProfileRequest: UpdatePersonalProfileRequest:
type: object type: object
@@ -995,7 +995,7 @@ components:
required: [username, phone, currentPassword] required: [username, phone, currentPassword]
properties: properties:
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'} username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
phone: {type: string, pattern: '^1\d{10}$'} phone: {type: string, pattern: '^1[3-9]\d{9}$'}
currentPassword: {type: string, format: password, writeOnly: true} currentPassword: {type: string, format: password, writeOnly: true}
PersonalProfileApiResponse: PersonalProfileApiResponse:
allOf: allOf:
@@ -1398,7 +1398,7 @@ components:
properties: properties:
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'} username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
displayName: {type: string, maxLength: 64} displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1\\d{10}$'} phone: {type: string, pattern: '^1[3-9]\\d{9}$'}
email: {type: string, format: email, maxLength: 128} email: {type: string, format: email, maxLength: 128}
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0} tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0}
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}} roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
@@ -1410,7 +1410,7 @@ components:
required: [displayName, phone, tenantId, roleIds] required: [displayName, phone, tenantId, roleIds]
properties: properties:
displayName: {type: string, maxLength: 64} displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1\\d{10}$'} phone: {type: string, pattern: '^1[3-9]\\d{9}$'}
email: {type: string, format: email, maxLength: 128} email: {type: string, format: email, maxLength: 128}
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0} tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0}
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}} roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
@@ -1424,7 +1424,7 @@ components:
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'} username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true} password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true}
displayName: {type: string, maxLength: 64} displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1\d{10}$'} phone: {type: string, pattern: '^1[3-9]\d{9}$'}
email: {type: string, format: email, maxLength: 128} email: {type: string, format: email, maxLength: 128}
memberNo: {type: string, maxLength: 64} memberNo: {type: string, maxLength: 64}
roleIds: {type: array, items: {type: integer, format: int64}} roleIds: {type: array, items: {type: integer, format: int64}}
@@ -1433,7 +1433,7 @@ components:
required: [displayName, memberNo, roleIds] required: [displayName, memberNo, roleIds]
properties: properties:
displayName: {type: string, maxLength: 64} displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1\d{10}$'} phone: {type: string, pattern: '^1[3-9]\d{9}$'}
email: {type: string, format: email, maxLength: 128} email: {type: string, format: email, maxLength: 128}
memberNo: {type: string, maxLength: 64} memberNo: {type: string, maxLength: 64}
roleIds: {type: array, items: {type: integer, format: int64}} roleIds: {type: array, items: {type: integer, format: int64}}
@@ -1495,7 +1495,7 @@ components:
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'} username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
password: {type: string, format: password, minLength: 8, maxLength: 64} password: {type: string, format: password, minLength: 8, maxLength: 64}
displayName: {type: string, maxLength: 64} displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1\d{10}$'} phone: {type: string, pattern: '^1[3-9]\d{9}$'}
email: {type: string, format: email, maxLength: 128} email: {type: string, format: email, maxLength: 128}
memberType: {type: string, enum: [EMPLOYEE]} memberType: {type: string, enum: [EMPLOYEE]}
memberNo: {type: string} memberNo: {type: string}
@@ -1508,7 +1508,7 @@ components:
required: [displayName, memberType] required: [displayName, memberType]
properties: properties:
displayName: {type: string, maxLength: 64} displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1\d{10}$'} phone: {type: string, pattern: '^1[3-9]\d{9}$'}
email: {type: string, format: email, maxLength: 128} email: {type: string, format: email, maxLength: 128}
memberType: {type: string, enum: [EMPLOYEE]} memberType: {type: string, enum: [EMPLOYEE]}
memberNo: {type: string, maxLength: 64} memberNo: {type: string, maxLength: 64}