banner
小鱼

小鱼's Blog

FastAPI 入门

FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 3.6+ 并基于标准的 Python 类型提示。

# 安装与启动#

首先安装 FastAPI

pip install "fastapi[all]"

以上安装还包括了  uvicorn ,可以将其用作运行代码的服务器。

执行代码时,使用一下命令

uvicorn main:app --reload

服务器上后台启动

nohup uvicorn main:app --host 0.0.0.0 --port 8000 > /文件路径/log.log 2>&1 &

接着执行

bg

可以将一个在后台暂停的作业切换到继续在后台执行

最后执行

disown

用于将一个作业从当前终端会话中分离,使其不会受到终端会话的影响,退出终端仍然运行


uvicorn main:app 命令含义如下:

  • mainmain.py 文件(一个 Python「模块」)。
  • app:在 main.py 文件中通过 app = FastAPI() 创建的对象,也可以是其他名称
  • --reload:让服务器在更新代码后重新启动。仅在开发时使用该选项。


image

此时访问 localhost:8000 即可调用

# Hello World#

## 新建一个 main.py 文件#

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

代码解释

from fastapi import FastAPI

FastAPI  是一个为你的 API 提供了所有功能的 Python 类。

app = FastAPI()

这里的变量 app 会是 FastAPI 类的一个「实例」。

这个实例将是创建所有 API 的主要交互对象。

这个  app  同样在启动的命令中被  uvicorn  所引用

也可以写成 myapi = FastAPI ()
启动命令就变成 uvicorn main --reload

@app.get("/")

告诉 FastAPI 在它下方的函数负责处理如下访问请求

请求的路径为 /

使用 get 操作调用

同样可以使用 @app.post () 、 @app.put () 、 @app.delete ()

async def root():

定义一个 Python 函数,函数名为 root

async 表示 这是一个 异步函数

return {"message": "Hello World"}

返回内容

## 执行文件#

uvicorn main:app --reload

执行成功


image

# 路径参数#

## 路径中传入参数#

@app.get("/items/{item_id}")
async def read_item(item_id):
    return {"item_id": item_id}

{item_id} 表示这是一个可以传入的参数

此时请求 localhost:8000/items/1

会返回

{ "item_id": "1"}

## 限制参数类型#

同时,可以给这个参数限制类型

@app.get("/items/{item_id}")
async def read_item(item_id:int):
    return {"item_id": item_id}

只需要在函数入参的后面加上 : 类型

此时如果传入一个字符串,将会返回 422 错误


image

类型可以使用 strfloatbool 等等


注意!

需要注意顺序问题

比如:

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/me")
async def read_user_me():
    return {"user_id": "the current user"}


@app.get("/users/{user_id}")
async def read_user(user_id: str):
    return {"user_id": user_id}

现在有两个接口,如果 /users/{user_id} 在 /users/me 前面

由于_路径操作_是按顺序依次运行的

那当请求 /users/me 的时候,会执行到 /users/{user_id} 接口,返回 “me” 字符串

所以需要确保 /users/me 在 /users/{user_id} 的 前面


## 枚举类型预设值#

### 创建枚举类#

from fastapi import FastAPI
# 引入枚举类型
from enum import Enum

# 创建枚举类
class ModelNameEnum(str, Enum):
      fire = "fire"
      peach = "peach"
      firepeach = "firepeach"

app = FastAPI()


@app.get("/") 
async def root():
    return {"message": "Hello World"}

@app.get("/items/{item_id}") 
async def read_item(item_id:str): 
	return {"item_id": item_id}

@app.get("/models/{model_name}")
async def get_model(model_name: ModelNameEnum):
    # 通过枚举类的值来判断
    if model_name is ModelNameEnum.fire:
        return {"model_name": model_name, "message": "Fire Fire Fire"}
    if model_name.value == "peach":
        return {"model_name": model_name, "message": "Peach Peach Peach"}
    return {"model_name": model_name, "message": "FirePeach FirePeach FirePeach"}

代码解释:

async def get_model(model_name: ModelNameEnum):

定义的入参 model_name 是 ModelNameEnum 枚举类型

if model_name is ModelNameEnum.fire:

这里的 is 和 == 的作用是一样的,用来和 枚举成员 进行比较

但是使用 == 的时候,需要用入参 .value 来获取值

model_name.value

### 测试请求#


image

## 文件路径作为入参#

如果需要传入的入参是一个路径

可以使用

/files/{file_path:path}

作为请求路径

@app.get("/files/{file_path:path}") 
async def read_file(file_path: str): 
	return {"file_path": file_path}

当需要传入 /home/test/text.txt 路径的时候

请求的入参为 localhost:8000/files//home/test/text.txt 注意为 两个 /


image

# 查询参数#

## 默认值#

声明不属于路径参数的其他函数参数时,它们将被自动解释为 "查询字符串" 参数

比如下面的代码

from fastapi import FastAPI

app = FastAPI()

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
    return fake_items_db[skip : skip + limit]

在函数入参中

async def read_item(skip: int = 0, limit: int = 10):

定义了默认值 skip 默认为 0,limit 默认为 10

所以

传入的参数就是 skip = 0 , limit = 10

传入的参数就是 skip = 1 , limit = 10,因为 limit 不传时,默认为 10

传入的参数就是 skip = 0 , limit = 1,skip 不传默认为 0

## 可选值#

需要引入 Union

from typing import Union
@app.get("/items/{item_id}")
async def read_itemUnion(item_id: str, q: Union[str, None] = None):
    if q:
        return {"item_id": item_id, "q": q}
    return {"item_id": item_id}

此时,q 就是可传可不穿的参数


image

还可以加入 bool 类型的入参

@app.get("/items/{item_id}")
async def read_itemUnion(item_id: str, q: Union[str, None] = None, short: bool = False):
    item = {"item_id": item_id}
    if q:
        # 将 q 作为 item 的一个键值对,update 可以将新的键值对合并进去
        item.update({"q": q})
    if not short:
         item.update({"description": "This is an amazing item that has a long description"})
    return item

使用 update ({"XX" : "XX"}),可以在原有的键值对中新增这个键值对拼接上去

测试访问


image


image

## 多个路径和查询参数#

@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(
    user_id: int, item_id: str, q: Union[str, None] = None, short: bool = False
):
    item = {"item_id": item_id, "owner_id": user_id}
    if q:
        item.update({"q": q})
    if not short:
        item.update(
            {"description": "This is an amazing item that has a long description"}
        )
    return item

可以支持多个 参数 的请求


image

## 必须的查询参数#

如果在非路径的参数中指定了一个没有默认值的参数,那么这个参数就是必传的

也就是说 不是在 @app.get (/XXX/{ XXX}) 中指定的,是在函数的入参中指定的

而且没有配置 Union[str, None] = None , 那么这个参数就是必传的

@app.get("/items/{item_id}") 
async def read_user_item(item_id: str, needy: str): 
item = {"item_id": item_id, "needy": needy} 
	return item

比如这里的 needy 参数就是必传


image

# 请求体#

当你需要将数据从客户端(例如浏览器)发送给 API 时,你将其作为「请求体」发送。

请求体是客户端发送给 API 的数据。响应体是 API 发送给客户端的数据。

你的 API 几乎总是要发送响应体。但是客户端并不总是需要发送请求体。

## 导入 BaseModel#

from pydantic import BaseModel

需要用到 pydantic 模型来声明请求体

## 创建数据模型#

Class Item(BaseModel):
	name: str
	description: Union[str,None] = None
	price: float
	tax: Union[float,None] = None

这时候就声明了一个名称为 Item 的请求体的结构类型

{
    "name": "Foo",
    "description": "An optional description",
    "price": 45.2,
    "tax": 3.5
}

接着添加一个 post 方法,就可以指定入参为 Item 模型类型

@app.post("/items/") 
async def create_item(item: Item): 
	return item

测试请求


image

## 使用模型#

在函数的内部,可以直接访问模型对象的所有属性

但是需要通过 模型.dict () 获取模型的 字典 才可以进行修改

模型.dict () 就是模型中的每一个属性单独拎出来

@app.post("/items/") 
async def create_item(item: Item): 
	item_dict = item.dict() 
	if item.tax: 
		price_with_tax = item.price + item.tax 
		item_dict.update({"price_with_tax": price_with_tax}) 
	return item_dict


image

## 请求体加路径参数#

@app.put("/items/{item_id}")
async def create_item2(item_id: int, item: Item):
    return {"item_id": item_id, "item":item, **item.dict()}

可以同时传入路径参数和请求体

注意返回 item.dict () 的时候要加上 **


image

可以看出 如果返回 item.dict () 就是把模型中的每一个属性,单独的拎出来

## 请求体、路径参数加查询参数#

@app.put("/items/{item_id}")
async def create_item(item_id: int, item: Item, q: Union[str, None] = None):
    result = {"item_id": item_id, **item.dict()}
    if q:
        result.update({"q": q})
    return result


image

# 参数校验#

查询参数校验#

首先需要导入 Query

from fastapi import FastAPI, Query

### 长度校验#

@app.get("/items/")
async def read_items(q: Union[str, None] = Query(default=None, min_length=2, max_length=5)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

可以在参数后加上 Query ()

由于必须用 Query(default=None) 替换默认值 NoneQuery 的第一个参数同样也是用于定义默认值。

上面的代码表示 q 这个字段 最小长度为 2 最大长度为 5


image

### 正则表达式校验#

@app.get("/items/")
async def read_items(q: Union[str, None] = Query(default=None, min_length=2, max_length=5,regex='^query$')):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
regex='^query$'

上面的代码指定了正则表达式, q 的内容必须为 query


image

### 默认值#

同时默认值也可以是指定的

default = 'query'


image

## 路径参数的校验#

需要引入 Path

from fastapi import FastAPI,Query,Path

添加方法

from fastapi import FastAPI, Path, Query

app = FastAPI()


@app.get("/items/{item_id}")
async def read_items(
    *,
    item_id: int = Path(title="The ID of the item to get", ge=0, le=1000),
    q: str,
    size: float = Query(gt=0, lt=10.5),
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

@app.get("/items4/{item_id}")
async def read_items6(

    *,
    item_id: int = Path(title="The ID of the item to get", ge=0, le=1000),
    q: str,
    size: float = Query(gt=0, lt=10.5),
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results
  • gt:大于(greater than)
  • ge:大于等于(greater than or equal)
  • lt:小于(less than)
  • le:小于等于(less than or equal)

传递 * 作为函数的第一个参数。

Python 不会对该 * 做任何事情,但是它将知道之后的所有参数都应作为关键字参数(键值对),也被称为 kwargs,来调用。即使它们没有默认值。

也就是说,如果不加 * 作为第一个参数的话, 函数入参里不能只填写 q: str
因为他没有默认值


# 请求体中多个 Model 参数#

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None


class User(BaseModel):
    username: str
    full_name: Union[str, None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User):
    results = {"item_id": item_id, "item": item, "user": user}
    return results

之后的入参就可以使用下面这种形式

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    }
}

## 请求体中的单一值#

如果想在同一个请求体里面在新增一个键值对

比如

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    },
    "key": "key"
}

那么可以引入 Body

from fastapi import FastAPI,Query,Path,Body
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User, key: str = Body()):
    results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
    return results

通过添加下面代码来实现

key: str = Body()

同样的 Body () 中也可以和 Query、Path 一样添加校验

key: str = Body(gt=1)

测试接口


image

## 嵌入单个请求体参数#

如果期望实现的是下面这种请求方式

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    }
}

而不是

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2
}

则可以使用 Body () 的 embed 参数

比如

item: Item = Body(embed=True)
from typing import Union

from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item = Body(embed=True)):
    results = {"item_id": item_id, "item": item}
    return results

# 请求体中的嵌套模型#

## 具有子类型的 List 字段#

首先引入 List

from typing import List, Union

比如需要传入多个标签

可以使用下面代码来表示 List

# 限制内容的类型,只能传入 str 类型
tags: List[str] = []

# 不限制内容的类型,str、int等等都可以
tags:List =[]

代码如下

from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: List[str] = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results


image

## Set 类型#

和 List 不同的是,Set 中的元素不可以重复,所以用于存放标签会更合适

因为重复的元素会被自动合并

一样需要从 typing 中引入 Set

from typing import Set

使用代码

tags: Set[str] = set()

代码如下

class Message(BaseModel):
    role:str
    content:str
    tags:Set[str] = set()
  
@app.post("/chat")
async def chat(message: Message, model: str = Body()):
    return {"message":message,"model":model}


image

## 嵌套模型#

可以在模型中,嵌套另一个模型

例如

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": ["rock", "metal", "bar"],
    "image": {
        "url": "http://example.com/baz.jpg",
        "name": "The Foo live"
    }
}

先定义子模型

class Image(BaseModel): 
	url: str 
	name: str

接着添加到原来的模型中

class Message(BaseModel):
    role:str
    content:str
    tags:Set[str] = set()
    
    image: Union[Image,None] = None


image

## 特殊的类型和校验#

除了普通的单一值类型(如 strintfloat 等)外,你还可以使用从 str 继承的更复杂的单一值类型。

外部的特殊类型,可以通过 Pydantic 引入,具体可以访问 Pydantic 官网

比如 image 中的 URL 应该是个地址,相对应的 Pydantic 提供了 HttpUrl 的类型用来校验

同样的先引入

from pydantic import BaseModel, HttpUrl

接着修改 image 模型类

class Image(BaseModel):

    url: HttpUrl
    
    name: str

此时如果 URL 传入的不是 http 或者 https 开头的地址,都会报错


image

## 带有一组子模型的属性#

同样的,还可以将 Pydantic 模型用作 listset 等的子类型

比如

class Message(BaseModel):
    role:str
    content:str
    tags:Set[str] = set()

    image: Union[List[Image],None] = None


image

# 响应模型#

## 用户注册响应#

可以在任意的_路径操作_中使用 response_model 参数来声明用于响应的模型:

比如用户注册,注册成功后返回用户数据

但是密码是不能直接返回的,所以可以设置一个没有密码的响应模型

用户返回 定的模型的 出参

首先引入 Any 类型 和 EmailStr

from typing import Any

from pydantic import EmailStr

定义注册用户的模型

class User(BaseModel):
	username:str
	full_name: Union[str,None] = None
	email: EmailStr
	password: str

定义注册成功后返回的响应模型

class UserOut(BaseModel):
	username: str
	full_name: Union[str,None] = None
	email: EmailStr

实现代码如下

@app.post("/user/" response_model= UserOut):
async def create_user(user User) -> Any:
	return user

这里的 Any 是指 返回的 user 可以是任何类型

测试接口


image

## 预设模型响应#

首先定义一个相应模型

class Item2(BaseModel):
    name: str
    description: Union[str,None] = None
    price: float
    tax: float = 10
    tags: List[str] = []

接着设置默认的返回

item2s = {
    "fire": {"name": "Fire", "price": 3.2},
    "peach": {"name": "Peach", "description": "Peach Peach Peach", "price": 2.2,"tags":["aa","bb","cc"]},
    "firepeach": {"name": "firepeach", "description": "firepeach firepeach firepeach", "price": 2.2}
}

接口实现如下

@app.get("/item2s/{item_id}" response_model=Item2)
async def read_item2(item_id: str):
	return item2s[item_id]

此时请求入参发送 item_id 就会返回 item2s 中对应的 item_id 的那条数据

测试调用


image

如果不想返回那些带有默认值的参数

可以添加一个参数  response_model_exclude_unset=True

修改代码如下

@app.get("/item2s/{item_id}", response_model=Item2 , response_model_exclude_unset=True)
async def read_item2(item_id: str):
    return item2s[item_id]

此时接口调用,将不会返回 数据内容为默认值,或则内容为空 的数据


image

与默认值不同的仍然会返回


image

# 额外的模型#

保存用户数据的时候,需要新增一个存入数据库的模型

因为密码不能通过明文去保存,例如可以用 hash 值去保存

新增 UserDB 模型

class UserDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: Union[str,None] = None

定义一个假的密码加密函数

def fake_password_hasher(raw_password: str):
    return "hashpassword" + raw_password

定义一个拼接 UserDB 模型类型的 数据 并保存到数据库 的函数

def fake_save_user(user_in: User):
    fake_hash_password = fake_password_hasher(user_in.password);
    # 可以使用 ** 将一个字典解包成关键字参数
    # 然后将解包好的 user_in 作为关键字参数传入 user_db
    # 并将密码加密成 hashed_password 一起赋予 user_db
    user_db = UserDB(**user_in.dict(), hashed_password=fake_hash_password);

    print("用户假的保存成功!");
    return user_db```

新增一个注册用户的接口

``` python
@app.post("/user2/" response_model=UserOut)
async def create_user2(user: User):
	user_save = fake_save_user(user)
	return user_save

测试接口


image

## 关于 **user.dict() #

### Pydantic 模型#

user 是一个 User 类的 Pydantic 模型.

Pydantic 模型具有 .dict() 方法,该方法返回一个拥有模型数据的 dict (字典)。

所以如果创建一个 User 模型的对象是

user_in = UserIn(username="john", password="secret", email="[email protected]")

如果 user.dict ()

user_dict = user.dict()

user_dict 的数据则是

{
    'username': 'john',
    'password': 'secret',
    'email': '[email protected]',
    'full_name': None,
}

也就是所有的属性都被单独出来

### 解包 dict#

如果将 user_dict 这样的 dict 以 **user_dict 形式传递给一个函数(或类),Python 将对其进行「解包」。它会将 user_dict 的键和值作为关键字参数直接传递。

所以如果使用以下代码

UserDB(**user_dict)
# 或者
UserDB(**user.dict())

此时我们得到的 UserDB 则是

UserDB(
	'username': 'john',
    'password': 'secret',
    'email': '[email protected]',
    'full_name': None,
)

或者更确切地,直接使用 user_dict 来表示将来可能包含的任何内容:

UserDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
)

然后添加额外的关键字参数 hashed_password=hashed_password,例如:

UserDB(**user.dict(),fake_hashed_password = fake_hashed_password)

最终的结果就会是

UserDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
    hashed_password = hashed_password,
)

## 关于代码重复#

代码重复会增加出现 bug、安全性问题、代码失步问题(当你在一个位置更新了代码但没有在其他位置更新)等的可能性。

上面的这些模型都共享了大量数据,并拥有重复的属性名称和类型。

因此,基于上面提到的 dict 和 解包,可以直接定义一个 UserBase 的基类

class UserBase(BaseModel):
	username: str
	email: EmailStr
	full_name: Union[str,None] = None

而其他的模型只需要在此基础上,只赋予新增的属性

例如

class User(BaseModel):
    password: str


class UserDB(BaseModel):
    hashed_password: str


class UserOut(BaseModel):
    # pass 代表不做任何处理,保持和 UserBase 一致
    pass

因为后面接口的使用中基本所有的字段都会有,只需要解决新增的字段

## Union 模型#

可以将一个响应声明为两种类型的 Union,这意味着该响应将是两种类型中的任何一种。

这将在文档中表示为 anyOf

例如

 response_model=Union[PlaneItem, CarItem]

代码如下

from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class BaseItem(BaseModel):
    description: str
    type: str

class CarItem(BaseItem):
    type = "car"

class PlaneItem(BaseItem):
    type = "plane"
    size: int

items = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "plane",
        "size": 5,
    },
}

@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
    return items[item_id]

## 模型列表#

可以用同样的方式声明由对象列表构成的响应。

response_model=List[Item]

代码如下

from typing import List
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@app.get("/items/", response_model=List[Item])
async def read_items():
    return items

# 响应状态码#

可以通过 status_code 来指定状态码

例如

@app.post("/user2/", response_model=UserOut,status_code=201)
async def create_user2(user: User):
    user_saved = fake_save_user(user);
    return user_saved;

成功返回时,将返回 201 的状态码

同时 fastapi 也有提供便捷的变量

引入

from fastapi import status
@app.post("/user2/", response_model=UserOut,status_code=status.HTTP_201_CREATED)
async def create_user2(user: User):
    user_saved = fake_save_user(user);
    return user_saved;

常用的状态码有


image

有关每个状态代码以及适用场景的更多信息可以参考 HTTP 响应状态码 - HTTP | MDN

# 表单数据#

与 JSON 不同,HTML 表单(<form></form>)向服务器发送数据通常使用「特殊」的编码。

接收的不是 JSON,而是表单字段时,要使用 Form

要使用表单,需预先安装 python-multipart

执行

pip install python-multipart

导入 fastapi 提供的 Form

from fastapi import FastAPI, Form
app = FastAPI()

创建表单(Form)参数的方式与 Body 和 Query 一样

@app.post("/login/")
async def login(username: str = Form(), password: str = Form()):
    return {"username": username}

例如,OAuth2 规范的 "密码流" 模式规定要通过表单字段发送 username 和 password

该规范要求字段必须命名为 username 和 password,并通过表单字段发送,不能用 JSON。

使用 Form 可以声明与 Body (及 QueryPathCookie)相同的元数据和验证。

表单数据的「媒体类型」编码一般为 `application/x-www-form-urlencoded`。

但包含文件的表单编码为 `multipart/form-data`。

# 请求文件#

# 处理错误#

某些情况下,需要向客户端返回错误提示。

这里所谓的客户端包括前端浏览器、其他应用程序、物联网设备等。

需要向客户端返回错误提示的场景主要如下:

  • 客户端没有执行操作的权限
  • 客户端没有访问资源的权限
  • 客户端要访问的项目不存在
  • 等等 ...

遇到这些情况时,通常要返回 4XX(400 至 499)HTTP 状态码

4XX 状态码与表示请求成功的 2XX(200 至 299) HTTP 状态码类似。

只不过,4XX 状态码表示客户端发生的错误。

## HTTPException#

向客户端返回 HTTP 错误响应,可以使用 HTTPException

from fastapi import HTTPException

例如

@app.get("/item2s/{item_id}", response_model=Item2 , response_model_exclude_unset=True)
async def read_item2(item_id: str):
    if item_id not in item2s:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND , detail="没这个文件")
    return item2s[item_id]

测试调用


image

## 安装自定义异常处理器#

添加自定义处理器,要使用 Starlette 的异常工具

假设要触发的自定义异常叫作 UnicornException

且需要 FastAPI 实现全局处理该异常。

此时,可以用 @app.exception_handler() 添加自定义异常控制器:

也可以直接引入 fastapi 封装好的

from fastapi import  Request
from fastapi.response import JSONResponse

代码如下

class UnicornException(Exception):
    def __init__(self, name: str):
        self.name = name

@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
    return JSONResponse(
        status_code=418,
	    content={"message": f"哈哈! {exc.name} 这里出错了..."},
    )

@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
    if name == "yolo":
        raise UnicornException(name="自定义错误名")
    return {"unicorn_name": name}
{"message": f"哈哈! {exc.name} 这里出错了..."} 中的 f
可以获取自定义错误名

测试结果


image

# JSON 兼容编码器 与 更新数据#

在某些情况下,您可能需要将数据类型(如 Pydantic 模型)转换为与 JSON 兼容的数据类型(如dictlist等)。

比如,如果您需要将其存储在数据库中。

对于这种要求, FastAPI提供了jsonable_encoder()函数。

## 使用 jsonable_encoder () 更新数据#

假设有一个数据库名为fake_db,它只能接收与 JSON 兼容的数据。

例如,它不接收datetime这类的对象,因为这些对象与 JSON 不兼容。

因此,datetime对象必须将转换为包含ISO 格式化str类型对象。

同样,这个数据库也不会接收 Pydantic 模型(带有属性的对象),而只接收dict

对此可以使用 jsonable_encoder

更新数据请用 HTTP PUT 操作。

它接收一个对象,比如 Pydantic 模型,并会返回一个 JSON 兼容的版本:

把输入数据转换为以 JSON 格式存储的数据(比如,使用 NoSQL 数据库时),可以使用 jsonable_encoder。例如,把 datetime 转换为 str

from datetime import datetime

from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel

# 假的数据库
fake_db = {
    "fee": {"title": "foo", "timestamp": datetime.now()}
}

# 定义一个ItemJ 模型
class ItemJ(BaseModel):
    title: str
    timestamp: datetime
    description: str | None = None

# put 用来更新
@app.put("/itemsJ/{id}")
def update_itemJ(id: str, item: ItemJ):
	# 将时间传唤成 str 类型的 Json格式
    json_compatible_item_data = jsonable_encoder(item)
    fake_db[id] = json_compatible_item_data
    return fake_db[id]

测试调用


image

# 安全性#

假设后端 API 在某个域。

前端在另一个域,或(移动应用中)在同一个域的不同路径下。

并且,前端要使用后端的 username 与 password 验证用户身份。

固然,FastAPI 支持 OAuth2 身份验证。

## 概览#

引入 Depends 依赖 和 OAuth2PasswordBearer 身份认证

from fastapi import Depends, FastAPI 
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/oauth/")
async def read_items(token: str = Depends(oauth2_scheme)):
    return {"token": token}

此时在 api 文档 localhost:8000/docs 中可以看到

页面右上角出现了一个「Authorize」按钮。

_路径操作_的右上角也出现了一个可以点击的小锁图标。

点击 Authorize 按钮,弹出授权表单,输入 username 与 password 及其它可选字段:

如果没有,需要安装 python-multipart

执行 pip install python-multipart

是因为 OAuth2 使用 表单数据 发送 `username` 与 `password`。


image


image

### 密码流#

Password 是 OAuth2 定义的,用于处理安全与身份验证的方式()。

OAuth2 的设计目标是为了让后端或 API 独立于服务器验证用户身份。

但在本例中,FastAPI 应用会处理 API 与身份验证。

下面是简化的运行流程:

  • 用户在前端输入 username 与password,并点击回车
  • (用户浏览器中运行的)前端把 username 与password 发送至 API 中指定的 URL(使用 tokenUrl="token" 声明)
  • API 检查 username 与password,并用令牌(Token) 响应(暂未实现此功能):
  • 令牌只是用于验证用户的字符串
  • 一般来说,令牌会在一段时间后过期
    • 过时后,用户要再次登录
    • 这样一来,就算令牌被人窃取,风险也较低。因为它与永久密钥不同,在绝大多数情况下不会长期有效
  • 前端临时将令牌存储在某个位置
  • 用户点击前端,前往前端应用的其它部件
  • 前端需要从 API 中提取更多数据:
    • 为指定的端点(Endpoint)进行身份验证
    • 因此,用 API 验证身份时,要发送值为 Bearer + 令牌的请求头 Authorization
    • 假如令牌为 foobarAuthorization 请求头就是: Bearer foobar

### OAuth2PasswordBearer#

FastAPI 提供了不同抽象级别的安全工具。

本例使用 OAuth2 的 Password 流以及 Bearer 令牌(Token

为此要使用 OAuth2PasswordBearer 类。

创建 OAuth2PasswordBearer 的类实例时,要传递 tokenUrl 参数。该参数包含客户端(用户浏览器中运行的前端) 的 URL,用于发送 username 与 password,并获取令牌。

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

在此,tokenUrl="token" 指向的是暂未创建的相对 URL token。这个相对 URL 相当于 ./token

因为使用的是相对 URL,如果 API 位于 https://example.com/,则指向 https://example.com/token。但如果 API 位于 https://example.com/api/v1/,它指向的就是https://example.com/api/v1/token

使用相对 URL 非常重要,可以确保应用在遇到 使用代理这样的高级用例时,也能正常运行

### 使用#

oauth2_scheme 变量是 OAuth2PasswordBearer 的实例,也是可调用项

调用方式为

oauth2_scheme(some, parameters)

因此,Depends (依赖) 可以调用 oauth2_scheme 变量。

接下来,使用 Depends 把 oauth2_scheme 传入依赖项。

@app.get("/items/") 
async def read_items(token: str = Depends(oauth2_scheme)): 
	return {"token": token}

该依赖项使用字符串(str)接收_路径操作函数_的参数 token

## 获取当前的用户#

### 创建用户模型#

class UserAuth(BaseModel):
    username: str
    # 先使用 Union[str, None],后面会改成 EmailStr
    email: Union[str, None] = None
    full_name: Union[str, None] = None
    disabled: Union[bool, None] = None

### 创建依赖项#

创建一个 get_current_user 依赖项。

get_current_user 将具有一个之前所创建的同一个 oauth2_scheme 作为依赖项。

与之前直接在路径操作中所做的相同,新的依赖项 get_current_user 将从子依赖项 oauth2_scheme 中接收一个 str 类型的 token

async def get_current_user(token: str = Depends(oauth2_scheme)): 
	user = fake_decode_token(token) 
	return user

### 获取用户信息#

将从 oauth2_scheme 获取到的 token 用来获取用户

get_current_user 将使用创建的(伪)工具函数 fake_decode_token ,该函数接收 str 类型的令牌并返回我们的 Pydantic User 模型

def fake_decode_token(token): 
	return User( username=token + "fakedecoded", email="[email protected]", full_name="John Doe" )

### 注入当前用户#

现在 可以在 路径操作 中使用 get_current_user 作为 Depends 了:

@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
    return current_user

## 使用密码和 Bearer 的简单 OAuth2#

### 概述#

现在将使用 FastAPI 的安全性实用工具来获取 username 和 password

OAuth2 规定在使用「password 流程」时,客户端 / 用户必须将 username 和 password 字段作为表单数据发送。

而且规范明确了字段必须这样命名。因此 user-name 或 email 是行不通的。

不过数据库模型也可以使用任何其他名称。

但是对于登录 路径操作,需要使用这些名称来与规范兼容(以具备例如使用集成的 API 文档系统的能力)。

规范还写明了 username 和 password 必须作为表单数据发送(因此,此处不能使用 JSON)。

### 获取 username 和 password#

#

需要用到 OAuth2PasswordRequestForm

首先引入

from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):

    user_dict = fake_users_db.get(form_data.username)
	# 如果 user_dict 不存在 
    if not user_dict:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

	# 将 User 的数据 传给 UserDB 模型类
    user = UserDB(**user_dict)
    # 将密码 hash 化
    hashed_password = fake_hash_password(form_data.password)

	# 如果 密码不正确
    if not hashed_password == user.hashed_password:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

	# 返回 用户名 token的类型
    return {"access_token": user.username, "token_type": "bearer"}

OAuth2PasswordRequestForm 是一个类依赖项,声明了如下的请求表单:

  • username
  • password
  • 一个可选的 scope 字段,是一个由空格分隔的字符串组成的大字符串。
  • 一个可选的 grant_type.

#### 密码 哈希 化#

「哈希」的意思是:将某些内容(在本例中为密码)转换为看起来像乱码的字节序列(只是一个字符串)。

每次你传入完全相同的内容(完全相同的密码)时,你都会得到完全相同的乱码。

但是你不能从乱码转换回密码。

    # 将密码 hash 化
    hashed_password = fake_hash_password(form_data.password)

	# 如果 密码不正确
    if not hashed_password == user.hashed_password:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

#### 返回 Token#

token 端点的响应必须是一个 JSON 对象。

它应该有个 token_type。在例子中,使用的是「Bearer」令牌,因此令牌类型应为「bearer」。

并且还应该有一个 access_token 字段,它是一个包含我们的访问令牌的字符串。

对于这个简单的示例,我们将极其不安全地返回相同的 username 作为令牌。

	# 返回 用户名 token的类型
    return {"access_token": user.username, "token_type": "bearer"}

#### 更新依赖项#

现在将更新依赖项。

我们想要仅当此用户处于启用状态时才能获取 current_user

因此,创建了一个额外的依赖项 get_current_active_user,而该依赖项又以 get_current_user 作为依赖项。

如果用户不存在或处于未启用状态,则这两个依赖项都将仅返回 HTTP 错误。

因此,在我们的端点中,只有当用户存在,身份认证通过且处于启用状态时,我们才能获得该用户:

# 通过 token 获取用户信息
async def get_current_user(token: str = Depends(oauth2_scheme)):
    user = fake_decode_token(token)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return user

# 获取当前活跃的用户,登陆了之后就活跃了
# 身份认证通过且处于启用状态时,才能获得此用户
async def get_current_active_user(current_user: UserAuth = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user

#### 创建虚假的数据库#

# 模拟数据库

fake_users_db = {
    "bryce": {
        "username": "bryce",
        "full_name": "bryce Yu",
        "email": "[email protected]",
        "hashed_password": "hash123123",
        "disabled": False,
    },

    "yu": {
        "username": "yu",
        "full_name": "yu",
        "email": "[email protected]",
        "hashed_password": "hash123456",
        "disabled": True,
    },
}

### 完整代码 (重点!!!!)#

完整的用户登陆到获取当前用户的代码如下

# 创建一个 用户 模型类
class UserAuth(BaseModel):
    username: str
    # 先使用 Union[str, None],后面会改成 EmailStr
    email: Union[str, None] = None
    full_name: Union[str, None] = None
    disabled: Union[bool, None] = None

#  创建一个 用户 数据库 模型类
class UserDB(UserAuth):
    hashed_password: str

# 模拟将密码哈希化
def fake_hash_password(raw_password: str):
    # 模拟哈希化 密码,实际上应该使用 bcrypt 或者其他哈希算法
    return "hash" + raw_password

# 通过 token 获取用户信息
def fake_decode_token(token):
    # 通过 token 获取用户信息,现在不安全的使用 用户名 作为 token
    user = get_user(fake_users_db, token)
    return user

# 从 db 数据库中,获取用户名为 username 的用户信息
def get_user(db, username: str):
    # 现在不安全的使用 用户名 作为 token
    if username in db:
        user_dict = db[username]
        return UserDB(**user_dict)



# 先通过通过 oauth2_scheme 获得token ,在使用 token 获取当前登陆的用户信息
# oauth2_scheme 是一个认证方案,它会从请求头中获取 Authorization 字段的值,然后将其解析成 token
async def get_current_user(token: str = Depends(oauth2_scheme)):
    # 通过token 获取用户信息
    user = fake_decode_token(token)
    # 如果用户不存在
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="无效的认证凭证",
            headers={"WWW-Authenticate": "Bearer"},
        )
    # 如果用户存在,返回用户信息
    return user


# 获取当前活跃的用户
async def get_current_active_user(current_user: UserAuth = Depends(get_current_user)):
    # 通过 get_current_user 获取当前登陆的用户 current_user
    # 接着判断用户是否被禁用
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="用户被禁用")
    return current_user


# 通过表单 登陆用户
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):

    # 通过用户名 到 模拟数据库中获取用户
    user_dict = fake_users_db.get(form_data.username)

	# 如果 user_dict 不存在 
    if not user_dict:
        raise HTTPException(status_code=400, detail="用户名或密码错误")

	# 将 User 的数据 传给 UserDB 模型类,获取 UserDB 的实例用来获取密码进行比较
    user = UserDB(**user_dict)

    # 将密码 hash 化,用来和数据库中的密码进行比较
    hashed_password = fake_hash_password(form_data.password)

	# 如果 密码不正确
    if not hashed_password == user.hashed_password:
        raise HTTPException(status_code=400, detail="用户名或密码错误")
    
	# 返回 token 和 token的类型,现在不安全的使用 用户名 作为 token
    return {"access_token": user.username, "token_type": "bearer"}


# 调用这个接口的时候,需要在 headers 中添加 Authorization: Bearer token,token 就是登陆时返回的 token
# 当使用 openApi 测试的时候,会自动在 headers 中添加 Authorization: Bearer token
# 而实际的请求中,需要手动添加 Authorization: Bearer token
@app.get("/users/me")
async def read_users_me(current_user: UserAuth = Depends(get_current_active_user)):
    # 通过 get_current_active_user 获取当前登陆并且没有被禁用的用户 current_user
    return current_user

#### 流程总结#

首先调用 /token 接口,通过表单提交 username 和 password ,接口会返回一个 token

接着需要在 headers 中的 Authorization 中添加 Token 的值,调用 /user/me 获取当前登陆用户

因为本案例中使用的是 Bearer ,所以 Authorization 的值为 "Bearer" + 返回的 Token

而在 openApi 调用测试的时候,为了方便测试,会自动加上 Authorization 的 Token 值

在实际项目的使用中,需要手动添加 headers 中的 Authorization 值

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。