项目:用户注册、登录与个人主页(完整 MVP 交付)
本文档提供了一个完整的、可直接落地的用户认证体系 MVP 套件,包括:
- 产品需求文档(PRD)
- 两个架构决策记录(ADR)
- OpenAPI 3.0 规范(YAML)
- 可运行的 FastAPI 实现(完整代码 + 运行说明)
可直接将文档放入仓库docs/目录,将代码放入src/或根目录。
一、产品需求文档(PRD)
1. 概述
功能名称:用户注册、登录与个人主页
目标:构建一个简洁、安全的用户账户体系,支持邮箱+密码注册、登录,以及登录后查看/编辑个人主页(昵称、简介、头像 URL)。作为后续社交、内容等功能的用户基础模块。
目标用户:所有新用户及需要账户体系的业务线。
2. 背景与问题
当前系统缺少账户体系,用户无法保存个性化信息、偏好设置或发布内容。需要一个最小可行产品(MVP)快速验证用户对注册及完善个人信息的意愿。
3. 目标与成功指标
核心目标:4 周内完成注册、登录、个人主页编辑功能并上线。
成功指标(上线后可量化跟踪):
- 新用户注册转化率 ≥ 5%(访问注册页 → 成功注册的比例)
- 7 天用户留存率 ≥ 15%
- 注册后 7 天内完成个人主页信息填写率 ≥ 40%(至少填写昵称、简介或头像之一)
4. 用户故事(User Stories)
- 作为访客,我可以通过邮箱 + 密码注册账号,以获得系统使用权限。
- 作为已注册用户,我可以使用邮箱 + 密码登录,并获取访问令牌(JWT)。
- 作为已登录用户,我可以查看并编辑自己的个人主页信息(昵称、简介、头像 URL)。
5. 功能清单(按优先级)
MVP 必选功能:
- 注册接口
POST /signup(邮箱 + 密码,密码哈希存储,返回 201) - 登录接口
POST /login(返回 JWT access_token) - 获取个人信息
GET /me(需认证) - 更新个人信息
PUT /me(需认证,支持更新昵称、简介、头像 URL)
可选后续迭代:
- 邮箱验证
- 密码找回
- 第三方社交登录(OAuth)
- 头像文件上传与存储
- 多因素认证(MFA)
6. 非功能性需求
- 安全性:密码使用 bcrypt 哈希存储;JWT 有效期 7 天;接口限流防暴力破解。
- 可用性:MVP 阶段单实例部署即可,长期目标 API 可用率 ≥ 99%。
- 隐私保护:响应中绝不返回密码或哈希值;日志对敏感字段脱敏。
7. 验收标准(Acceptance Criteria)
- 注册:合法参数返回 201;重复邮箱返回 400。
- 登录:正确凭证返回 200 + access_token;错误凭证返回 401。
- 认证保护:未携带有效 token 访问
/me返回 401。 - 更新个人信息:仅更新提供的字段,返回最新用户信息。
8. MVP 范围与时间规划
范围:仅实现上述 4 个核心 API(后端),可选最简前端演示页面。
时间箱:4 周(含设计、开发、测试、验证上线)。
9. 风险与缓解措施
风险:简单实现可能存在安全薄弱点(如 JWT 密钥管理不当)。
缓解:
- 使用成熟库(PyJWT + passlib[bcrypt])
- 设置合理 token 有效期
- 短周期迭代 + 安全审核
二、架构决策记录(ADR)
建议存放路径:docs/adr/
ADR 0001 - 使用 JWT(HS256)作为认证令牌
日期:2025-12-17
状态:Accepted
背景:需要快速实现前后端分离的认证机制,用户量中等,短期内无需复杂会话一致性。
决策:采用 JWT(HS256 对称签名),token 中包含sub(用户 ID)和exp(过期时间)。
考虑的备选方案:
- 完整 OAuth2:对第三方友好,但实现成本高,超出 MVP。
- 后端 Session:实现简单,但不利于横向扩展和移动端。
后果:
- 优点:实现快速,客户端易存储与传递,满足 MVP 需求。
- 缺点:无法服务器端即时撤销(可通过短有效期或后续引入黑名单缓解),需妥善管理密钥。
ADR 0002 - MVP 阶段使用 SQLite 存储用户信息
日期:2025-12-17
状态:Accepted
背景:MVP 需要快速、可单机运行的 PoC,降低开发与部署成本。
决策:使用 SQLite 文件数据库,后续按需迁移至 MySQL/PostgreSQL。
考虑的备选方案:
- PostgreSQL/MySQL:生产更稳健,但需额外运维配置。
- MongoDB 等 NoSQL:结构灵活,但事务一致性不如关系型数据库。
后果:
- 优点:零运维、易迁移,适合早期测试与演示。
- 缺点:并发与扩展能力有限,生产环境必须迁移。
三、OpenAPI 规范(openapi.yaml)
建议存放路径:docs/spec/openapi.yaml
openapi:3.0.1info:title:用户系统 APIversion:"1.0.0"description:用户注册、登录与个人主页 APIservers:-url:http://localhost:8000paths:/signup:post:summary:用户注册requestBody:required:truecontent:application/json:schema:$ref:'#/components/schemas/SignupReq'responses:'201':description:注册成功content:application/json:schema:$ref:'#/components/schemas/MessageResp''400':description:参数错误或用户已存在/login:post:summary:用户登录requestBody:required:truecontent:application/json:schema:$ref:'#/components/schemas/LoginReq'responses:'200':description:登录成功,返回 access_tokencontent:application/json:schema:$ref:'#/components/schemas/LoginResp''401':description:凭证错误/me:get:summary:获取当前用户信息security:-bearerAuth:[]responses:'200':description:用户信息content:application/json:schema:$ref:'#/components/schemas/User''401':description:未认证或 token 无效put:summary:更新当前用户信息security:-bearerAuth:[]requestBody:required:truecontent:application/json:schema:$ref:'#/components/schemas/UserUpdateReq'responses:'200':description:更新成功,返回最新用户content:application/json:schema:$ref:'#/components/schemas/User'components:securitySchemes:bearerAuth:type:httpscheme:bearerbearerFormat:JWTschemas:SignupReq:type:objectrequired:-email-passwordproperties:email:type:stringformat:emailpassword:type:stringminLength:8LoginReq:type:objectrequired:-email-passwordproperties:email:type:stringformat:emailpassword:type:stringLoginResp:type:objectproperties:access_token:type:stringtoken_type:type:stringdefault:BearerMessageResp:type:objectproperties:message:type:stringUser:type:objectproperties:id:type:integeremail:type:stringformat:emailnickname:type:stringbio:type:stringavatar_url:type:stringUserUpdateReq:type:objectproperties:nickname:type:stringbio:type:stringavatar_url:type:string四、FastAPI MVP 实现
目录结构
mvp-user-auth/ ├─ README.md ├─ requirements.txt ├─ app.py ├─ db.py ├─ utils.py └─ schemas.pyrequirements.txt
fastapi uvicorn[standard] sqlalchemy passlib[bcrypt] pyjwt pydanticdb.py
fromsqlalchemyimportcreate_engine,MetaData,Table,Column,Integer,String,Textfromsqlalchemy.sqlimportselect DATABASE_URL="sqlite:///./users.db"engine=create_engine(DATABASE_URL,connect_args={"check_same_thread":False})metadata=MetaData()users=Table('users',metadata,Column('id',Integer,primary_key=True,autoincrement=True),Column('email',String(255),unique=True,nullable=False),Column('password_hash',String(255),nullable=False),Column('nickname',String(100),nullable=True),Column('bio',Text,nullable=True),Column('avatar_url',String(512),nullable=True),)metadata.create_all(engine)defget_user_by_email(conn,email):q=select(users).where(users.c.email==email)returnconn.execute(q).fetchone()defcreate_user(conn,email,password_hash):ins=users.insert().values(email=email,password_hash=password_hash)res=conn.execute(ins)returnres.inserted_primary_key[0]defget_user_by_id(conn,user_id):q=select(users).where(users.c.id==user_id)returnconn.execute(q).fetchone()defupdate_user(conn,user_id,**fields):upd=users.update().where(users.c.id==user_id).values(**fields)conn.execute(upd)returnget_user_by_id(conn,user_id)utils.py
frompasslib.contextimportCryptContextimportjwtimporttime PWD_CTX=CryptContext(schemes=["bcrypt"],deprecated="auto")SECRET="change_this_to_a_long_random_secret"# 生产环境务必更换为安全密钥ALGORITHM="HS256"ACCESS_TOKEN_EXPIRE_SECONDS=7*24*3600# 7 天defhash_password(password:str)->str:returnPWD_CTX.hash(password)defverify_password(plain:str,hashed:str)->bool:returnPWD_CTX.verify(plain,hashed)defcreate_access_token(subject:str):now=int(time.time())payload={"sub":subject,"iat":now,"exp":now+ACCESS_TOKEN_EXPIRE_SECONDS}returnjwt.encode(payload,SECRET,algorithm=ALGORITHM)defdecode_access_token(token:str):try:returnjwt.decode(token,SECRET,algorithms=[ALGORITHM])exceptjwt.ExpiredSignatureError:raiseexceptException:raiseschemas.py
frompydanticimportBaseModel,EmailStr,FieldfromtypingimportOptionalclassSignupReq(BaseModel):email:EmailStr password:str=Field(...,min_length=8)classLoginReq(BaseModel):email:EmailStr password:strclassLoginResp(BaseModel):access_token:strtoken_type:str="Bearer"classMessageResp(BaseModel):message:strclassUser(BaseModel):id:intemail:EmailStr nickname:Optional[str]=Nonebio:Optional[str]=Noneavatar_url:Optional[str]=NoneclassUserUpdateReq(BaseModel):nickname:Optional[str]=Nonebio:Optional[str]=Noneavatar_url:Optional[str]=Noneapp.py
fromfastapiimportFastAPI,HTTPException,Dependsfromfastapi.securityimportHTTPBearer,HTTPAuthorizationCredentialsfromsqlalchemyimportcreate_engineimportdbas_dbimportschemasas_schemasimportutilsas_utils app=FastAPI(title="User Auth MVP")engine=create_engine(_db.DATABASE_URL,connect_args={"check_same_thread":False})bearer_scheme=HTTPBearer()@app.post('/signup',status_code=201,response_model=_schemas.MessageResp)defsignup(req:_schemas.SignupReq):withengine.connect()asconn:if_db.get_user_by_email(conn,req.email):raiseHTTPException(status_code=400,detail="user exists")pw_hash=_utils.hash_password(req.password)_db.create_user(conn,req.email,pw_hash)return{"message":"created"}@app.post('/login',response_model=_schemas.LoginResp)deflogin(req:_schemas.LoginReq):withengine.connect()asconn:user=_db.get_user_by_email(conn,req.email)ifnotuserornot_utils.verify_password(req.password,user['password_hash']):raiseHTTPException(status_code=401,detail="invalid credentials")token=_utils.create_access_token(str(user['id']))return{"access_token":token}defget_current_user(creds:HTTPAuthorizationCredentials=Depends(bearer_scheme)):token=creds.credentialstry:payload=_utils.decode_access_token(token)except:raiseHTTPException(status_code=401,detail="invalid or expired token")user_id=int(payload["sub"])withengine.connect()asconn:user=_db.get_user_by_id(conn,user_id)ifnotuser:raiseHTTPException(status_code=401,detail="user not found")returnuser@app.get('/me',response_model=_schemas.User)defme(user=Depends(get_current_user)):return{k:user[k]forkinuser.keys()ifkin('id','email','nickname','bio','avatar_url')}@app.put('/me',response_model=_schemas.User)defupdate_me(req:_schemas.UserUpdateReq,user=Depends(get_current_user)):fields={k:vfork,vinreq.dict().items()ifvisnotNone}ifnotfields:returnme(user)withengine.connect()asconn:updated=_db.update_user(conn,user['id'],**fields)return{k:updated[k]forkinupdated.keys()ifkin('id','email','nickname','bio','avatar_url')}README.md
# User Auth MVP ## 快速启动 1. 创建并激活虚拟环境 ```bash python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate- 安装依赖
pipinstall-rrequirements.txt- 启动服务
uvicorn app:app--reload--port8000- 访问交互文档
http://localhost:8000/docs
--- ## 五、后续扩展建议(生产化路线) - 数据库迁移至 PostgreSQL/MySQL,使用 Alembic 管理迁移。 - 引入 Redis 实现 token 黑名单与接口限流。 - 使用密钥管理服务(KMS)存储 JWT secret,或改为非对称签名(RS256)。 - 增加邮箱验证、密码找回、头像上传(S3/Object Storage)等功能。 - 添加单元测试与集成测试覆盖核心流程。 --- 至此,一个完整、可运行的用户注册、登录、个人主页 MVP 已交付,可直接用于开发验证或作为后续功能的基础。