登录支持用户名,与手机号并存 (#2)

原来 Auth::attempt 只按 phone_hash 查,用户名永远登不进去。
家人账号常常不填手机号,演示场景下用名字登录更自然。

- 先按手机号 hash 精确查(唯一),未命中再按 name 查
- 重名命中多条一律拒绝,绝不取第一条(否则 A 的密码能登进 B 的账号)
- 表单 label 改「手机号 / 用户名」,错误文案改「账号或密码错误」

验证:逻辑单测 10/10(含重名拒绝);反向验证改坏判断即变红 (#2)
This commit is contained in:
2026-11-01 11:07:30 +08:00
parent fdc890fee0
commit d6a7cc99bc
3 changed files with 34 additions and 11 deletions
+31 -8
View File
@@ -60,21 +60,44 @@ class Auth
}
/**
* 通过手机号 + 密码尝试登录
* 尝试登录。账号可以是**手机号**,也可以是**姓名**。
*
* 为什么两种都收:安装向导创建的管理员填的是手机号,而家人账号常常不填手机号
* (只有名字),演示/内网场景下用名字登录更自然。
*
* 顺序:先按手机号 hash 精确匹配(唯一,不会歧义),再按姓名匹配。
* 姓名可能重名 ⇒ **命中多条一律判失败**,绝不「取第一条」——
* 那等于让 A 用密码登进了 B 的账号,是最坏的一种 bug。
*
* @return array|null 成功返回用户数组,失败返回 null
*/
public static function attempt(string $phone, string $password): ?array
public static function attempt(string $account, string $password): ?array
{
$phone = trim($phone);
if ($phone === '' || $password === '') return null;
$account = trim($account);
if ($account === '' || $password === '') return null;
$phoneHash = hash('sha256', $phone);
$stmt = Database::get()->prepare(
$db = Database::get();
// ① 手机号:走 sha256 精确查(唯一,不会歧义)
$stmt = $db->prepare(
'SELECT * FROM users WHERE phone_hash = ? AND role IN ("admin","user") AND status = "active" LIMIT 1'
);
$stmt->execute([$phoneHash]);
$stmt->execute([hash('sha256', $account)]);
$user = $stmt->fetch();
if (!$user) return null;
// ② 手机号没命中,再按姓名查
if (!$user) {
$stmt = $db->prepare(
'SELECT * FROM users WHERE name = ? AND role IN ("admin","user") AND status = "active"'
);
$stmt->execute([$account]);
$rows = $stmt->fetchAll();
// 重名 → 拒绝,不做猜测
if (count($rows) !== 1) return null;
$user = $rows[0];
}
if (!password_verify($password, (string)($user['password_hash'] ?? ''))) return null;
return $user;
+1 -1
View File
@@ -24,7 +24,7 @@ class AuthController
$user = Auth::attempt($phone, $password);
if (!$user) {
flash('error', '手机号或密码错误');
flash('error', '账号或密码错误');
redirect('auth/login');
}
+2 -2
View File
@@ -5,8 +5,8 @@
<p class="muted">请登录后继续</p>
<div class="form-row">
<label>手机号</label>
<input type="text" name="phone" required autofocus placeholder="请输入手机号">
<label>手机号 / 用户名</label>
<input type="text" name="phone" required autofocus placeholder="手机号或用户名">
</div>
<div class="form-row">
<label>密码</label>