FastAPI + LangGraph 从零开发智能实验室预约系统

本集视频在 B 站 BV13hbP6zEUd · P7

去 B 站看本集

06. 开发后端 JWT 用户认证

本节课解决什么问题

text
POST /api/auth/login

验证用户名 + 密码

生成 JWT Token

返回 Token + 用户信息

前端保存 Token

请求其他接口时携带 Token

后端解析 Token

获取当前登录用户

配置 VsCode 的用户设置,支持自动导入

json
{
  "python.analysis.autoImportCompletions": true,
  "python.languageServer": "Pylance",
  "python.analysis.extraPaths": ["./backend"],
  "editor.quickSuggestions": {
    "other": true,
    "comments": false,
    "strings": false
  },
  "[python]": {
    "editor.codeActionsOnSave": {
      "source.organizeImports": "explicit"
    },
    "editor.defaultFormatter": "ms-python.black-formatter"
  },
}

requirements.txt 新增依赖

text
bcrypt==5.0.0
PyJWT==2.13.0

新增 jwt 工具方法

配置环境变量

jwt 密钥生成器:https://jwtsecretkeygenerator.com/zh/

text
JWT_SECRET_KEY=sjQ9crUw71awph8ORnvERoUtMQ19qelECFjj06i1urz
JWT_EXPIRE_HOURS=24
JWT_ALGORITHM="HS256"

config 新增配置

python
class Settings(BaseSettings):
    DATABASE_URL: str

    JWT_SECRET_KEY: str
    JWT_ALGORITHM: str = "HS256"
    JWT_EXPIRE_HOURS: int = 24

utils/jwt.py

python
from datetime import datetime, timedelta
import jwt
from app.config import settings

def create_access_token(user_id: int) -> str:
    """创建JWT token"""
    expire = datetime.now() + timedelta(hours=settings.JWT_EXPIRE_HOURS)

    payload = {"user_id": user_id, "exp": expire}  # exp是固定key,不能写成别的

    return jwt.encode(
        payload, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM
    )

def decode_access_token(token: str) -> dict:
    """解析jwt token"""
    return jwt.decode(
        token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]
    )

utils/passwod.py

python
import bcrypt

def hash_password(password: str) -> str:
    """对密码加密"""
    return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")

def verify_password(password: str, hashed_password: str) -> bool:
    """验证密码的有效性"""
    """  "123" -> "xxsdasdasdasfa"  """
    return bcrypt.checkpw(password.encode("utf-8"), hashed_password.encode("utf-8"))

在线密码 Bcrpyt 加密:https://www.bejson.com/encrypt/bcrpyt_encode/

最终版本的 login api

python
from fastapi import APIRouter, Depends
from app.schemas.auth import LoginRequest
from sqlalchemy.orm import Session
from app.database import get_db
from app.models.user import User
from app.schemas.user import UserResponse
from app.utils.password import verify_password
from app.utils.jwt import create_access_token

router = APIRouter(prefix="/auth", tags=["权限验证"])

@router.post("/login")
def login(data: LoginRequest, db: Session = Depends(get_db)):
    # 根据用户账号查询数据库
    user = db.query(User).filter(User.username == data.username).first()

    # 判断账号和密码是否正确
    if not user or not verify_password(data.password, user.password):
        return {"code": 400, "message": "账号或密码错误"}
    # 验证账号的状态
    if user.status != 1:
        return {"code": 403, "message": "账号已被禁用"}
    # 创建token
    token = create_access_token(user.id)
    # 返回信息
    return {
        "code": 200,
        "message": "操作成功",
        "data": {"token": token, "user": UserResponse.model_validate(user)},
    }

jwt 鉴权

dependencies/auth.py

GET /api/user/info

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

token: str=Depends(oauth2_scheme) 就会提取

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

↓ oauth2_scheme

↓ token = "eyJhbGciOiJIUzI1NiIs..."

python
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session

from app.database import get_db
from app.models.user import User
from app.utils.jwt import decode_access_token

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/autrh/login")

def get_current_user(
    token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)
) -> User:
    """JWT token 鉴权验证合法性"""
    try:
        payload = decode_access_token(token)
    except Exception:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="登录已失效,请重新登录"
        )
    # 获取到用户ID
    user_id = payload.get("user_id")
    if not user_id:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的登录凭证"
        )
    # 从数据库根据用户ID查询用户信息
    user = db.query(User).filter(User.id == user_id).first()
    if user.status != 1:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="用户被禁用"
        )
    return user

新增获取用户信息的 api

api/user.py

注意:此接口是验证 token 的示例接口,只要有 current_user: User = Depends(get_current_user)

表示都需要验证 token 合法才可以访问

python
from fastapi import APIRouter, Depends
from app.models.user import User
from app.dependencies.auth import get_current_user
from app.schemas.user import UserResponse

router = APIRouter(prefix="/user", tags=["用户信息接口"])

@router.get("/info")
def get_user_info(current_user: User = Depends(get_current_user)):
    """获取当前登录用户信息"""
    return {
        "code": 200,
        "message": "请求成功",
        "data": UserResponse.model_validate(current_user),
    }

Api Post 测试带鉴权的接口