feat(auth): 手机号也能用密码登录,验证码之外多一条路

短信通道挂了人就进不来,所以给账号加第二把自助的钥匙:设过密码的用密码进,
没设过的照旧只用验证码,不强制。

- app_user.password_hash(迁移 0008 加列)+ scrypt 哈希(标准库实现,
  n=2^14/r=8/p=1,存 scrypt$n$r$p$salt$hash),不引 passlib
- POST /auth/login/password:没设过密码与密码错都回 2013,不区分账号是否存在;
  15 分钟内连错 10 次 → 2016,期间正确密码也挡住
- POST /auth/password:设/改共用,改要先验旧密码,新旧相同算弱密码 2015
- PATCH /auth/me 改昵称;用户输出走白名单 _user_out,密码哈希一个字节都不出去
- 登录页顶部「验证码 / 密码」两段切换

tests/test_account.py 10 项;全量 pytest 113 passed,ruff 全绿。
This commit is contained in:
Steven
2026-10-31 04:04:55 +08:00
parent 29aae12224
commit a5510cdb79
9 changed files with 544 additions and 56 deletions
@@ -0,0 +1,40 @@
"""登录密码(M1 账号功能)
Revision ID: 0008
Revises: 0007
Create Date: 2026-10-31
M0 只有手机号 + 验证码登录。验证码登录在换手机、短信通道欠费、
或者只想快一点的时候都不够用,所以给账号加一条**可选**的密码登录路径。
两个取舍:
1. **密码可空**。老用户不去设置就一直走验证码,不会被强制改流程;
登录页两种方式并存,谁设过谁自己知道。
2. **只存 scrypt 串**(见 `app/security.py`),不存明文也不存 md5。
密码本身永远不回给客户端,接口只返回 `has_password` 这个布尔。
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
revision: str = "0008"
down_revision: str = "0007"
branch_labels: Sequence[str] | None = None
depends_on: Sequence[str] | None = None
def upgrade() -> None:
op.execute(
"""
ALTER TABLE app_user
ADD COLUMN IF NOT EXISTS password_hash VARCHAR(255)
"""
)
def downgrade() -> None:
op.execute("ALTER TABLE app_user DROP COLUMN IF EXISTS password_hash")