文章目录
- 一、Starlette、Pydanatic与FastAPI的关系
- 1.Starlette
- 1.1ASGI框架
- 2.Pydantic
- 2.1基础教程
- 1.定义接受实体
- 2.请求参数
- 3.可以安装插件辅助数据定义和矫正
- 4.处理校验失败的例子
- 5.模型的属性和方法
- 6.解析文件
- 7.递归模型
- 8.ORM模型:从类实例创建符合ORM对象的模型
- 二、请求参数和验证
- 1.第一个FastAPI
- 1.1 创建app
- 命令行启动
- main启动
- 1.2 创建实体
- 1.4 创建api
- 2.默认集成swagger-ui
- 3.路由
- 3.1路由定义
- 3.2路由实现
- 3.路径参数和数据的解析、验证
- 3.1使用枚举类型
- 3.2传递文件路径
- 3.3校验路径参数
- 3.4.查询参数和数据的解析、验证
- 3.5.请求体和混合参数的使用
- 3.6.混合参数的使用
- 3.7.数据格式嵌套的请求体
- 3.8.Cookie 和 Header 参数
- 4. 响应处理和FastAPI配置
- 1.处理返回响应实体
- 2.响应状态码
- 3.Form Data 表单数据处理
- 4.Request Files 单文件、多文件上传及参数详解
- 5.FASTAPI项目的静态文件配置
- 6.url路径操作配置
- 7.swagger-ui界面配置
- 8.Handling Errors 错误处理
- 5.依赖注入系统
- 创建、导入、和声明依赖
- 函数作为依赖
- 类作为依赖
- 子依赖
- 路径操作装饰器中的多依赖
- 全局依赖(token-head验证适用)
- 带yield的依赖
- 6.安全、认证和授权
- 1.OAuth2验证
- 第一步:声明和获取token
- 第二步:模拟数据库
- 第三步:写服务于校验
- 第四步:写接口
- 2.Bearer with JWT tokens 开发基于JSON Web Tokens的认证
一、Starlette、Pydanatic与FastAPI的关系
1.Starlette
就是处理前端请求的json工具包
Starlette 是一种轻量级的ASGI框架/工具包,是构建高性能Asyncio服务的理想选择
1.1ASGI框架
一种规范,和WSGI
2.Pydantic
Pydanatic是一个基于Python 类型提示来定义数据验证,序列化和文档(使用JSON 模式)库
2.1基础教程
定义给一个类,用于接受前端请求
1.定义接受实体
from pydantic import BaseModel, ValidationError
class User(BaseModel):
id: int # 必须字段
name: str = "John Snow" # 有默认值,选填字段
signup_ts: Optional[datetime] = None
friends: List[int] = [] # 列表中元素是int类型或者可以直接转换成int类型
2.请求参数
external_data = {
"id": "123",
"signup_ts": "2020-12-22 12:22",
"friends": [1, 2, "3"], # "3"是可以int("3")的
}
user = User(**external_data)
print(user.id, user.friends) # 实例化后调用属性
print(repr(user.signup_ts))
print(user.dict())
3.可以安装插件辅助数据定义和矫正
4.处理校验失败的例子
try:
User(id=1, signup_ts=datetime.today(), friends=[1, 2, "not number"])
except ValidationError as e:
print(e.json())
》》》》结果返回
loc:报错的位置
msg:原因
type:类型
[
{
"loc": [
"friends",
2
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
5.模型的属性和方法
print(user.dict())# 转化成字典
print(user.json())# 转化成json
print(user.copy()) # 这里是浅拷贝
print(User.parse_obj(external_data))# 使用类直接解析external_data数据
print(User.parse_raw('{"id": "123", "signup_ts": "2020-12-22 12:22", "friends": [1, 2, "3"]}'))#使用类直接解析原生数据
》》》
{'id': 123, 'name': 'John Snow', 'signup_ts': datetime.datetime(2020, 12, 22, 12, 22), 'friends': [1, 2, 3]}
{"id": 123, "name": "John Snow", "signup_ts": "2020-12-22T12:22:00", "friends": [1, 2, 3]}
id=123 name='John Snow' signup_ts=datetime.datetime(2020, 12, 22, 12, 22) friends=[1, 2, 3]
id=123 name='John Snow' signup_ts=datetime.datetime(2020, 12, 22, 12, 22) friends=[1, 2, 3]
id=123 name='John Snow' signup_ts=datetime.datetime(2020, 12, 22, 12, 22) friends=[1, 2, 3]
#print(user.dict())# 转化成字典
#print(user.json())# 转化成json
# 和上面的区别是,下面的更详细,说了什么解析的方案
print(user.schema())
print(user.schema_json())
》》》
{'title': 'User', 'type': 'object', 'properties': {'id': {'title': 'Id', 'type': 'integer'}, 'name': {'title': 'Name', 'default': 'John Snow', 'type': 'string'}, 'signup_ts': {'title': 'Signup Ts', 'type': 'string', 'format': 'date-time'}, 'friends': {'title': 'Friends', 'default': [], 'type': 'array', 'items': {'type': 'integer'}}}, 'required': ['id']}
{"title": "User", "type": "object", "properties": {"id": {"title": "Id", "type": "integer"}, "name": {"title": "Name", "default": "John Snow", "type": "string"}, "signup_ts": {"title": "Signup Ts", "type": "string", "format": "date-time"}, "friends": {"title": "Friends", "default": [], "type": "array", "items": {"type": "integer"}}}, "required": ["id"]}
# 不做校验直接赋值
user_data = {"id": "error", "signup_ts": "2020-12-22 12 22", "friends": [1, 2, 3]} # id是字符串 是错误的
print(User.construct(**user_data)) # 不检验数据直接创建模型类,不建议在construct方法中传入未经验证的数据
》》》
name='John Snow' signup_ts='2020-12-22 12 22' friends=[1, 2, 3] id='error'
print(User.__fields__.keys()) # 定义模型类的时候,所有字段都注明类型,字段顺序就不会乱
》》》
dict_keys(['id', 'name', 'signup_ts', 'friends'])
6.解析文件
from pathlib import Path
path = Path('pydantic_tutorial.json') #当前目录下的 pydantic_tutorial文件
path.write_text('{"id": "123", "signup_ts": "2020-12-22 12:22", "friends": [1, 2, "3"]}') #写入
print(User.parse_file(path)) #读取
7.递归模型
class Sound(BaseModel):
sound: str
class Dog(BaseModel):
birthday: date
weight: float = Optional[None]
sound: List[Sound] # 不同的狗有不同的叫声。递归模型(Recursive Models)就是指一个嵌套一个
dogs = Dog(birthday=date.today(), weight=6.66, sound=[{"sound": "wang wang ~"}, {"sound": "ying ying ~"}])
print(dogs.dict())
》》》
{'birthday': datetime.date(2023, 10, 28), 'sound': [{'sound': 'wang wang ~'}, {'sound': 'ying ying ~'}]}
8.ORM模型:从类实例创建符合ORM对象的模型
from typing import List
from typing import Optional
from pydantic import BaseModel, ValidationError
from pydantic import constr
from sqlalchemy import Column, Integer, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
# 先建立ORM数据表
class CompanyOrm(Base):
__tablename__ = 'companies'
id = Column(Integer, primary_key=True, nullable=False)
public_key = Column(String(20), index=True, nullable=False, unique=True)
name = Column(String(63), unique=True)
domains = Column(ARRAY(String(255)))
# 继承ORM表
class CompanyModel(BaseModel):
id: int
public_key: constr(max_length=20)
name: constr(max_length=63)
domains: List[constr(max_length=255)]
class Config:
orm_mode = True
# 创建实例
co_orm = CompanyOrm(
id=123,
public_key='foobar',
name='Testing',
domains=['example.com', 'foobar.com'],
)
# 进行格式化
print(CompanyModel.from_orm(co_orm))
Pydantic支撑的字段类型官方文档
二、请求参数和验证
1.第一个FastAPI
1.1 创建app
http://localhost:8000/docs
默认8000端口和swagger-ui文档地址
命令行启动
启动命令:
uvicorn hello_world:app --reload
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI() # 这里不一定是app,名字随意
main启动
import time
import uvicorn
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from coronavirus import application
app = FastAPI(
title='FastAPI Tutorial and Coronavirus Tracker API Docs',
description='FastAPI教程 描述',
version='1.0.0',
docs_url='/docs',
redoc_url='/redocs',
)
if __name__ == '__main__':
uvicorn.run('run:app', host='0.0.0.0', port=8000, reload=True, debug=True, workers=1)
1.2 创建实体
class CityInfo(BaseModel):
province: str
country: str
is_affected: Optional[bool] = None # 与bool的区别是可以不传,默认是null
1.4 创建api
async :使用异步的方式
@app.get('/')
async def hello_world():
return {'hello': 'world'}
# 使用请求url的city的方式
@app.get('/city/{city}')
async def result(city: str, query_string: Optional[str] = None):
return {'city': city, 'query_string': query_string}
# 使用URL 和请求实体类的方式
@app.put('/city/{city}')
async def result(city: str, city_info: CityInfo):
return {'city': city, 'country': city_info.country, 'is_affected': city_info.is_affected}
# 启动命令:uvicorn hello_world:app --reload
2.默认集成swagger-ui
http://localhost:8000/docs
http://localhost:8000/redoc
只能看不能用
http://localhost:8000/openapi.json
是默认的信息
3.路由
3.1路由定义
子项目
主应用
引入子项目
tags:标题
wokers:进程的数量
#!/usr/bin/python3
# -*- coding:utf-8 -*-
# __author__ = '__Jack__'
import time
import uvicorn
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from coronavirus import application
from tutorial import app03, app04, app05, app06, app07, app08
# from fastapi.exceptions import RequestValidationError
# from fastapi.responses import PlainTextResponse
# from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI(
title='FastAPI Tutorial and Coronavirus Tracker API Docs',
description='FastAPI教程 新冠病毒疫情跟踪器API接口文档,项目代码:https://github.com/liaogx/fastapi-tutorial',
version='1.0.0',
docs_url='/docs',
redoc_url='/redocs',
)
# mount表示将某个目录下一个完全独立的应用挂载过来,这个不会在API交互文档中显示
app.mount(path='/static', app=StaticFiles(directory='./coronavirus/static'), name='static') # .mount()不要在分路由APIRouter().mount()调用,模板会报错
# @app.exception_handler(StarletteHTTPException) # 重写HTTPException异常处理器
# async def http_exception_handler(request, exc):
# """
# :param request: 这个参数不能省
# :param exc:
# :return:
# """
# return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
#
#
# @app.exception_handler(RequestValidationError) # 重写请求验证异常处理器
# async def validation_exception_handler(request, exc):
# """
# :param request: 这个参数不能省
# :param exc:
# :return:
# """
# return PlainTextResponse(str(exc), status_code=400)
@app.middleware('http')
async def add_process_time_header(request: Request, call_next): # call_next将接收request请求做为参数
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers['X-Process-Time'] = str(process_time) # 添加自定义的以“X-”开头的请求头
return response
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://127.0.0.1",
"http://127.0.0.1:8080"
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(app03, prefix='/chapter03', tags=['第三章 请求参数和验证'])
app.include_router(app04, prefix='/chapter04', tags=['第四章 响应处理和FastAPI配置'])
app.include_router(app05, prefix='/chapter05', tags=['第五章 FastAPI的依赖注入系统'])
app.include_router(app06, prefix='/chapter06', tags=['第六章 安全、认证和授权'])
app.include_router(app07, prefix='/chapter07', tags=['第七章 FastAPI的数据库操作和多应用的目录结构设计'])
app.include_router(app08, prefix='/chapter08', tags=['第八章 中间件、CORS、后台任务、测试用例'])
app.include_router(application, prefix='/coronavirus', tags=['新冠病毒疫情跟踪器API'])
if __name__ == '__main__':
uvicorn.run('run:app', host='0.0.0.0', port=8000, reload=True, debug=True, workers=1)
3.2路由实现
3.路径参数和数据的解析、验证
3.1使用枚举类型
class CityName(str, Enum):
Beijing = "Beijing China"
Shanghai = "Shanghai China"
@app03.get("/enum/{city}") # 枚举类型的参数
async def latest(city: CityName):
if city == CityName.Shanghai:
return {"city_name": city, "confirmed": 1492, "death": 7}
if city == CityName.Beijing:
return {"city_name": city, "confirmed": 971, "death": 9}
return {"city_name": city, "latest": "unknown"}
3.2传递文件路径
@app03.get("/files/{file_path:path}") # 通过path parameters传递文件路径
def filepath(file_path: str):
return f"The file path is {file_path}"
3.3校验路径参数
@app03.get("/path_/{num}")
def path_params_validate(
num: int = Path(..., title="Your Number", description="不可描述", ge=1, le=10),
):
return num
3.4.查询参数和数据的解析、验证
给了默认值就是选填的参数,没给默认值就是必填参数
from fastapi import APIRouter, Query, Path, Body, Cookie, Header
@app03.get("/query")
def page_limit(page: int = 1, limit: Optional[int] = None): # 给了默认值就是选填的参数,没给默认值就是必填参数
if limit:
return {"page": page, "limit": limit}
return {"page": page}
@app03.get("/query/bool/conversion") # bool类型转换:yes on 1 True true会转换成true, 其它为false
def type_conversion(param: bool = False):
return param
@app03.get("/query/validations") # 长度+正则表达式验证,比如长度8-16位,以a开头。其它校验方法看Query类的源码
def query_params_validate(
value: str = Query(..., min_length=8, max_length=16, regex="^a"), # ...换成None就变成选填的参数
values: List[str] = Query(["v1", "v2"], alias="alias_name")
): # 多个查询参数的列表。参数别名
return value, values
3.5.请求体和混合参数的使用
Request Body and Fields 请求体和字段
class CityInfo(BaseModel):
name: str = Field(..., example="Beijing") # example是注解的作用,值不会被验证
country: str
country_code: str = None # 给一个默认值
country_population: int = Field(default=800, title="人口数量", description="国家的人口数量", ge=800)
class Config:
schema_extra = {
"example": {
"name": "Shanghai",
"country": "China",
"country_code": "CN",
"country_population": 1400000000,
}
}
@app03.post("/request_body/city")
def city_info(city: CityInfo):
print(city.name, city.country) # 当在IDE中输入city.的时候,属性会自动弹出
return city.dict()
3.6.混合参数的使用
"""Request Body + Path parameters + Query parameters 多参数混合"""
@app03.put("/request_body/city/{name}")
def mix_city_info(
name: str,
city01: CityInfo,
city02: CityInfo, # Body可以是多个的
confirmed: int = Query(ge=0, description="确诊数", default=0),
death: int = Query(ge=0, description="死亡数", default=0),
):
if name == "Shanghai":
return {"Shanghai": {"confirmed": confirmed, "death": death}}
return city01.dict(), city02.dict()
@app03.put("/request_body/multiple/parameters")
def body_multiple_parameters(
city: CityInfo = Body(..., embed=True), # 当只有一个Body参数的时候,embed=True表示请求体参数嵌套。多个Body参数默认就是嵌套的
confirmed: int = Query(ge=0, description="确诊数", default=0),
death: int = Query(ge=0, description="死亡数", default=0),
):
print(f"{city.name} 确诊数:{confirmed} 死亡数:{death}")
return city.dict()
3.7.数据格式嵌套的请求体
class Data(BaseModel):
city: List[CityInfo] = None # 这里就是定义数据格式嵌套的请求体
date: date # 额外的数据类型,还有uuid datetime bytes frozenset等,参考:https://fastapi.tiangolo.com/tutorial/extra-data-types/
confirmed: int = Field(ge=0, description="确诊数", default=0)
deaths: int = Field(ge=0, description="死亡数", default=0)
recovered: int = Field(ge=0, description="痊愈数", default=0)
@app03.put("/request_body/nested")
def nested_models(data: Data):
return data
3.8.Cookie 和 Header 参数
@app03.get("/cookie") # 效果只能用Postman测试
def cookie(cookie_id: Optional[str] = Cookie(None)): # 定义Cookie参数需要使用Cookie类,否则就是查询参数
return {"cookie_id": cookie_id}
@app03.get("/header")
def header(user_agent: Optional[str] = Header(None, convert_underscores=True), x_token: List[str] = Header(None)):
"""
有些HTTP代理和服务器是不允许在请求头中带有下划线的,所以Header提供convert_underscores属性让设置
:param user_agent: convert_underscores=True 会把 user_agent 变成 user-agent
:param x_token: x_token是包含多个值的列表
:return:
"""
return {"User-Agent": user_agent, "x_token": x_token}
这个是cookie
heard
4. 响应处理和FastAPI配置
1.处理返回响应实体
新建实体
# 接受
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
mobile: str = "10086"
address: str = None
full_name: Optional[str] = None
# 输出
class UserOut(BaseModel):
username: str
email: EmailStr # 用 EmailStr 需要 pip install pydantic[email]
mobile: str = "10086"
address: str = None
full_name: Optional[str] = None
接受处理response_model指定返回类处理
users = {
"user01": {"username": "user01", "password": "123123", "email": "user01@example.com"},
"user02": {"username": "user02", "password": "123456", "email": "user02@example.com", "mobile": "110"}
}
@app04.post("/response_model/", response_model=UserOut, response_model_exclude_unset=True)
async def response_model(user: UserIn):
"""response_model_exclude_unset=True表示默认值不包含在响应中,仅包含实际给的值,如果实际给的值与默认值相同也会包含在响应中"""
print(user.password) # password不会被返回
# return user
return users["user01"]
进一步通过response_model相似的处理我们的返回类属性时都是必须的
@app04.post(
"/response_model/attributes",
response_model=UserOut,
# response_model=Union[UserIn, UserOut],
# response_model=List[UserOut],
response_model_include=["username", "email", "mobile"],
response_model_exclude=["mobile"]
)
async def response_model_attributes(user: UserIn):
"""response_model_include列出需要在返回结果中包含的字段;response_model_exclude列出需要在返回结果中排除的字段"""
# del user.password # Union[UserIn, UserOut]后,删除password属性也能返回成功
return user
# return [user, user]
2.响应状态码
@app04.post("/status_code", status_code=200)
async def status_code():
return {"status_code": 200}
@app04.post("/status_attribute", status_code=status.HTTP_200_OK)
async def status_attribute():
print(type(status.HTTP_200_OK))
return {"status_code": status.HTTP_200_OK}
》》》
# 上面是状态码
# return是返回数据
{"status_code": 200}
3.Form Data 表单数据处理
@app04.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)): # 定义表单参数
"""用Form类需要pip install python-multipart; Form类的元数据和校验方法类似Body/Query/Path/Cookie"""
return {"username": username}
4.Request Files 单文件、多文件上传及参数详解
上传
@app04.post("/file")
async def file_(file: bytes = File(...)): # 如果要上传多个文件 files: List[bytes] = File(...)
"""使用File类 文件内容会以bytes的形式读入内存 适合于上传小文件"""
return {"file_size": len(file)}
上传大文件
@app04.post("/upload_files")
async def upload_files(files: List[UploadFile] = File(...)): # 如果要上传单个文件 file: UploadFile = File(...)
"""
使用UploadFile类的优势:
1.文件存储在内存中,使用的内存达到阈值后,将被保存在磁盘中
2.适合于图片、视频大文件
3.可以获取上传的文件的元数据,如文件名,创建时间等
4.有文件对象的异步接口
5.上传的文件是Python文件对象,可以使用write(), read(), seek(), close()操作
"""
for file in files:
contents = await file.read()
print(contents)
return {"filename": files[0].filename, "content_type": files[0].content_type}
5.FASTAPI项目的静态文件配置
path:api请求的前缀路径
StaticFiles:文件地址
name:名字
# mount表示将某个目录下一个完全独立的应用挂载过来,这个不会在API交互文档中显示
app.mount(path='/static', app=StaticFiles(directory='./coronavirus/static'), name='static') # .mount()不要在分路由APIRouter().mount()调用,模板会报错
6.url路径操作配置
response_model:返回类
response_description:返回表述
status_code:返回状态码
summary:就是通常swagger-ui都是显示url调用的函数名称,这个可以自定义内容
tags:swagger-ui另起一个标题
deprecated=True:表示接口已经废了,但是可以调用
"""Path Operation Configuration 路径操作配置"""
@app04.post(
"/path_operation_configuration",
response_model=UserOut,
# tags=["Path", "Operation", "Configuration"],
summary="This is summary",
description="This is description",
response_description="This is response description",
deprecated=True,
status_code=status.HTTP_200_OK
)
async def path_operation_configuration(user: UserIn):
"""
Path Operation Configuration 路径操作配置
:param user: 用户信息
:return: 返回结果
"""
return user.dict()
7.swagger-ui界面配置
app = FastAPI(
title='FastAPI Tutorial and Coronavirus Tracker API Docs',
description='FastAPI教程 新冠病毒疫情跟踪器API接口文档,项目代码:https://github.com/liaogx/fastapi-tutorial',
version='1.0.0',
docs_url='/docs',
redoc_url='/redocs',
)
8.Handling Errors 错误处理
@app04.get("/http_exception")
async def http_exception(city: str):
if city != "Beijing":
raise HTTPException(status_code=404, detail="City not found!", headers={"X-Error": "Error"})
return {"city": city}
@app04.get("/http_exception/{city_id}")
async def override_http_exception(city_id: int):
if city_id == 1:
raise HTTPException(status_code=418, detail="Nope! I don't like 1.")
return {"city_id": city_id}
全局处理
在run.py添加
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
@app.exception_handler(StarletteHTTPException) # 重写HTTPException异常处理器
async def http_exception_handler(request, exc):
"""
:param request: 这个参数不能省
:param exc:
:return:
"""
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError) # 重写请求验证异常处理器
async def validation_exception_handler(request, exc):
"""
:param request: 这个参数不能省
:param exc:
:return:
"""
return PlainTextResponse(str(exc), status_code=400)
5.依赖注入系统
创建、导入、和声明依赖
函数作为依赖
创建
async def common_parameters(q: Optional[str] = None, page: int = 1, limit: int = 100):
return {"q": q, "page": page, "limit": limit}
使用Depends()
@app05.get("/dependency01")
async def dependency01(commons: dict = Depends(common_parameters)):
return commons
@app05.get("/dependency02")
def dependency02(commons: dict = Depends(common_parameters)): # 可以在async def中调用def依赖,也可以在def中导入async def依赖
return commons
类作为依赖
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
class CommonQueryParams:
def __init__(self, q: Optional[str] = None, page: int = 1, limit: int = 100):
self.q = q
self.page = page
self.limit = limit
@app05.get("/classes_as_dependencies")
# async def classes_as_dependencies(commons: CommonQueryParams = Depends(CommonQueryParams)):
# async def classes_as_dependencies(commons: CommonQueryParams = Depends()):
async def classes_as_dependencies(commons=Depends(CommonQueryParams)):
response = {}
if commons.q:
response.update({"q": commons.q})
items = fake_items_db[commons.page: commons.page + commons.limit]
response.update({"items": items})
return response
子依赖
def query(q: Optional[str] = None):
return q
def sub_query(q: str = Depends(query), last_query: Optional[str] = None):
if not q:
return last_query
return q
@app05.get("/sub_dependency")
async def sub_dependency(final_query: str = Depends(sub_query, use_cache=True)):
"""use_cache默认是True, 表示当多个依赖有一个共同的子依赖时,每次request请求只会调用子依赖一次,多次调用将从缓存中获取"""
return {"sub_dependency": final_query}
路径操作装饰器中的多依赖
async def verify_token(x_token: str = Header(...)):
"""没有返回值的子依赖"""
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
async def verify_key(x_key: str = Header(...)):
"""有返回值的子依赖,但是返回值不会被调用"""
if x_key != "fake-super-secret-key":
raise HTTPException(status_code=400, detail="X-Key header invalid")
return x_key
@app05.get("/dependency_in_path_operation", dependencies=[Depends(verify_token), Depends(verify_key)]) # 这时候不是在函数参数中调用依赖,而是在路径操作中
async def dependency_in_path_operation():
return [{"user": "user01"}, {"user": "user02"}]
全局依赖(token-head验证适用)
app05 = APIRouter(dependencies=[Depends(verify_token), Depends(verify_key)])
或者
主app使用
带yield的依赖
# 这个需要Python3.7才支持,Python3.6需要pip install async-exit-stack async-generator
# 以下都是伪代码
async def get_db():
db = "db_connection"
try:
yield db
finally:
db.endswith("db_close")
async def dependency_a():
dep_a = "generate_dep_a()"
try:
yield dep_a
finally:
dep_a.endswith("db_close")
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = "generate_dep_b()"
try:
yield dep_b
finally:
dep_b.endswith(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = "generate_dep_c()"
try:
yield dep_c
finally:
dep_c.endswith(dep_b)
6.安全、认证和授权
1.OAuth2验证
第一步:声明和获取token
"""OAuth2 密码模式和 FastAPI 的 OAuth2PasswordBearer"""
"""
OAuth2PasswordBearer是接收URL作为参数的一个类:客户端会向该URL发送username和password参数,然后得到一个Token值
OAuth2PasswordBearer并不会创建相应的URL路径操作,只是指明客户端用来请求Token的URL地址
当请求到来的时候,FastAPI会检查请求的Authorization头信息,如果没有找到Authorization头信息,或者头信息的内容不是Bearer token,它会返回401状态码(UNAUTHORIZED)
"""
# 整理/chapter06/token 还没有实现
oauth2_schema = OAuth2PasswordBearer(tokenUrl="/chapter06/token") # 请求Token的URL地址 http://127.0.0.1:8000/chapter06/token
# 用户拿着账户密码过来,返回token
@app06.get("/oauth2_password_bearer")
async def oauth2_password_bearer(token: str = Depends(oauth2_schema)):
return {"token": token}
第二步:模拟数据库
"""基于 Password 和 Bearer token 的 OAuth2 认证"""
fake_users_db = {
"john snow": {
"username": "john snow",
"full_name": "John Snow",
"email": "johnsnow@example.com",
"hashed_password": "fakehashedsecret",
"disabled": False,
},
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice@example.com",
"hashed_password": "fakehashedsecret2",
"disabled": True,
},
}
#对密码进行加密
def fake_hash_password(password: str):
return "fakehashed" + password
# 建立实体
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
class UserInDB(User):
hashed_password: str
第三步:写服务于校验
# 获取用户,在不在上面的模拟数据库里面,相当于java的DAO层
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
# 模拟,看token在不在数据库里面,相当于java的server层
def fake_decode_token(token: str):
user = get_user(fake_users_db, token)
return user
# 验证token在不在,同时返回给前端,相当于server层
async def get_current_user(token: str = Depends(oauth2_schema)):
user = fake_decode_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"}, # OAuth2的规范,如果认证失败,请求头中返回“WWW-Authenticate”
)
return user
# 如果是活跃的,就返回活跃的用户信息,否则不返回
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
return current_user
第四步:写接口
# 获取token
@app06.post("/chapter06/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user_dict = fake_users_db.get(form_data.username)
if not user_dict:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Incorrect username or password")
user = UserInDB(**user_dict)
hashed_password = fake_hash_password(form_data.password)
if not hashed_password == user.hashed_password:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Incorrect username or password")
return {"access_token": user.username, "token_type": "bearer"}
# 获取活跃的信息
@app06.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
直接请求
2.Bearer with JWT tokens 开发基于JSON Web Tokens的认证
fake_users_db.update({
"john snow": {
"username": "john snow",
"full_name": "John Snow",
"email": "johnsnow@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
})
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" # 生成密钥 openssl rand -hex 32
ALGORITHM = "HS256" # 算法
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 访问令牌过期分钟
class Token(BaseModel):
"""返回给用户的Token"""
access_token: str
token_type: str
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_schema = OAuth2PasswordBearer(tokenUrl="/chapter06/jwt/token")
def verity_password(plain_password: str, hashed_password: str):
"""对密码进行校验"""
return pwd_context.verify(plain_password, hashed_password)
def jwt_get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def jwt_authenticate_user(db, username: str, password: str):
user = jwt_get_user(db=db, username=username)
if not user:
return False
if not verity_password(plain_password=password, hashed_password=user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(claims=to_encode, key=SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@app06.post("/jwt/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = jwt_authenticate_user(db=fake_users_db, username=form_data.username, password=form_data.password)
if not user:
raise HTTPException(
status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
async def jwt_get_current_user(token: str = Depends(oauth2_schema)):
credentials_exception = HTTPException(
status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token=token, key=SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = jwt_get_user(db=fake_users_db, username=username)
if user is None:
raise credentials_exception
return user
async def jwt_get_current_active_user(current_user: User = Depends(jwt_get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
return current_user
@app06.get("/jwt/users/me")
async def jwt_read_users_me(current_user: User = Depends(jwt_get_current_active_user)):
return current_user