跳至内容

HTTP 基本认证

对于最简单的用例,您可以使用 HTTP 基本认证。

在 HTTP 基本认证中,应用程序期望一个包含用户名和密码的头部。

如果它没有收到,它将返回 HTTP 401“未授权”错误。

并返回一个头部 WWW-Authenticate,其值为 Basic,以及可选的 realm 参数。

这告诉浏览器显示用于用户名和密码的集成提示。

然后,当您输入该用户名和密码时,浏览器会自动将其发送到头部。

简单的 HTTP 基本认证

  • 导入 HTTPBasicHTTPBasicCredentials
  • 使用 HTTPBasic 创建一个“security 模式”。
  • 在您的路径操作中使用该 security 和依赖项。
  • 它返回一个类型为 HTTPBasicCredentials 的对象
    • 它包含发送的 usernamepassword
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}
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}

提示

如果可能,最好使用 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: HTTPBasicCredentials = Depends(security)):
    return {"username": credentials.username, "password": credentials.password}

当您第一次尝试打开 URL(或在文档中点击“执行”按钮)时,浏览器将要求您输入用户名和密码

检查用户名

这是一个更完整的示例。

使用依赖项检查用户名和密码是否正确。

为此,使用 Python 标准模块 secrets 来检查用户名和密码。

secrets.compare_digest() 需要接受 bytes 或仅包含 ASCII 字符(英语中的字符)的 str,这意味着它不适用于像 á 这样的字符,就像在 Sebastián 中一样。

为了处理这个问题,我们首先将 usernamepassword 转换为 bytes,并使用 UTF-8 对其进行编码。

然后我们可以使用 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}
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}

提示

如果可能,最好使用 Annotated 版本。

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 中的第一个 jstanleyjobson 中的第一个 s 进行比较的那一刻,它将返回 False,因为它已经知道这两个字符串不相同,认为“没有必要浪费更多计算来比较其余的字母”。并且您的应用程序将显示“用户名或密码不正确”。

但是,然后攻击者尝试使用用户名 stanleyjobsox 和密码 love123

并且您的应用程序代码执行以下操作:

if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
    ...

在意识到这两个字符串不相同之前,Python 必须比较 stanleyjobsoxstanleyjobson 中的整个 stanleyjobso。因此,它将需要一些额外的微秒才能回复“用户名或密码不正确”。

响应时间会帮助攻击者

此时,通过注意到服务器发送“用户名或密码错误”响应的时间稍微延长了几微秒,攻击者就会知道他们猜对了某些东西,一些初始字母是正确的。

然后他们可以再次尝试,知道它可能与stanleyjobsox更相似,而不是与johndoe相似。

“专业”攻击

当然,攻击者不会手动尝试所有这些,他们会编写一个程序来执行此操作,可能每秒进行数千或数百万次测试。并且他们每次只会获得一个额外的正确字母。

但是,通过这样做,在几分钟或几小时内,攻击者就可以猜测出正确的用户名和密码,借助我们应用程序的“帮助”,仅仅是利用了响应时间。

使用secrets.compare_digest()修复

但在我们的代码中,我们实际上使用了secrets.compare_digest()

简而言之,将stanleyjobsoxstanleyjobson进行比较所需的时间与将johndoestanleyjobson进行比较所需的时间相同。密码也是如此。

这样,在应用程序代码中使用secrets.compare_digest(),就可以安全地抵御这一系列的安全攻击。

返回错误

在检测到凭据不正确后,返回一个状态代码为 401 的HTTPException(与未提供凭据时返回的状态代码相同),并添加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}
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}

提示

如果可能,最好使用 Annotated 版本。

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}