FastAPI JWT token认证

847 阅读1分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。

如题。

项目目录结构:

app
├── config.py
├── dependencies.py
├── main.py
├── models
│   ├── __init__.py
│   ├── base.py
│   └── users.py
├── routers
│   ├── __init__.py
│   ├── mob.py
│   └── notify.py
├── schemas
│   ├── __init__.py
│   ├── records.py
│   └── robots.txt
├── tests
│   ├── __init__.py
│   ├── notify.json
│   └── test_money.py
└── utils.py

依赖(pyproject.toml)

[tool.poetry]
name = "tingcheyi"
version = "0.1.0"
description = ""
authors = [""]
 
[tool.poetry.dependencies]
python = "^3.8"
python-dotenv = "^0.17.0"
fastapi = "^0.63.0"
redis = "^3.5.3"
tortoise-orm = "^0.17.2"
uvicorn = "^0.13.4"
asyncpg = "^0.22.0"
httpx = "^0.17.1"
python-multipart = "^0.0.5"
PyJWT = "^2.1.0"
 
[tool.poetry.dev-dependencies]
pytest = "^6.2.3"
requests = "^2.25.1"
coverage = "^5.5"
 
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

JWT配置

# config.py
import os
 
from dotenv import load_dotenv
 
load_dotenv()
 
DEBUG = os.getenv("DEBUG", "").lower() in ("true", "y", "yes", "1")
PORT = 9920
DB_NAME = "tingcheyi"
 
# DB_URL = "sqlite://:memory:"
# DB_URL = "sqlite://db.sqlite"
DB_URL = f"postgres://postgres:postgres@127.0.0.1:5432/{DB_NAME}"
if _uri := os.getenv("TINGCHEYI_DB_URL"):
    DB_URL = _uri  # type:ignore
 
 
JWT_SECRET = os.getenv(
    "JWT_SECRET", "(1ja8h3a1^n+xx*d)*jv4jy4"
)
JWT_ALGO = os.getenv("JWT_ALGO", "HS256")

认证相关

# dependencies.py
import jwt
from config import JWT_ALGO, JWT_SECRET
from fastapi import HTTPException, Request
from fastapi.security import HTTPBearer
from pydantic import BaseModel, ValidationError
 
 
class AuthBearer(HTTPBearer):
    status_code = 401
 
    async def __call__(self, *args, **kwargs):
        try:
            return await super().__call__(*args, **kwargs)
        except HTTPException as e:
            # HTTPBearer解析token失败会抛403,手动改为401
            e.status_code = self.status_code
            raise e
 
 
auth_scheme = AuthBearer()
 
 
def decode_jwt(token: str) -> dict:
    try:
        payload = jwt.decode(token.split()[-1], JWT_SECRET, algorithms=[JWT_ALGO])
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="token已过期")
    except jwt.DecodeError:
        # print(f"{token = }; {JWT_SECRET = }")
        raise HTTPException(status_code=401, detail="token解析失败!")
    return payload
 
 
class ClientUser(BaseModel):
    id: int
    nickname: str
    role: str
 
 
async def get_client_user(request: Request):
    await auth_scheme(request)
    token = request.headers.get("authorization")
    try:
        request.client_user = ClientUser(**decode_jwt(token)["user"])  # type:ignore
    except (KeyError, ValidationError):
        raise HTTPException(status_code=401, detail="token内容异常,请确认是否为最新token!")


视图

# routers/mob.py
from typing import List, Optional
 
from dependencies import get_client_user, ClientUser
from fastapi import APIRouter, Depends, Query, Request
from models import Spot
 
router = APIRouter()
 
 
@router.get("/my-spots", dependencies=[Depends(get_client_user)])
async def my_spots_个人已关联Spots(
    request: Request,
    # openid: Optional[str] = Query(None, description="微信OpenID", max_length=120),
):
    """我的 -> 已关联Spots"""
    user: ClientUser = request.client_user
    spot_objs: List[Spot] = await Spot.filter(user_id=user.id)
    spots = [s.to_json() for s in spot_objs]
    return spots