文章目录
- 前文:[vue3+vite5前端(一)](https://blog.csdn.net/weixin_44894663/article/details/140476800)
- 后文: [vue3+vite5前端(二)](https://blog.csdn.net/weixin_44894663/article/details/140547092)
- 1. 安装后端环境
- 2. 开发工具配置`python`
- 3. 使用`fastapi`
- 4.`Annotated`用法
- 5. `dataclasses`用法
- 6. `Depends`用法(`fastapi`)
- 7. 配置文件(`pydantic + env`文件)
- 8. 同步连接`mysql(sqlachemy + pymysql)`
- 9. 异步连接`mysql (sqlachemy + aiomysql)`
- 10. 抛自定义异常`HTTPException`
- 11. 自定义全局异常处理`app.exception_handler`
- 12. 中间件
- 13. `jsonable_encoder`(待整理)
- 14. `Pydantic`中的`BaseModel`
- 15. 跨域问题
- 16. 前端路由响应状态码为 `307 Temporary Redirect `然后直接访问后端接口
- 17. 自动生成模型类工具`sqlacodegen`
- 18. `msyql8.0`登录报错`ERROR 1045 (28000) Access denied for user ‘root‘@‘localhost‘ (using password YES/NO)`加上`skip-grant-tables`后无法启动
- 19. `jwt`验证
前文: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)
app | fastapi应用实例 |
---|---|
host | 运行的ip地址,默认127.0.0.1 |
port | 端口,默认8000 |
reload | true/false,文件改动自动重载载 |
log_level | 日志级别,debug,info,warning,error,critical |
workers | 指定启动的工作进程数量 |
access_log | true/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)
typing
中Optional
和Union
说明:
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 data
,Starlette
只支持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=["*"])
vue3
中vite.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
后无法启动
-
打开管理员窗口关闭msyql服务
net stop mysql
-
另起管理员窗口,执行
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.
- 新窗口执行
mysql -uroot -p
,直接回车,不需要输入密码 - 执行
UPDATE mysql.user SET authentication_string='' WHERE user='root' and host='localhost'
,修改密码为空 - 登录之后修改密码:
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