使用 Python FastAPI 与 Tortoise ORM 实现账号密码登录和手机验证码登录
1. 项目概述与准备工作本文将详细介绍如何使用 Python 的 FastAPI 框架结合 Tortoise ORM 实现两种常见的用户登录方式传统的账号密码登录和现代化的手机验证码登录。我们将从环境搭建、数据库设计、核心逻辑实现到完整 API 测试一步步构建一个安全、可扩展的认证系统。1.1 技术栈说明FastAPI: 现代、高性能的 Python Web 框架用于构建 API。Tortoise ORM: 异步友好的 Python ORM支持 SQLite、PostgreSQL、MySQL 等数据库。Pydantic: 用于数据验证和设置管理。JWT (JSON Web Tokens): 用于生成和验证访问令牌。Redis(可选): 用于缓存验证码和会话信息提升性能。1.2 环境准备首先创建项目目录并安装必要的依赖mkdir fastapi-auth-demo cd fastapi-auth-demo python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install fastapi uvicorn tortoise-orm pydantic python-jose[cryptography] passlib[bcrypt] python-multipart redis2. 数据库模型设计使用 Tortoise ORM 定义用户表同时支持密码和手机号两种认证方式。2.1 用户模型 (models.py)from tortoise import fields, models from tortoise.contrib.pydantic import pydantic_model_creator class User(models.Model): 用户模型 id fields.IntField(pkTrue) username fields.CharField(max_length50, uniqueTrue, nullTrue) email fields.CharField(max_length100, uniqueTrue, nullTrue) phone fields.CharField(max_length20, uniqueTrue, nullTrue) # 密码相关字段 hashed_password fields.CharField(max_length255, nullTrue) 用户信息 full_name fields.CharField(max_length100, nullTrue) is_active fields.BooleanField(defaultTrue) created_at fields.DatetimeField(auto_now_addTrue) updated_at fields.DatetimeField(auto_nowTrue) class Meta: table users def str(self): return fUser(id{self.id}, username{self.username}) 创建 Pydantic 模型用于序列化/验证 User_Pydantic pydantic_model_creator(User, nameUser) UserIn_Pydantic pydantic_model_creator(User, nameUserIn, exclude_readonlyTrue)2.2 验证码记录模型class VerificationCode(models.Model): 手机验证码记录 id fields.IntField(pkTrue) phone fields.CharField(max_length20, indexTrue) code fields.CharField(max_length6) created_at fields.DatetimeField(auto_now_addTrue) expires_at fields.DatetimeField() is_used fields.BooleanField(defaultFalse) class Meta: table verification_codes def str(self): return fVerificationCode(phone{self.phone}, code{self.code})/code/pre 3. 核心配置与工具函数 3.1 配置文件 (config.py) from pydantic import BaseSettings class Settings(BaseSettings): 应用配置 app_name: str FastAPI Auth Demo debug: bool True JWT 配置 secret_key: str your-secret-key-change-in-production algorithm: str HS256 access_token_expire_minutes: int 30 数据库配置 database_url: str sqlite://db.sqlite3 Redis 配置 (用于缓存验证码) redis_url: str redis://localhost:6379/0 短信服务配置 (示例) sms_api_key: str your-sms-api-key class Config: env_file .env settings Settings() 3.2 密码哈希工具 (auth.py) from passlib.context import CryptContext 密码哈希上下文 pwd_context CryptContext(schemes[bcrypt], deprecatedauto) def verify_password(plain_password: str, hashed_password: str) - bool: 验证密码 return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) - str: 生成密码哈希 return pwd_context.hash(password) 3.3 JWT 工具函数 from datetime import datetime, timedelta from typing import Optional from jose import JWTError, jwt from config import settings def create_access_token(data: dict, expires_delta: Optional[timedelta] None): 创建 JWT 访问令牌 to_encode data.copy() if expires_delta: expire datetime.utcnow() expires_delta else: expire datetime.utcnow() timedelta(minutessettings.access_token_expire_minutes) to_encode.update({exp: expire}) encoded_jwt jwt.encode(to_encode, settings.secret_key, algorithmsettings.algorithm) return encoded_jwt def verify_token(token: str): 验证 JWT 令牌 try: payload jwt.decode(token, settings.secret_key, algorithms[settings.algorithm]) return payload except JWTError: return None 4. 账号密码登录实现 4.1 请求/响应模型 from pydantic import BaseModel, EmailStr from typing import Optional class PasswordLoginRequest(BaseModel): 密码登录请求 username: Optional[str] None email: Optional[EmailStr] None phone: Optional[str] None password: str class Config: schema_extra { example: { username: john_doe, password: securepassword123 } } class TokenResponse(BaseModel): 令牌响应 access_token: str token_type: str bearer user_id: int username: Optional[str] None 4.2 密码登录 API from fastapi import APIRouter, Depends, HTTPException, status from tortoise.exceptions import DoesNotExist from models import User from schemas import PasswordLoginRequest, TokenResponse from auth import verify_password, create_access_token router APIRouter(prefix/auth, tags[authentication]) router.post(/login/password, response_modelTokenResponse) async def password_login(login_data: PasswordLoginRequest): 账号密码登录 支持用户名、邮箱或手机号登录 user None 根据提供的标识查找用户 if login_data.username: try: user await User.get(usernamelogin_data.username) except DoesNotExist: pass elif login_data.email: try: user await User.get(emaillogin_data.email) except DoesNotExist: pass elif login_data.phone: try: user await User.get(phonelogin_data.phone) except DoesNotExist: pass 用户不存在或密码错误 if not user or not user.hashed_password: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail用户名或密码错误 ) 验证密码 if not verify_password(login_data.password, user.hashed_password): raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail用户名或密码错误 ) 检查用户是否激活 if not user.is_active: raise HTTPException( status_codestatus.HTTP_403_FORBIDDEN, detail用户账户已被禁用 ) 创建访问令牌 access_token create_access_token( data{sub: str(user.id), username: user.username} ) return TokenResponse( access_tokenaccess_token, user_iduser.id, usernameuser.username )/code/pre 5. 手机验证码登录实现 5.1 验证码发送与验证 import random import redis from datetime import datetime, timedelta from fastapi import HTTPException, status from models import VerificationCode from config import settings 初始化 Redis 客户端 redis_client redis.from_url(settings.redis_url, decode_responsesTrue) async def generate_verification_code(phone: str) - str: 生成并存储验证码 生成6位数字验证码 code str(random.randint(100000, 999999)) 存储到数据库 expires_at datetime.utcnow() timedelta(minutes5) await VerificationCode.create( phonephone, codecode, expires_atexpires_at ) 同时缓存到 Redis (可选提升性能) redis_key fverify_code:{phone} redis_client.setex(redis_key, 300, code) # 5分钟过期 这里应该调用短信服务发送验证码 await send_sms(phone, f您的验证码是{code}5分钟内有效) return code async def verify_phone_code(phone: str, code: str) - bool: 验证手机验证码 先检查 Redis 缓存 redis_key fverify_code:{phone} cached_code redis_client.get(redis_key) if cached_code and cached_code code: # 验证成功清除缓存 redis_client.delete(redis_key) # 标记数据库中的验证码为已使用 await VerificationCode.filter(phonephone, codecode, is_usedFalse).update(is_usedTrue) return True 如果缓存没有检查数据库 now datetime.utcnow() verification await VerificationCode.filter( phonephone, codecode, is_usedFalse, expires_at__gtnow ).first() if verification: # 验证成功 verification.is_used True await verification.save() # 清除可能的缓存 redis_client.delete(redis_key) return True return False/code/pre 5.2 手机登录 API from pydantic import BaseModel from fastapi import APIRouter, HTTPException, status class PhoneLoginRequest(BaseModel): 手机登录请求 phone: str code: str class SendCodeRequest(BaseModel): 发送验证码请求 phone: str router.post(/login/phone/send-code) async def send_verification_code(request: SendCodeRequest): 发送手机验证码 验证手机号格式 (这里简单示例实际需要更严格的验证) if not request.phone.startswith(1) or len(request.phone) ! 11: raise HTTPException( status_codestatus.HTTP_400_BAD_REQUEST, detail手机号格式不正确 ) 检查用户是否存在如果不存在则创建 user, created await User.get_or_create( phonerequest.phone, defaults{ username: fuser_{request.phone}, is_active: True } ) 生成并发送验证码 code await generate_verification_code(request.phone) 在实际项目中这里应该调用短信服务 为了演示我们返回验证码生产环境不要这样做 return { message: 验证码已发送, phone: request.phone, code: code, # 仅用于演示生产环境移除 user_created: created } router.post(/login/phone, response_modelTokenResponse) async def phone_login(login_data: PhoneLoginRequest): 手机验证码登录 验证验证码 if not await verify_phone_code(login_data.phone, login_data.code): raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail验证码错误或已过期 ) 获取或创建用户 user await User.get_or_none(phonelogin_data.phone) if not user: # 如果用户不存在创建新用户 user await User.create( phonelogin_data.phone, usernamefuser_{login_data.phone}, is_activeTrue ) 检查用户是否激活 if not user.is_active: raise HTTPException( status_codestatus.HTTP_403_FORBIDDEN, detail用户账户已被禁用 ) 创建访问令牌 access_token create_access_token( data{sub: str(user.id), phone: user.phone} ) return TokenResponse( access_tokenaccess_token, user_iduser.id, usernameuser.username )/code/pre 6. 用户注册与数据库初始化 6.1 用户注册 API from pydantic import BaseModel, EmailStr from fastapi import HTTPException, status from auth import get_password_hash class UserRegisterRequest(BaseModel): 用户注册请求 username: str email: EmailStr phone: str password: str full_name: Optional[str] None router.post(/register, status_codestatus.HTTP_201_CREATED) async def register_user(user_data: UserRegisterRequest): 用户注册 检查用户名是否已存在 if await User.exists(usernameuser_data.username): raise HTTPException( status_codestatus.HTTP_400_BAD_REQUEST, detail用户名已存在 ) 检查邮箱是否已存在 if await User.exists(emailuser_data.email): raise HTTPException( status_codestatus.HTTP_400_BAD_REQUEST, detail邮箱已存在 ) 检查手机号是否已存在 if await User.exists(phoneuser_data.phone): raise HTTPException( status_codestatus.HTTP_400_BAD_REQUEST, detail手机号已存在 ) 创建用户 hashed_password get_password_hash(user_data.password) user await User.create( usernameuser_data.username, emailuser_data.email, phoneuser_data.phone, hashed_passwordhashed_password, full_nameuser_data.full_name, is_activeTrue ) return { message: 用户注册成功, user_id: user.id, username: user.username }/code/pre 6.2 数据库初始化 from tortoise import Tortoise from config import settings async def init_db(): 初始化数据库 await Tortoise.init( db_urlsettings.database_url, modules{models: [models]} ) await Tortoise.generate_schemas() print(数据库初始化完成) async def close_db(): 关闭数据库连接 await Tortoise.close_connections() 7. 主应用与路由集成 7.1 主应用文件 (main.py) from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager from routers import auth_router from database import init_db, close_db asynccontextmanager async def lifespan(app: FastAPI): 应用生命周期管理 启动时初始化数据库 await init_db() yield 关闭时清理资源 await close_db() app FastAPI( titleFastAPI 认证系统, description使用 Tortoise ORM 实现账号密码和手机验证码登录, version1.0.0, lifespanlifespan ) 配置 CORS app.add_middleware( CORSMiddleware, allow_origins[], # 生产环境应限制具体域名 allow_credentialsTrue, allow_methods[], allow_headers[*], ) 注册路由 app.include_router(auth_router) app.get(/) async def root(): return {message: FastAPI 认证系统已启动} if name main: import uvicorn uvicorn.run(app, host0.0.0.0, port8000) 7.2 项目结构 以下是完整的项目目录结构 fastapi-auth-demo/ ├── main.py # 主应用入口 ├── config.py # 配置文件 ├── models.py # 数据库模型 ├── schemas.py # Pydantic 模型 ├── auth.py # 认证工具函数 ├── routers.py # API 路由 ├── database.py # 数据库初始化 ├── requirements.txt # 依赖文件 └── .env # 环境变量文件 项目结构说明 main.pyFastAPI 应用主入口包含应用配置、中间件和路由注册 config.py应用配置文件使用 Pydantic 管理环境变量 models.py数据库模型定义包含用户和验证码模型 schemas.pyPydantic 模型定义用于请求/响应数据验证 auth.py认证相关工具函数包括密码哈希和 JWT 处理 routers.pyAPI 路由定义包含登录、注册等接口 database.py数据库初始化和连接管理 requirements.txt项目依赖包列表 .env环境变量配置文件

相关新闻

C++ <functional>深度解析:从函数对象到现代函数式编程实践

C++ <functional>深度解析:从函数对象到现代函数式编程实践

1. 项目概述&#xff1a;为什么我们需要深入理解<functional>如果你写过一段时间的 C&#xff0c;尤其是接触过标准库算法&#xff08;比如std::sort,std::for_each&#xff09;或者多线程&#xff08;比如std::thread&#xff09;&#xff0c;那你大概率已经和<funct…

2026/10/14 2:01:21 阅读更多 →
近期量化工具推荐,AI检查要跟着核心问题走

近期量化工具推荐,AI检查要跟着核心问题走

工具推荐看起来像是在比较工具&#xff0c;实际更应该先比较问题。对于想把手工交易规则转成量化表达的读者来说&#xff0c;真正需要先回答的是&#xff1a;现在最需要被解决的卡点是什么。只有这个问题清楚了&#xff0c;工具才有选择依据。工具要跟着当前任务走如果读者只是…

2026/10/13 13:52:45 阅读更多 →
口碑好的氮化硅陶瓷供应商

口碑好的氮化硅陶瓷供应商

最近后台收到不少做新能源、半导体、高端装备的朋友提问&#xff1a;找氮化硅陶瓷供应商总踩雷&#xff0c;要么参数不达标用俩月就磨坏&#xff0c;要么非标定制拖三个月交不了货&#xff0c;到底哪些供应商口碑靠谱&#xff1f; 其实选氮化硅供应商不能只看报价&#xff0c;核…

2026/10/14 8:30:58 阅读更多 →

最新新闻

4 步完整指南:抖音视频无水印批量下载,主页合集一次存进本地

4 步完整指南:抖音视频无水印批量下载,主页合集一次存进本地

4 步完整指南&#xff1a;抖音视频无水印批量下载&#xff0c;主页合集一次存进本地 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and brows…

2026/10/16 5:46:42 阅读更多 →
Symphony-Coord 安全与部署指南:Risk Guard 风险守卫、API 密钥安全与边缘设备落地

Symphony-Coord 安全与部署指南:Risk Guard 风险守卫、API 密钥安全与边缘设备落地

【免费下载链接】symphony-coord 项目地址&#xff1a; https://gitcode.com/gh_mirrors/sy/symphony-coord 点击查看 免费下载 Symphony-Coord 是一个去中心化的多智能体协调框架&#xff0c;它把 Agent 选择转化为在线多臂老虎机&#xff08;Multi-Armed Bandit&#xff09;…

2026/10/16 5:46:42 阅读更多 →
FFmpeg AVPacket 深度解析:结构、生命周期与踩坑指南

FFmpeg AVPacket 深度解析:结构、生命周期与踩坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 5:46:42 阅读更多 →
两节锂电池主动均衡设计:MP2642GR-Z与PIC18F4525协同实现<15mV压差控制

两节锂电池主动均衡设计:MP2642GR-Z与PIC18F4525协同实现<15mV压差控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 5:46:42 阅读更多 →
VSCode搭配Keil MDK搭建高效嵌入式开发环境

VSCode搭配Keil MDK搭建高效嵌入式开发环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 5:46:42 阅读更多 →
金融信息服务系统开发:技术选型与合规实践指南

金融信息服务系统开发:技术选型与合规实践指南

我无法基于“financial-services”这一孤立标题生成符合要求的高质量博文。原因如下&#xff1a;输入信息严重缺失&#xff1a;您提供的内容中&#xff0c;“项目正文”为空&#xff0c;“关键词”未列出具体词项&#xff0c;“摘要描述”亦为空。整个输入仅包含一个宽泛的英文…

2026/10/16 5:45:42 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课&#xff0c;其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置&#xff0c;通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测&#xff0c;本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章&#xff0c;跟着敲完了投票应用&#xff0c;大概会有一种"我好像会了&#xff0c;又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型&#xff0c;下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介&#xff1a;基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码&#xff0c;面向计算机相关专业课程设计与期末大作业学生&#xff0c;以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程&#xff0c;…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程&#xff1a;键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化&#xff0c;十个新手有八个栽在"往输入框里填东西"这件事上&#xff1a;要么填不进去&#xff0c;要么填了一半&#xff0c;要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程&#xff1a;阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀&#xff1a;什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面&#xff0c;跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →