HTTP 基础授权¶
最简单的用例是使用 HTTP 基础授权(HTTP Basic Auth)。
在 HTTP 基础授权中,应用需要请求头包含用户名与密码。
如果没有接收到 HTTP 基础授权,就返回 HTTP 401 "Unauthorized"
错误。
并返回含 Basic
值的请求头 WWW-Authenticate
以及可选的 realm
参数。
HTTP 基础授权让浏览器显示内置的用户名与密码提示。
输入用户名与密码后,浏览器会把它们自动发送至请求头。
简单的 HTTP 基础授权¶
- 导入
HTTPBasic
与HTTPBasicCredentials
- 使用
HTTPBasic
创建安全概图 - 在路径操作的依赖项中使用
security
- 返回类型为
HTTPBasicCredentials
的对象:- 包含发送的
username
与password
- 包含发送的
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
@app.get("/users/me")
def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
return {"username": credentials.username, "password": credentials.password}
🤓 Other versions and variants
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from typing_extensions import Annotated
app = FastAPI()
security = HTTPBasic()
@app.get("/users/me")
def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
return {"username": credentials.username, "password": credentials.password}
Tip
Prefer to use the Annotated
version if possible.
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
@app.get("/users/me")
def read_current_user(credentials: HTTPBasicCredentials = Depends(security)):
return {"username": credentials.username, "password": credentials.password}
第一次打开 URL(或在 API 文档中点击 Execute 按钮)时,浏览器要求输入用户名与密码:
检查用户名¶
以下是更完整的示例。
使用依赖项检查用户名与密码是否正确。
为此要使用 Python 标准模块 secrets
检查用户名与密码。
secrets.compare_digest()
需要仅包含 ASCII 字符(英语字符)的 bytes
或 str
,这意味着它不适用于像á
一样的字符,如 Sebastián
。
为了解决这个问题,我们首先将 username
和 password
转换为使用 UTF-8 编码的 bytes
。
然后我们可以使用 secrets.compare_digest()
来确保 credentials.username
是 "stanleyjobson"
,且 credentials.password
是"swordfish"
。
import secrets
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
def get_current_username(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
return {"username": username}
🤓 Other versions and variants
import secrets
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from typing_extensions import Annotated
app = FastAPI()
security = HTTPBasic()
def get_current_username(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
return {"username": username}
Tip
Prefer to use the Annotated
version if possible.
import secrets
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
return {"username": username}
这类似于:
if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"):
# Return some error
...
但使用 secrets.compare_digest()
,可以防御时差攻击,更加安全。
时差攻击¶
什么是时差攻击?
假设攻击者试图猜出用户名与密码。
他们发送用户名为 johndoe
,密码为 love123
的请求。
然后,Python 代码执行如下操作:
if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
...
但就在 Python 比较完 johndoe
的第一个字母 j
与 stanleyjobson
的 s
时,Python 就已经知道这两个字符串不相同了,它会这么想,没必要浪费更多时间执行剩余字母的对比计算了。应用立刻就会返回错误的用户或密码。
但接下来,攻击者继续尝试 stanleyjobsox
和 密码 love123
。
应用代码会执行类似下面的操作:
if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
...
此时,Python 要对比 stanleyjobsox
与 stanleyjobson
中的 stanleyjobso
,才能知道这两个字符串不一样。因此会多花费几微秒来返回错误的用户或密码。
反应时间对攻击者的帮助¶
通过服务器花费了更多微秒才发送错误的用户或密码响应,攻击者会知道猜对了一些内容,起码开头字母是正确的。
然后,他们就可以放弃 johndoe
,再用类似 stanleyjobsox
的内容进行尝试。
专业攻击¶
当然,攻击者不用手动操作,而是编写每秒能执行成千上万次测试的攻击程序,每次都会找到更多正确字符。
但是,在您的应用的帮助下,攻击者利用时间差,就能在几分钟或几小时内,以这种方式猜出正确的用户名和密码。
使用 secrets.compare_digest()
修补¶
在此,代码中使用了 secrets.compare_digest()
。
简单的说,它使用相同的时间对比 stanleyjobsox
和 stanleyjobson
,还有 johndoe
和 stanleyjobson
。对比密码时也一样。
在代码中使用 secrets.compare_digest()
,就可以安全地防御全面攻击了。
返回错误¶
检测到凭证不正确后,返回 HTTPException
及状态码 401(与无凭证时返回的内容一样),并添加请求头 WWW-Authenticate
,让浏览器再次显示登录提示:
import secrets
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
def get_current_username(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
return {"username": username}
🤓 Other versions and variants
import secrets
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from typing_extensions import Annotated
app = FastAPI()
security = HTTPBasic()
def get_current_username(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
return {"username": username}
Tip
Prefer to use the Annotated
version if possible.
import secrets
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
return {"username": username}