fastapi简单使用

前文:vue3+vite5前端(一)
后文: vue3+vite5前端(二)
1. 安装后端环境

查看python可用版本:https://www.python.org/downloads/

查看miniconda支持python版本:https://docs.anaconda.com/miniconda/miniconda-other-installer-links/

# miniconda安装python
conda create --name python311 python=3.11

# 更新conda版本
conda update -n base -c defaults conda

# 切换环境python3.11
conda activate python311

# 安装fastapi
conda install fastapi     (0.103)
conda install uvicorn     (0.20)

# 密码加密和jwt: 
pip install passlib
pip install bcrypt
pip install pyjwt
2. 开发工具配置python

vscode配置widows本地环境:Ctrl+Shift+P --> Python:Select Interpreter

pycharm配置:可参考miniconda安装Python虚拟环境

3. 使用fastapi

__main__.py定义启动文件

from fastapi import FastAPI

app = FastAPI


@app.get("/")
async def get_hello_world():
    return {"message": "hello world!!!"}

if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8080)
appfastapi应用实例
host运行的ip地址,默认127.0.0.1
port端口,默认8000
reloadtrue/false,文件改动自动重载载
log_level日志级别,debug,info,warning,error,critical
workers指定启动的工作进程数量
access_logtrue/false,记录访问日志
timeout_keep_alive定义空心链接在关闭前保持活动的秒数,默认5秒
proxy_headers是否信任X-Forwarded-*头部信息,设置为True会允许Uvicorn根据这些头信息正确解析客户端的真实IP地址等信息。
root_path指定根目录
limit_concurrency限制同时处理请求的最大数量,防止服务过载
limit_max_requests指定每个工作进程能处理的最大请求数量,达到后会重启

终端命令运行:uvicorn app文件名:app --reload 或者python -m main文件目录名称

4.Annotated用法

Annnotated是python标准库typing中的一种注解,用于为类型添加额外的元数据(即传递给类型检查器额外信息)

官方文档:https://docs.python.org/zh-tw/3/library/typing.html#typing.Annotated

语法:Annotated[<type>, <metadata1>, <metadata2>]

5. dataclasses用法

dataclasses自定义一种数据,可通过对象形式访问属性,并对属性添加注解。官方文档:https://docs.python.org/zh-cn/3.11/library/dataclasses.html

from dataclasses import dataclass, field
from typing import List

@dataclass
class User:
    name: str
    age: int
    email: str

@dataclass
class Team:
    city: str
    teammate: List[User] = field(default_factory=list)


user1 = User('1111', 444, '111111')
user2 = User('2222', 55, "22222")
team1 = Team('team111', [user1, user2])

# User(name='1111', age=444, email='111111')
# User(name='2222', age=55, email='22222')
for i in team1.teammate:
    print(i)

typingOptionalUnion说明:

from typing import Optional, Union

# 表示字段可选,可为None,str类型
Optinal[str]

# 表示字段在str,int中可选,也可为None
Union[str, int]
6. Depends用法(fastapi

Depends依赖注入是一种设计方法。用于降低组件间耦合度

from fastapi import Depends, FastAPI  # 2导入 Depends

app = FastAPI()


# 1创建依赖项
async def common_parameters(
        q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):  # 3声明依赖项
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):  # 3声明依赖项
    return commons
7. 配置文件(pydantic + env文件)

安装依赖:

conda install python-dotenv   (0.21)
pip install pydantic_settings   (2.3.4)

使用pydantic_settings定义相关配置的注解类:

from pydantic_settings import BaseSettings

class Configs(BaseSettings):
    db_host: str
    db_port: int
    db_user: str
    db_passwd: str
    db_name: str

    class Config:
        env_type = ".env"

.env具体配置项:名称需要和上面定义的类属性名称对应, 但不区分大小

db_host="127.0.0.1"
db_port= 3306
from dotenv import load_dotenv
from functools import lru_cache
from setting.config_type import Configs


@lru_cache()
def get_config():
    # 从.env文件导入配置项
    load_dotenv()
    return Configs()

项目中使用配置

from setting.config import get_config

config = get_config()

# config类型:<class 'setting.config_type.Configs'>
print(type(config.db_host), config.db_host) # 类型:<class 'str'> 值:127.0.0.1
8. 同步连接mysql(sqlachemy + pymysql)

安装依赖:

# 安装sqlachemy
conda install sqlclchemy  (2.0.30)
conda install pymysql     (1.1.1)

数据库相关配置:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session

# 此处和异步url不同
SQLALCHEMY_DATABASE_URL = "mysql+pymysql://{}:{}@{}:{}/{}".format(db_user, db_passwd, db_host, db_port, db_name)

engine = create_engine(SQLALCHEMY_DATABASE_URL, echo=True)
SessionLocal = sessionmaker(class_=AsyncSession,
                            autocommit=False,
                            autoflush=False,
                            bind=engine)
Base = declarative_base()

def get_db():
    db: Session = SessionLocal()
    try:
        yield db
    finally:
        db.close()

模型类(异步和同步一样):

import datetime

from setting.database import Base
from sqlalchemy import Column, Integer, String
from sqlalchemy import DateTime


class User(Base):
    __tablename__ = "users"
    # 2.创建模型属性/列
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    username = Column(String(255), unique=True, nullable=False)
    email = Column(String(64), nullable=True)
    password = Column(String(255), nullable=False)
    telephone = Column(String(11), nullable=True)
    avatar = Column(String(255), nullable=True)
    status = Column(Integer, default=0)
    creator = Column(String(255), nullable=True)
    desc = Column(String(500), nullable=True)
    age = Column(Integer, nullable=False)
    is_delete = Column(Integer, default=0)
    create_time = Column(DateTime, default=datetime.datetime.now)
    update_time = Column(DateTime, onupdate=datetime.datetime.now, default=datetime.datetime.now)

增删改查:

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from db.db_models import User
from db.session import get_db


router = APIRouter()

# 查
def getByUsername(db: Session, *, username: str):
	return db.query(User).filter(User.username == username).first()
	
@router.get("/get_uer_list")
async def login(username: str, db: Session = Depends(get_db)):
    data = user_crud.getByUsername(db=db, username=username)
    return {'message': '登录成功', 'data': data}

9. 异步连接mysql (sqlachemy + aiomysql)

官方文档:https://docs.sqlalchemy.org/en/20/orm/queryguide/dml.html#orm-queryguide-upsert

安装依赖:

conda install sqlclchemy   (2.0.30)
conda install aiomysql   (0.2.0)
conda install pymysql     (1.1.1)

数据库相关配置:

sqlachemy 官方文档:https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase

SQLALCHEMY_DATABASE_URL = "mysql+aiomysql://{}:{}@{}:{}/{}".format(db_user,
                                                                   db_passwd,
                                                                   db_host,
                                                                   db_port,
                                                                   db_name)
engine = create_async_engine(SQLALCHEMY_DATABASE_URL, echo=True)
SessionLocal = async_sessionmaker(class_=AsyncSession,
                                  autocommit=False,
                                  autoflush=False,
                                  bind=engine)

# 此种方式Base创建模型类支持继承
# from sqlalchemy.ext.declarative import declarative_base
# Base = declarative_base()

# 此种方式Base创建模型类支持继承,可定义模型类中的公共属性,其他模型类继承Base
class Base(DeclarativeBase):
    is_delete = Column(Integer, default=0)
    create_time = Column(DateTime, default=datetime.datetime.now)
    update_time = Column(DateTime, onupdate=datetime.datetime.now, default=datetime.datetime.now)


async def get_db() -> AsyncSession:
    async with SessionLocal() as session:
        yield session

模型类(异步和同步一样),省略。

增删改查:

from sqlalchemy import select, insert, text, update,delete
from sqlalchemy.ext.asyncio import AsyncSession
from utils.crypto import set_password

from .models import User
from .vo import AddUser, UpdateUser


async def get_users_list(search_parm: dict, db: AsyncSession):
    sql = select(User)
    if search_parm.get("username"):
        sql = sql.filter(User.username==search_parm["username"]).offset(search_parm["skip"]).limit(search_parm["limit"])
    # 使用sql语句查询
    total = await db.execute(text("select count(id) from users"))
    # 使用函数func查询,提供max,min等函数
    # total = await db.execute(select(func.count()).select_from(User))
    try:
        res = await db.execute(sql)
    finally:
        await db.close()
    return res.scalars().all(), total


async def add_user(db: AsyncSession, user: AddUser):
    # try:
    #     newUser = User(**user.dict(), password=set_password())
    #     user = db.add(newUser)
    #     await db.flush()
    #     获取插入值的id
    #	  print(newUser.id)
    #     await db.commit()
    #     await db.flush(user)
    # except Exception as e:
    #     await db.rollback()
    try:
        sql = insert(User).values(password=set_password())
        newUser = await db.execute(sql, user.dict())
        await db.flush()
        # 获取插入值的id
        print(newUser.lastrowid)
        await db.commit()
    except Exception as e:
        await db.rollback()

    return ""


async def delete_user(db: AsyncSession, username: str):
    try:
        sql = delete(User).filter_by(username=username)
        await db.execute(sql)
        await db.commit()
    except Exception as e:
        await db.rollback()
    return ""


async def update_user(db: AsyncSession, username: str, item: UpdateUser):
    try:
        sql = update(User).filter_by(id=id).values(item.dict())
        await db.execute(sql)
        await db.commit()
    except Exception as e:
        await db.rollback()
    return ""


# 定义响应统一结构体
def common_json_resp_success(data: Union[list, dict, str, None]):
    return {
        "code": 200,
        "msg": "success",
        "data": data
    }


@router.get("/")
async def get_user_list(db: AsyncSession = Depends(get_db)):
    raise HTTPException(400)
    result = await UserApiHandler().get_user_list(db)
    return common_json_resp_success(result)


@router.post("/")
async def add_user(user: AddUser, db: AsyncSession = Depends(get_db)):
    result = await UserApiHandler().add_user(db, user)
    return common_json_resp_success(result)


@router.delete("/")
async def delete_user(username: str, db: AsyncSession = Depends(get_db)):
    result = await UserApiHandler().delete_user(db, username)
    return common_json_resp_success(result)

@router.put("/")
async def update_user(username: str, item: UpdateUser, db: AsyncSession = Depends(get_db)):
    result = await UserApiHandler().update_user(db, username, item)
    return common_json_resp_success(result)

10. 抛自定义异常HTTPException

只能raise

from fastapi import HTTPException

# status_code: int,  状态码
# detail: Any = None, 响应体
# headers: Optional[Dict[str, str]] = None, 设置响应头
raise HTTPException(
            status_code=404,
            detail="Item not found",
            headers={"X-Error": "There goes my error"},
        )

响应:

// 响应体
{
    "detail": "Item not found"
}

fastapi HTTPException 继承自Starlette's HTTPException,但是fastapi 支持any JSON-able dataStarlette 只支持str

自定义异常:

from fastapi import HTTPException

class test_exception(HTTPException):

    def __init__(self):
        self.status_code = 400
        self.detail = "400, test raise exception"
        self.headers = {"Auth": "invalid"}
11. 自定义全局异常处理app.exception_handler

官方文档:https://fastapi.tiangolo.com/tutorial/handling-errors/?h=requestvalidationerror#use-the-requestvalidationerror-body

方式1: 装饰器方式(以捕获RequestValidationError修改异常返回结果为例),此方式需要和FastAPI()示例对象在一个文件中,在其他地方导入无效(本人暂时发现)

RequestValidationError用于fastapi中定义Pydantic model数据校验异常

from fastapi import FastAPI

app = FastAPI()


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    errors = []
    for error in exc.errors():
        obj = {
            "type": error["type"],
            "loc": error["loc"],
            "msg": error["msg"]
        }
        errors.append(obj)

    return JSONResponse(
        status_code=status.HTTP_400_BAD_REQUEST,
        content={
            'code': status.HTTP_400_BAD_REQUEST,
            'msg': 'bad request',
            'data': errors
        }
    )

RequestValidationError继承自 Pydantic's [ValidationError],异常信息示例:

{
    "detail": [
        {
            "type": "missing",
            "loc": [
                "body",
                "username"
            ],
            "msg": "Field required",
            "input": {
                "email": "string",
                "password": "123456",
                "telephone": "string",
                "avatar": "string",
                "status": 0,
                "desc": "string",
                "age": 0,
                "gender": 0
            },
            "url": "https://errors.pydantic.dev/2.8/v/missing"
        }
    ]
}

exception_handler原码:

    def exception_handler(
        self, exc_class_or_status_code: Union[int, Type[Exception]]
    ) -> Callable[[DecoratedCallable], DecoratedCallable]:
        def decorator(func: DecoratedCallable) -> DecoratedCallable:
            self.add_exception_handler(exc_class_or_status_code, func)
            return func

        return decorator

自定义异常处理函数返回的结果:

{
    "code": 400,
    "msg": "bad request",
    "data": [
        {
            "type": "missing",
            "loc": [
                "body",
                "username"
            ],
            "msg": "Field required"
        }
    ]
}

方式2:使用FastAPI exception_handlers参数

exception_handlers参数类型:

        exception_handlers: Optional[
            Dict[
                Union[int, Type[Exception]],
                Callable[[Request, Any], Coroutine[Any, Any, Response]],
            ]
        ] = None,

示例:

from fastapi import Request
from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse

# 自定义异常
class TestException(HTTPException):

    def __init__(self, detail):
        self.status_code = 400
        self.detail = detail or "400, test raise exception"
        self.headers = {"Auth": "invalid"}

# 自定义异常捕获处理函数
async def test_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=400,
        content="test exption handler"
    )


exception_handlers = {
    # key可以是状态码,也可以是异常类名, value为自定义处理类
    400: test_exception_handler
}

from fastapi import FastAPI
app = FastAPI(exception_handlers=exception_handlers)

抛异常: TestException

raise TestException('user list test exception')

响应:

"test exption handler"

方式3:通过FastAPI 示例对象的方法add_exception_handler

add_exception_handler原码:

    def add_exception_handler(
        self,
        exc_class_or_status_code: typing.Union[int, typing.Type[Exception]],
        handler: typing.Callable,
    ) -> None:  # pragma: no cover
        self.exception_handlers[exc_class_or_status_code] = handler

示例:

from fastapi import Request
from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse

# 自定义异常
class TestException(HTTPException):

    def __init__(self, detail):
        self.status_code = 400
        self.detail = detail or "400, test raise exception"
        self.headers = {"Auth": "invalid"}

# 自定义异常捕获处理函数
async def test_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=400,
        content="test exption handler"
    )


from fastapi import FastAPI
app = FastAPI(exception_handlers=exception_handlers)
# 状态码或者异常类名,异常处理函数
app.add_exception_handler(400, test_exception_handler)
12. 中间件

中间件是一种函数,在每个请求被特定的路径处理之前,以及每个响应返回之前

方式1: 装饰器@app.middleware("http")

官方文档:https://fastapi.tiangolo.com/zh/advanced/middleware/

import time

from fastapi import FastAPI, Request

app = FastAPI()


@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    # 请求前
    resp = await call_next(request)
    # 请求后
    process_time = time.time() - start_time
    resp.headers["X-Process-Time"] = str(process_time)
    return resp


app.middleware("http")装饰器内部调用的也是方式2

    def middleware(
        self, middleware_type: str
    ) -> Callable[[DecoratedCallable], DecoratedCallable]:
        def decorator(func: DecoratedCallable) -> DecoratedCallable:
            self.add_middleware(BaseHTTPMiddleware, dispatch=func)
            return func

        return decorator

方式2:FastAPI示例对象的方法add_middleware

add_middleware源码:

    def add_middleware(self, middleware_class: type, **options: typing.Any) -> None:
        if self.middleware_stack is not None:  # pragma: no cover
            raise RuntimeError("Cannot add middleware after an application has started")
        self.user_middleware.insert(0, Middleware(middleware_class, **options))

self.user_middleware是一个列表,源码看作用:

def build_middleware_stack(self) -> ASGIApp:        
    
    	middleware = (
            [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
            + self.user_middleware
            + [
                Middleware(
                    ExceptionMiddleware, handlers=exception_handlers, debug=debug
                )
            ]
        )

        app = self.router
        for cls, options in reversed(middleware):
            app = cls(app=app, **options)

每个自定义的中间件类继承自BaseHTTPMiddleware,所以每个自定义中间件必须实现dispatch方法,初始化方法app属性由上面add_middleware方法完成,只需定义函数需要的属性就行

class BaseHTTPMiddleware:
    def __init__(
        self, app: ASGIApp, dispatch: typing.Optional[DispatchFunction] = None
    ) -> None:
        self.app = app
        self.dispatch_func = self.dispatch if dispatch is None else dispatch
	
    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ) -> Response:
        raise NotImplementedError()  # pragma: no cover

示例:

import time

from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint

app = FastAPI()


class TestMiddleware(BaseHTTPMiddleware):

    def __init__(self, app, get_request_method=None):
        super().__init__(app)
        self.request_method = get_request_method

    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ) -> Response:
        request_method = self.request_method if self.request_method else request.method
        print("before request!!!!!, {request_method}", request_method)
        resp = await call_next(request)
        print("before request!!!!!, {request_method}", request_method)

        return resp


app.add_middleware(TestMiddleware, get_request_method="888888")
13. jsonable_encoder(待整理)

作用:复杂的数据类型(特别是那些基于 Pydantic 的模型或包含特殊 Python 类型的对象)转换为与 JSON 兼容的数据结构

# 参数与行为:
  obj: 要转换的对象,可以是任意类型,但通常为 Pydantic 模型实例或其他复杂数据结构。
  include: 可选参数,用于传递给 Pydantic 模型的 include 参数,控制哪些属性应当被包含在序列化结果中。
  by_alias: 可选参数,如果为 True,序列化时会使用模型定义中的别名(alias)而非原始字段名。
  exclude_unset: 可选参数,如果为 True,未赋值的模型属性将不会出现在序列化结果中。
  exclude_defaults: 可选参数,如果为 True,默认值未被覆盖的属性将不会出现在序列化结果中。
  encoder: 可选参数,自定义 JSON 编码器,用于处理特定类型。
  converters: 可选参数,自定义转换器字典,用于处理特定类型到 JSON 兼容类型的转换。

示例:

from fastapi.encoders import jsonable_encoder
from myapp.models import User

user = User(name="John Doe", email="john.doe@example.com", joined_at=datetime.now())

# 将 User 模型实例转换为 JSON 兼容的字典
json_compatible_data = jsonable_encoder(user)

# 现在可以将 json_compatible_data 直接用于 JSON 序列化或存储到数据库等操作
14. Pydantic中的BaseModel
from typing import Self

from pydantic import BaseModel, Field, validator, field_validator, ValidationInfo, model_validator


class Student(BaseModel):
    name: str = Field(..., min_length=3, max_length=5)
    age: int = Field(..., gt=0, le=130)


    # * 表示验证所有字段
    @field_validator("name", "age")
    @classmethod
    def validate_name(cls, v: str, info: ValidationInfo) -> str:
        # v 验证的字段值
        # ValidationInfo.data获取其他验证字段的值
        return v

    # mode表示设置验证前后获取的数据不同
    # mode='before'数据类型是dict[str, Any], 参数: 类方法cls,输入值,ValidationInfo
    # mode='after'验证失败不会调用。参数: 实例方法self
    @model_validator(mode='after')
    def check_passwords_match(self) -> Self:
        pw1 = self.password1
        pw2 = self.password2
        if pw1 is not None and pw2 is not None and pw1 != pw2:
            raise ValueError('passwords do not match')
        return self
15. 跨域问题

报错:Access to XMLHttpRequest at 'http://127.0.0.1:8080/user/' (redirected from 'http://127.0.0.1:3000/api/user') from origin 'http://127.0.0.1:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

解决办法:fastapi 添加中间件CORSMiddleware

from fastapi.middleware.cors import CORSMiddleware


app.add_middleware(CORSMiddleware,
                   allow_origins=["*"],  # 允许所有源,也可以指定具体的源
                   allow_credentials=True,
                   allow_methods=["*"],  # 允许所有方法
                   allow_headers=["*"])

vue3vite.config.ts配置允许跨域:

export default defineConfig((configEnv) => {
  const env = loadEnv(configEnv.mode, process.cwd())
  return {
    # 跨域
    server:{
      host: env.VITE_API_HOST || '0.0.0.0',
      # disableHostCheck: true,
      open: false,
      port: env.VITE_PORT || 8000,
      strictPort: true,
      proxy: {
        '/api': {
          target: env.VITE_API_TARGET_URL,
		  # 允许跨域
          changeOrigin: true,
		  # 将'/api'替换为'',后端接口不需要'/api'			
          rewrite: (path) => path.replace(/^\/api/, '')
        }
      }
    }
  }
})

16. 前端路由响应状态码为 307 Temporary Redirect 然后直接访问后端接口

现象描述:前端跨域访问后端,页面请求前端路由状态码为 307 Temporary Redirect ,然后重定向前端配置到后端的跨域接口200 OK 。意思就是抓包看到2次请求

排查步骤:确认前端设置没有问题,发现是后端fastapi设置路由有问题

router = APIRouter(prefix='/user', tags=['用户'])

# 这样设置前端先访问前端服务http://localhost:3000/api/user(307 Temporary Redirect ),再访问后端http://127.0.0.1:8080/user/
@router.get("/")
async def get_user_list(db) ...

# 解决办法,删除/,改为router.get(""),前端不会再直接访问http://127.0.0.1:8080/user/
@router.get("")
async def get_user_list(db) ...
17. 自动生成模型类工具sqlacodegen
# 3.0.0rc4不支持python 3.11.9和sqlalchemy 2.0
pip install sqlacodegen
18. msyql8.0登录报错ERROR 1045 (28000) Access denied for user ‘root‘@‘localhost‘ (using password YES/NO)加上skip-grant-tables后无法启动
  1. 打开管理员窗口关闭msyql服务net stop mysql

  2. 另起管理员窗口,执行mysqld --console --skip-grant-tables --shared-memory,不关闭窗口
    提示:

2024-07-25T14:22:53.057681Z 0 [System] [MY-010116] [Server] D:\mysql-8.0.21-winx64\bin\mysqld.exe (mysqld 8.0.21) starting as process 2652
2024-07-25T14:22:53.059823Z 0 [Warning] [MY-013242] [Server] --character-set-server: 'utf8' is currently an alias for the character set UTF8MB3, but will be an alias for UTF8MB4 in a future release. Please consider using UTF8MB4 in order to be unambiguous.
2024-07-25T14:22:53.075479Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2024-07-25T14:22:53.598567Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
2024-07-25T14:22:53.962751Z 0 [Warning] [MY-011311] [Server] Plugin mysqlx reported: 'All I/O interfaces are disabled, X Protocol won't be accessible'
2024-07-25T14:22:54.041283Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
2024-07-25T14:22:54.042396Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
  1. 新窗口执行mysql -uroot -p,直接回车,不需要输入密码
  2. 执行UPDATE mysql.user SET authentication_string='' WHERE user='root' and host='localhost',修改密码为空
  3. 登录之后修改密码:alter user 'root'@'localhost' identified with mysql_native_password by '新密码'
19. jwt验证

jwt: JSON Web Tokens
安装pyjwt: conda install pyjwt(会自动校验token过期时间)
示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTcyMjA3Njg1NX0.DWNtuuKFCsDOpT2a802DNMVHM6p-VovUdbv7ar3Ppw0

分三部分:

组件作用
Header(头部)描述 JWT 的元数据,如 {"alg": "HS256", "typ": "JWT"}
alg :生成签名算法,默认为 HS256 typ:令牌类型,默认为 JWT
Payload(负载)存放实际数据,官方字段如下
exp (Expiration Time):过期时间
nbf (Not Before Time):生效时间
iss (Issuer):签发人
aud (Audience):受众
iat (Issued At):签发时间
sub (Subject):主题
jti (JWT ID):编号
Signature(签名)HS256(自定义的key,base64后的header + b’.‘ + base64后的payload,digestmod=‘SHA256’)

示例代码:(支持接口依赖注入和中间件方式)

from typing import Annotated
from datetime import timedelta, datetime, timezone

from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
import jwt
from starlette import status
from jwt import InvalidTokenError

from .models import User
# 基于pydanic的BaseModel自定义的数据类型
from .vo import LoginUser, TokenData

app = FastAPI()

# 中间件方式
@app.middleware("http")
async def check_jwt(request: Request, call_next):
    # 设置白名单
    white_list = ["/user/login", "/docs", "/openapi.json"]
    if request.url.path in white_list:
        res = await call_next(request)
        return res

    # 获取数据库连接对象
    db = await get_db().__anext__()
    cur_user = await get_current_user_v2(request, db)
    # 将用户id和username保存到请求上下文
    request.state.user_id = cur_user.id
    request.state.username = cur_user.username
    # 请求前----------------------------
    resp = await call_next(request)
    # 请求后----------------------------
    print("6666666565555555555666666")
    return resp


# 仅使用jwt可不需要这个,可直接操作request对象获取请求头
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")
SECRET_KEY = "123456"
ALGORITHM = "HS256"


def create_access_token(data: dict, expires_delta: timedelta | None = None):
    """
    生成jwt
    :param data: header和playload
    :param expires_delta:  token过期时间
    :return: 
    """
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

async def get_db() -> AsyncSession:
    # 获取数据库连接对象
    async with SessionLocal() as session:
        yield session

async def get_users_by_username(username: str, db: AsyncSession):
    res = await db.execute(select(User).filter_by(username=username, status=0, is_delete=0))
    return res.scalars().first()

async def get_current_user_v1(token: Annotated[str, Depends(oauth2_scheme)], db: AsyncSession=Depends(get_db)):
    """
    获取当期登录用户
    场景: 接口中使用依赖注入
    :param token: 请求头中的token, oauth2_scheme方法已分离出token部分
    :param db: 数据库连接session
    :return:
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except InvalidTokenError:
        raise credentials_exception
    user = await get_users_by_username(username=token_data.username, db=db)
    if user is None:
        raise credentials_exception
    return user


async def get_current_user_v2(request: Request, db: AsyncSession):
    """
    获取当期登录用户
    场景: 中间件中使用
    :param token: 直接从request对象获取Authorization,未分离出token
    :param db:
    :return:
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    # 通过request对象获取请求头中的Authorization
    authorization: str = request.headers.get("Authorization", "")
    if not authorization:
        raise credentials_exception
    try:
        token = authorization.replace("Bearer ", "")
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except InvalidTokenError:
        raise credentials_exception
    user = await get_users_by_username(username=token_data.username, db=db)
    if user is None:
        raise credentials_exception
    return user


async def check_user_password(user: LoginUser, db: AsyncSession):
    is_auth = False
    res = await db.execute(select(User).filter_by(username=user.username))
    obj = res.scalars().first()
    is_auth = check_password(user.password, obj.password)
    return is_auth


async def check_user(self, user: LoginUser, db: AsyncSession):
    """
    校验用户密码,生成jwt
    :param user: 
    :param db: 
    :return: 
    """
    is_auth = await check_user_password(user, db)
    if not is_auth:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )

     # 生成jwt token
     access_token = create_access_token(
         data={"sub": user.username}
     )
     return {"is_auth": is_auth, "token": access_token, "token_type":"Bearer"}


@router.post("/login")
async def login(user: LoginUser, db: AsyncSession = Depends(get_db)):
    res = await check_user(user, db)
    return res


# 接口形式,使用依赖注入方式
@app.get("/user/{id}")
async def get_users_detail(id: int, cur_user: Annotated[User, Depends(get_current_user_v1)], db: AsyncSession = Depends(get_db)):
    # 若不需要使用仅鉴权可使用路由中的dependencies
    print(cur_user)
    res = await UserApiHandler().get_users_detail(id, db)
    return res

@app.get("", dependencies=[Depends(get_current_user_v1)])
async def get_users_list(search_parm: dict=Depends(common_search_params), db: AsyncSession = Depends(get_db)):
    res, total = await UserApiHandler().get_users_list(search_parm, db)
    return common_json_resp_success(res, total)

请求头实例:Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTcyMjA4MDc1N30.FD0enTgHfWR4KwduVpNtZz5OvUiD6PQCUU5IkWIMcpc

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值