鉴权 / 闯关模式

FastAPI 登录认证和 JWT 鉴权

理解登录、签发 token、校验当前用户、保护接口、管理员权限和退出策略。

一句话:登录认证解决“你是谁”,权限鉴权解决“你能做什么”,JWT 是前端每次请求时带给后端看的临时通行证。

本篇学完你会什么:能理解注册、登录、签发 token、校验当前用户、管理员权限、退出登录这条完整门禁流程。

1. 登录鉴权解决什么问题

真实后端不能让所有人随便调用接口。

要回答四个问题:

谁在访问?
他登录了吗?
他是不是管理员?
他退出后 token 还能不能用?

JWT 登录鉴权流程

2. 密码为什么不能明文保存

不要这样存:

password = "123456"

应该存 hash:

hashed_password = "$2b$12$..."

大白话:hash 像把密码做成不可逆指纹。后端能验证你输得对不对,但不应该知道你的原始密码。

常用依赖:

pip install passlib[bcrypt] python-jose

3. JWT 是什么

JWT 是一段字符串,里面通常包含:

{
  "sub": "1",
  "exp": 1730000000
}
字段意思
sub用户 id
exp过期时间

前端请求时带上:

Authorization: Bearer eyJxxx

注意:JWT 不是加密保险箱。很多 JWT 只是“签名后可验证”,内容本身可能被解码看到,所以不要把密码、手机号、身份证号、API key 这类敏感信息放进 token。token 里通常只放用户 id、过期时间、角色等必要信息。

4. 登录流程

用户输入用户名密码
  ↓
后端查用户
  ↓
校验密码 hash
  ↓
生成 JWT
  ↓
前端保存 token
  ↓
以后请求都带 Authorization

5. 数据表设计

最小用户表:

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_user

9. 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
[ ] 401403 区分清楚
[ ] 退出登录策略明确

总结表

名词大白话
认证确认你是谁
鉴权判断你能做什么
JWT临时通行证
hash密码指纹
Authorization放 token 的请求头
get_current_user当前用户门卫
require_admin管理员门卫
401没登录
403没权限

下一篇建议:大白话讲解——Redis 缓存和限流