鉴权 / 闯关模式
FastAPI 登录认证和 JWT 鉴权
理解登录、签发 token、校验当前用户、保护接口、管理员权限和退出策略。
一句话:登录认证解决“你是谁”,权限鉴权解决“你能做什么”,JWT 是前端每次请求时带给后端看的临时通行证。
本篇学完你会什么:能理解注册、登录、签发 token、校验当前用户、管理员权限、退出登录这条完整门禁流程。
1. 登录鉴权解决什么问题
真实后端不能让所有人随便调用接口。
要回答四个问题:
谁在访问?
他登录了吗?
他是不是管理员?
他退出后 token 还能不能用?
2. 密码为什么不能明文保存
不要这样存:
password = "123456"应该存 hash:
hashed_password = "$2b$12$..."大白话:hash 像把密码做成不可逆指纹。后端能验证你输得对不对,但不应该知道你的原始密码。
常用依赖:
pip install passlib[bcrypt] python-jose3. JWT 是什么
JWT 是一段字符串,里面通常包含:
{
"sub": "1",
"exp": 1730000000
}| 字段 | 意思 |
|---|---|
sub | 用户 id |
exp | 过期时间 |
前端请求时带上:
Authorization: Bearer eyJxxx注意:JWT 不是加密保险箱。很多 JWT 只是“签名后可验证”,内容本身可能被解码看到,所以不要把密码、手机号、身份证号、API key 这类敏感信息放进 token。token 里通常只放用户 id、过期时间、角色等必要信息。
4. 登录流程
用户输入用户名密码
↓
后端查用户
↓
校验密码 hash
↓
生成 JWT
↓
前端保存 token
↓
以后请求都带 Authorization5. 数据表设计
最小用户表:
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(32), unique=True, index=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
is_active = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)注意:字段叫 hashed_password,不是 password。
6. 密码加密和校验
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)大白话:
- 注册时,把明文密码变成 hash 再存。
- 登录时,把用户输入和数据库 hash 做比较。
7. 创建和解析 token
from datetime import datetime, timedelta
from jose import JWTError, jwt
JWT_SECRET = "change-me"
JWT_ALGORITHM = "HS256"
def create_access_token(user_id: int) -> str:
payload = {
"sub": str(user_id),
"exp": datetime.utcnow() + timedelta(days=7),
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
def decode_access_token(token: str) -> dict:
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])生产环境里 JWT_SECRET 必须放 .env,不能写死。
8. get_current_user
get_current_user 是很多接口共用的门卫。
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
):
token = credentials.credentials
try:
payload = decode_access_token(token)
except JWTError:
raise HTTPException(status_code=401, detail="登录已失效")
user_id = int(payload["sub"])
user = await get_user_by_id(user_id)
if not user or not user.is_active:
raise HTTPException(status_code=401, detail="用户不可用")
return user接口使用:
@router.get("/me")
async def me(current_user = Depends(get_current_user)):
return current_user9. require_admin
管理员接口要再加一层权限判断。
async def require_admin(current_user = Depends(get_current_user)):
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="需要管理员权限")
return current_user使用:
@router.delete("/users/{user_id}")
async def delete_user(user_id: int, admin = Depends(require_admin)):
...10. 退出登录和 token 失效
JWT 的特点是:签发后,在过期前默认都有效。
如果要实现“退出后立刻失效”,常见做法:
- token 设置短过期时间
- refresh token 单独管理
- Redis 记录 token 黑名单
简单项目可以先做:
access token 过期时间短一些
前端退出时删除本地 token正式后台系统再接 Redis 黑名单。
11. 常见错误
| 问题 | 原因 | 解决 |
|---|---|---|
| 登录成功但访问接口 401 | 前端没带 Authorization | 检查请求头 |
| token 永不过期 | 没设置 exp | 创建 token 时加过期时间 |
| 所有人都能访问管理员接口 | 没加 require_admin | 管理接口加依赖 |
| 密码泄露风险 | 明文存密码 | 只存 hash |
| 改了 JWT_SECRET 后全失效 | 签名密钥变了 | 这是正常现象 |
12. 检查清单
[ ] 密码只存 hash
[ ] 登录失败不暴露过多细节
[ ] token 有过期时间
[ ] JWT_SECRET 来自环境变量
[ ] 需要登录的接口使用 get_current_user
[ ] 管理员接口使用 require_admin
[ ] 401 和 403 区分清楚
[ ] 退出登录策略明确总结表
| 名词 | 大白话 |
|---|---|
| 认证 | 确认你是谁 |
| 鉴权 | 判断你能做什么 |
| JWT | 临时通行证 |
| hash | 密码指纹 |
| Authorization | 放 token 的请求头 |
| get_current_user | 当前用户门卫 |
| require_admin | 管理员门卫 |
| 401 | 没登录 |
| 403 | 没权限 |
下一篇建议:大白话讲解——Redis 缓存和限流。