fastapi
一. 安装运行相关
- 安装
pip install fastapi[all]
- 简单的示例
'''main.py'''
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
- 运行程序
uvicorn main:app --host 0.0.0.0 --port 80 --reload
二. Pydantic 基础教程
- pydantic基本使用
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel
class User(BaseModel):
id: int # 必填字段
name: str = "John Snow" # 有默认值,选填字段
signup_ts: Optional[datetime] = None
friends: List[int] = [] # 列表中元素是int类型或者可以直接转换成int类型
external_data = {
"id": "123",
"signup_ts": "2022-12-22 12:22",
"friends": [1, 2, "3"]
}
user = User(**external_data)
print(user.id, user.name)
print(repr(user.signup_ts))
print(user.dict())
- 校验失败处理
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ValidationError
class User(BaseModel):
id: int # 必填字段
name: str = "John Snow" # 有默认值,选填字段
signup_ts: Optional[datetime] = None
friends: List[int] = [] # 列表中元素是int类型或者可以直接转换成int类型
external_data = {
"id": "123",
"signup_ts": "2022-12-22 12:22",
"friends": [1, 2, "3"]
}
try:
User(id=1, signup_ts=datetime.today(), friends=[1, 2, "not number"])
except ValidationError as e:
print(e.json())
- 模型类的属性和方法
from pathlib import Path
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ValidationError
class User(BaseModel):
id: int # 必填字段
name: str = "John Snow" # 有默认值,选填字段
signup_ts: Optional[datetime] = None
friends: List[int] = [] # 列表中元素是int类型或者可以直接转换成int类型
external_data = {
"id": "123",
"signup_ts": "2022-12-22 12:22",
"friends": [1, 2, "3"]
}
user = User(**external_data)
print(user.dict())
print(user.json())
print(user.copy()) # 这里是浅拷贝
print(User.parse_obj(obj=external_data)) # 解析字典
print(User.parse_raw('{"id": "123", "signup_ts": "2022-12-22 12:22", "friends": [1, 2, "3"]}')) # 解析原生数据
# 解析文件
path = Path('pydantic_tutorial.json')
path.write_text('{"id": "123", "signup_ts": "2022-12-22 12:22", "friends": [1, 2, "3"]}')
print(User.parse_file(path))
print(user.schema())
print(user.schema_json())
print(User.__fields__.keys()) # 定义模型类的时候,所有字段都注明类型,字段顺序就不会乱
- 递归模型
from datetime import date
from pydantic import BaseModel
from typing import List, Optional
class Sound(BaseModel):
sound: str
class Dog(BaseModel):
birthday: date
weight: Optional[float] = None
sound: List[Sound] # 不同的狗有不同的叫声。递归模型(Recursive Models)就是指一个嵌套一个
dogs = Dog(birthday=date.today(), weight=6.66, sound=[{"sound": "wang wang ~"}, {"sound": "ying ying ~"}])
print(dogs.dict())
- ORM模型:从类实例创建符合ORM对象的模型
from typing import List
from pydantic import BaseModel, constr
from sqlalchemy import Column, Integer, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class CompanyOrm(Base):
__tablename__ = 'companies'
id = Column(Integer, primary_key=True, nullable=False) # nullable值为False时不能为空
public_key = Column(String(20), index=True, nullable=False, unique=True) # index建立索引 unique是否唯一
name = Column(String(63), unique=True)
domains = Column(ARRAY(String(255)))
class CompanyMode(BaseModel):
id: int
public_key: constr(max_length=20) # constr限制字段长度
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', 'imooc.com']
)
print(CompanyMode.from_orm(co_orm))
三. 请求参数和验证
- "hello world" 接口给后端传 COVID-19 感染数据
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class CityInfo(BaseModel):
province: str
country: str
is_affected: Optional[bool] = None
@app.get('/')
async def hello_world():
return {'hello': 'world'}
@app.get('/city/{city}')
async def result(city: str, query_string: Optional[str] = None):
return {'city': city, 'query_string': query_string}
@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}
- 多路由
"""chapter03-05.py"""
from fastapi import APIRouter
app03 = APIRouter()
"""__init__.py"""
from .chapter03 import app03
from .chapter04 import app04
from .chapter05 import app05
"""run.py"""
import uvicorn
from fastapi import FastAPI
from tutorial import app03, app04, app05
app = FastAPI()
app.include_router(app03, prefix='/chapter03', tags=['第三章 请求参数和验证'])
app.include_router(app04, prefix='/chapter04', tags=['第四章 相应处理和FastAPI配置'])
app.include_router(app05, prefix='/chapter05', tags=['第五章 FastAPI的依赖注入系统'])
if __name__ == '__main__':
uvicorn.run('run:app', host='0.0.0.0', port=80, reload=True, debug=True, workers=1)
- 路径参数和数据的解析、验证
"""chapter03.py"""
from enum import Enum
from fastapi import APIRouter, Path
app03 = APIRouter()
@app03.get('/path/parameters')
def path_params01():
return {"message": "This is a message"}
@app03.get('/path/{parameters}') # 函数的顺序就是路由的顺序
def path_params01(parameters: str):
return {"message": parameters}
class CityName(str, Enum):
Beijing = 'Beijing City'
Shanghai = 'Shanghai City'
@app03.get('/enum/{city}') # 枚举类型参数
async def latest(city: CityName):
if city == CityName.Shanghai:
return {'city_name': city, 'confirmed': 0, 'death': 0}
if city == CityName.Beijing:
return {'city_name': city, 'confirmed': 0, 'death': 0}
return {'city_name': city, 'latest': 'unknown'}
@app03.get('/files/{file_path:path}') # 通过path parameters传递文件路径
def filepath(file_path: str):
return f"The file path is {file_path}"
@app03.get('/path_/{num}') # 路径参数校验
def path_params_validate(num: int = Path(..., title='Your number', description='不可描述', ge=1, le=10)):
return num
- 查询参数和数据的解析、验证
"""chapter03.py"""
from typing import Optional, List
from fastapi import APIRouter, Query
app03 = APIRouter()
@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(default=['v1', 'v2'], alias='alias_name') # 多个查询参数的列表。参数别名
):
return value, values
- 请求体和字段
"""chapter03.py"""
from fastapi import APIRouter, Query
from pydantic import BaseModel, Field
app03 = APIRouter()
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()
- 多参数混合
"""chapter03.py"""
from fastapi import APIRouter, Query
from pydantic import BaseModel, Field, Body
app03 = APIRouter()
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.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()
- 数据格式嵌套的请求体
"""chapter03.py"""
from typing import List
from datetime import date
from fastapi import APIRouter
from pydantic import BaseModel, Field
app03 = APIRouter()
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": 140000000
}
}
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
- cookie和header参数
"""chapter03.py"""
from typing import Optional
from fastapi import APIRouter, Cookie, Header
app03 = APIRouter()
@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}
四. 响应处理和FastAPI配置
- 响应模型示例
"""chapter04.py"""
from fastapi import APIRouter
from typing import List, Optional, Union
from pydantic import BaseModel, EmailStr
app04 = APIRouter()
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
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"]
@app04.post("/response_model/attributes",
response_model=UserOut,
# response_model=Union[UserIn, UserOut]
# response_model=List[UserIn, UserOut]
response_model_include=["username", "email"],
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]
- 响应状态码
"""chapter04.py"""
from fastapi import APIRouter, status
app04 = APIRouter()
@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}
- 表单数据处理
"""chapter04.py"""
from fastapi import APIRouter, Form
app04 = APIRouter()
@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}
- 单文件、多文件上传及参数详解
"""chapter04.py"""
from typing import List
from fastapi import APIRouter, File, UploadFile
app04 = APIRouter()
@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}
- FastAPI项目的静态文件配置
"""run.py"""
"""coronavirus下创建static文件夹"""
import uvicorn
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from tutorial import app03, app04, app05
app = FastAPI()
# mount表示将某个目录下一个完全独立的应用挂载过来,这个不会在API交互文档中显示
app.mount(path='/static', app=StaticFiles(directory='./coronavirus/static'),
name='static') # .mount()不要在分路由APIRouter().mount()调用,模板会报错
app.include_router(app03, prefix='/chapter03', tags=['第三章 请求参数和验证'])
app.include_router(app04, prefix='/chapter04', tags=['第四章 相应处理和FastAPI配置'])
app.include_router(app05, prefix='/chapter05', tags=['第五章 FastAPI的依赖注入系统'])
if __name__ == '__main__':
uvicorn.run('run:app', host='0.0.0.0', port=80, reload=True, debug=True, workers=10)
- 路径操作配置
"""chapter04.py"""
from typing import Optional
from fastapi import APIRouter, status
from pydantic import BaseModel, EmailStr
app04 = APIRouter()
class UserOut(BaseModel):
username: str
email: EmailStr # 用EmailStr需要pip install pydantic[email]
mobile: str = "10086"
address: str = None
full_name: Optional[str] = None
@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, # 为True时表示接口已废弃,但是可以正常调用
status_code=status.HTTP_200_OK
)
async def path_operation_configuration(user: UserIn):
"""
Path Operation Configuration 路径操作配置
:param user: 用户信息
:return: 返回结果
"""
return user.dict()
- FastAPI应用的常见配置项
"""run.py"""
import uvicorn
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from tutorial import app03, app04, app05
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.include_router(app03, prefix='/chapter03', tags=['第三章 请求参数和验证'])
app.include_router(app04, prefix='/chapter04', tags=['第四章 相应处理和FastAPI配置'])
app.include_router(app05, prefix='/chapter05', tags=['第五章 FastAPI的依赖注入系统'])
if __name__ == '__main__':
uvicorn.run('run:app', host='0.0.0.0', port=80, reload=True, debug=True, workers=10)
- 错误处理
"""chapter04.py"""
from fastapi import APIRouter, HTTPException
app04 = APIRouter()
@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}
"""run.py"""
import uvicorn
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from tutorial import app03, app04, app05
# 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接口文档',
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.include_router(app03, prefix='/chapter03', tags=['第三章 请求参数和验证'])
app.include_router(app04, prefix='/chapter04', tags=['第四章 响应处理和FastAPI配置'])
app.include_router(app05, prefix='/chapter05', tags=['第五章 FastAPI的依赖注入系统'])
if __name__ == '__main__':
uvicorn.run('run:app', host='0.0.0.0', port=8000, reload=True, debug=True, workers=1)
"""chapter04.py"""
from fastapi import APIRouter, HTTPException
app04 = APIRouter()
@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}
五. FastAPI的依赖注入系统
- 创建、导入和声明依赖
"""chapter05.py"""
from typing import Optional
from fastapi import APIRouter, Depends
app05 = APIRouter()
async def common_parameters(q: Optional[str] = None, page: int = 1, limit: int = 100):
return {"q": q, "page": page, "limit": limit}
@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
- 类作为依赖项
"""chapter05.py"""
from fastapi import APIRouter, Depends
app05 = APIRouter()
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
- 子依赖的创建和调用
"""chapter05.py"""
from typing import Optional
from fastapi import APIRouter, Depends
app05 = APIRouter()
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}
- 路径操作装饰器中的多依赖
"""chapter05.py"""
from typing import Optional
from fastapi import APIRouter, Depends, Header, HTTPException
app05 = APIRouter()
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"}]
- 全局依赖
"""chapter05.py"""
from typing import Optional
from fastapi import APIRouter, Depends, Header, HTTPException
app05 = APIRouter()
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"}]
# app05 = APIRouter(dependencies=[Depends(verify_token), Depends(verify_key)])
- 带yield的依赖
"""chapter05.py"""
from typing import Optional
from fastapi import APIRouter, Depends
app05 = APIRouter()
# 这个需要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)
六. 安全、认证和授权
- OAuth2 密码模式和 FastAPI 的 OAuth2PasswordBearer
"""chapter06-08.py"""
from fastapi import APIRouter
app06 = APIRouter()
"""__init__.py"""
from .chapter03 import app03
from .chapter04 import app04
from .chapter05 import app05
from .chapter06 import app06
from .chapter07 import app07
from .chapter08 import app08
"""run.py"""
import uvicorn
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
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接口文档',
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.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、后台任务、测试用例'])
if __name__ == '__main__':
uvicorn.run('run:app', host='0.0.0.0', port=80, reload=True, debug=True, wokers=1)
"""chapter06.py"""
from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordBearer
app06 = APIRouter()
"""
OAuth2PasswordBearer是接收URL作为参数的一个类:客户端会向该URL发送username和password参数,然后得到一个Token值
OAuth2PasswordBearer并不会创建相应的URL路径操作,只是指明客户端用来请求Token的URL地址
当请求到来的时候,FastAPI会检查请求的Authorization头信息,如果没有找到Authorization头信息,或者头信息的内容不是Bearer token,它会返回401状态码(UNAUTHORIZED)
"""
oauth2_schema = OAuth2PasswordBearer(tokenUrl="/chapter06/token") # 请求Token的URL地址 http://127.0.0.1/chapter06/token
@app06.get("/oauth2_password_bearer")
async def oauth2_password_bearer(token: str = Depends(oauth2_schema)):
return {"token": token}
- 基于 Password 和 Bearer token 的 OAuth2 认证
"""chapter06.py"""
from typing import Optional
from pydantic import BaseModel
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app06 = APIRouter()
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
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def fake_decode_token(token: str):
user = get_user(fake_users_db, token)
return user
@app06.post("/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"}
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
@app06.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
- 开发基于JSON Web Tokens的认证
"""chapter06.py"""
"""需要安装bcrypt、passlib和python-jose模块"""
from typing import Optional
from pydantic import BaseModel
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app06 = APIRouter()
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
__EOF__

本文作者:闪耀星辰
本文链接:https://www.cnblogs.com/lip215591601/p/15843699.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
本文链接:https://www.cnblogs.com/lip215591601/p/15843699.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示