FastAPI 教程翻译 - 用户指南 26 - 安全性

FastAPI 教程翻译 - 用户指南 26 - 安全性

FastAPI Tutorial - User Guide - Secuity

Security Intro

安全性简介

There are many ways to handle security, authentication and authorization.

有许多方法可以处理安全性、身份验证和授权。

And it normally is a complex and “difficult” topic.

这通常是一个复杂而『困难』的话题。

In many frameworks and systems just handling security and authentication takes a big amount of effort and code (in many cases it can be 50% or more of all the code written).

在许多框架和系统中,仅处理安全性和身份验证就需要大量的精力和代码(在许多情况下,可能占编写的所有代码的50%或更多)。

FastAPI provides several tools to help you deal with Security easily, rapidly, in a standard way, without having to study and learn all the security specifications.

FastAPI 提供了多种工具,可帮助您以标准方式轻松、快速地处理安全性,而无需研究和学习所有安全规范。

But first, let’s check some small concepts.

但是首先,让我们熟悉一些小的概念。

In a hurry?

匆忙吗?

If you don’t care about any of these terms and you just need to add security with authentication based on username and password right now, skip to the next chapters.

如果您不关心这些术语中的任何一个,而只需要立即基于用户名和密码通过身份验证来增加安全性,请跳到下一章。

OAuth2

OAuth2 is a specification that defines several ways to handle authentication and authorization.

OAuth2 是一个规范,定义了几种处理身份验证和授权的方式。

It is quite an extensive specification and covers several complex use cases.

它是一个相当广泛的规范,涵盖了几个复杂的用例。

It includes ways to authenticate using a “third party”.

它包括使用『第三方』进行身份验证的方法。

That’s what all the systems with “login with Facebook, Google, Twitter, GitHub” use underneath.

这就是所有带有『使用 Fackbook、Google、Twitter、GitHub 登录』的系统的基础。

OAuth1

There was an OAuth1, which is very different from OAuth2, and more complex, as it included directly specifications on how to encrypt the communication.

有一个 OAuth1,它与 OAuth2 完全不同,并且更为复杂,因为它直接包含有关如何加密通信的规范。

It is not very popular or used nowadays.

它现在不是很流行或常用。

OAuth2 doesn’t specify how to encrypt the communication, it expects you to have your application served with HTTPS.

OAuth2 没有指定如何加密通信,它希望您使用 HTTPS 为您的应用程序提供服务。

Tip

提示

In the section about deployment you will see how to set up HTTPS for free, using Traefik and Let’s Encrypt.

在『部署』部分中,您将看到如何使用 Traefik 和 Let’s Encrypt 免费设置 HTTPS。

OpenID Connect

OpenID Connect is another specification, based on OAuth2.

OpenID Connect 是另一个基于 OAuth2 的规范。

It just extends OAuth2 specifying some things that are relatively ambiguous in OAuth2, to try to make it more interoperable.

它只是扩展了 OAuth2,以指定一些在 OAuth2 中相对模糊的内容,以尝试使其更具互操作性。

For example, Google login uses OpenID Connect (which underneath uses OAuth2).

例如,Google 登录使用 OpenID Connect(在下面使用 OAuth2)。

But Facebook login doesn’t support OpenID Connect. It has its own flavor of OAuth2.

但是,Facebook 登录不支持 OpenID Connect。它具有自己的 OAuth2 风格。

OpenID (not “OpenID Connect”)
OpenID(不是 OpenID Connect)

There was also an “OpenID” specification. That tried to solve the same thing as OpenID Connect, but was not based on OAuth2.

还有一个『OpenID』规范。它试图解决与 OpenID Connect 相同的问题,但不是基于 OAuth2.

So, it was a complete additional system.

因此,它是一个完整的附加系统。

It is not very popular or used nowadays.

它现在不是很流行或常用。

OpenAPI

OpenAPI (previously known as Swagger) is the open specification for building APIs (now part of the Linux Foundation).

OpenAPI(以前称为 Swagger)是用于构建 API(现已成为 Linux Foundation 的一部分)的开放规范。

FastAPI is based on OpenAPI.

FastAPI 是基于 OpenAPI

That’s what makes it possible to have multiple automatic interactive documentation interfaces, code generation, etc.

这就是使支持多个自动交互式文档界面和代码生成等成为可能的原因。

OpenAPI has a way to define multiple security “schemes”.

OpenAPI 具有定义多个安全『方案』的方法。

By using them, you can take advantage of all these standard-based tools, including these interactive documentation systems.

通过使用它们,您可以利用所有这些基于标准的工具,包括这些交互式文档系统。

OpenAPI defines the following security schemes:

OpenAPI 定义了以下安全方案:

  • apiKey: an application specific key that can come from:

    特定于应用程序的密钥,可以来自:

    • A query parameter.

      一个查询参数。

    • A header.

      一个 header。

  • A cookie.

    一个 cookie。

  • http: standard HTTP authentication systems, including:

    标准的 HTTP 身份验证系统,包括:

    • bearer: a header Authorization with a value of Bearer plus a token. This is inherited from OAuth2.

      bearer:一个 header 的 Authorization,其值为 Bearer 加一个令牌。这是从 OAuth2 继承的。

    • HTTP Basic authentication.

      HTTP 基本身份验证。

  • HTTP Digest, etc.

    HTTP 摘要等。

  • oauth2: all the OAuth2 ways to handle security (called “flows”).

    所有 OAuth2 处理安全性的方式(称为『流程』)。

    • Several of these flows are appropriate for building an OAuth 2.0 authentication provider (like Google, Facebook, Twitter, GitHub, etc):

      其中一些流程适用于构建 OAuth 2.0 身份验证提供程序(例如 Google、Facebook、Twitter、GitHub 等):

      • implicit
    • clientCredentials

      • authorizationCode
  • But there is one specific “flow” that can be perfectly used for handling authentication in the same application directly:

    但是有一个特定的『流程』可以完美地用于直接在同一应用程序中处理身份验证:

    • password: some next chapters will cover examples of this.

      password:接下来的几章将介绍该示例。

  • openIdConnect: has a way to define how to discover OAuth2 authentication data automatically.

    openIdConnect:具有定义自动发现 OAuth2 身份验证数据的方法。

  • This automatic discovery is what is defined in the OpenID Connect specification.

    此自动发现是 OpenID Connect 规范中定义的内容。

Tip

提示

Integrating other authentication/authorization providers like Google, Facebook, Twitter, GitHub, etc. is also possible and relatively easy.

也可以集成其他身份验证 / 授权提供商,例如 Google、Facebook、Twitter、GitHub 等,并且相对容易。

The most complex problem is building an authentication/authorization provider like those, but FastAPI gives you the tools to do it easily, while doing the heavy lifting for you.

最复杂的问题是建立像这样的身份验证 / 授权提供程序,但是 FastAPI 为您提供了轻松完成任务的工具,同时为您提供了重量级的能力。

FastAPI utilities

FastAPI 实用程序

FastAPI provides several tools for each of these security schemes in the fastapi.security module that simplify using these security mechanisms.

FastAPI 为 fastapi.security 模块中的每个安全方案提供了几种工具,这些工具简化了这些安全机制的使用。

In the next chapters you will see how to add security to your API using those tools provided by FastAPI.

在下一章中,您将看到如何使用 FastAPI 所提供的工具为您的 API 添加安全性。

And you will also see how it gets automatically integrated into the interactive documentation system.

您还将看到如何将其自动集成到交互式文档系统中。

First Steps

第一步

Let’s imagine that you have your backend API in some domain.

假设您在某个域中拥有后端 API。

And you have a frontend in another domain or in a different path of the same domain (or in a mobile application).

并且您在另一个域中或同一个域(或在移动应用程序中)的不同路径中有一个前端

And you want to have a way for the frontend to authenticate with the backend, using a username and password.

而且,您希望使用用户名密码来让前端与后端进行身份验证。

We can use OAuth2 to build that with FastAPI.

我们可以使用 OAuth2FastAPI 来构建它。

But let’s save you the time of reading the full long specification just to find those little pieces of information you need.

但是,让我们节省阅读完整的长期规范的时间,仅查找所需的那些小信息。

Let’s use the tools provided by FastAPI to handle security.

让我们使用 FastAPI 提供的工具来处理安全性。

How it looks

看起来如何

Let’s first just use the code and see how it works, and then we’ll come back to understand what’s happening.

首先,让我们使用代码并查看其工作方式,然后再回来了解发生了什么。

Create main.py

创建 main.py

Copy the example in a file main.py:

将示例复制到文件 main.py 中:

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


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

Run it

运行

Info

信息

First install python-multipart.

首先安装 python-multipart

E.g. pip install python-multipart.

例如 pip install python-multipart

This is because OAuth2 uses “form data” for sending the username and password.

这是因为 OAuth2 使用『表单数据』发送 usernamepassword

Run the example with:

使用以下示例运行:

uvicorn main:app --reload

Check it

检查

Go to the interactive docs at: http://127.0.0.1:8000/docs.

转到位于以下位置的交互式文档:http://127.0.0.1:8000/docs。

You will see something like this:

您将看到如下内容:

在这里插入图片描述

Authorize button!

授权按钮!

You already have a shinny new “Authorize” button.

您已经有了一个崭新的『授权』按钮。

And your path operation has a little lock in the top-right corner that you can click.

您的路径操作在右上角有一个小锁,您可以单击它。

And if you click it, you have a little authorization form to type a username and password (and other optional fields):

如果单击它,您会得到授权,可以输入 usernamepassword(以及其他可选字段):

在这里插入图片描述

Note

注意

It doesn’t matter what you type in the form, it won’t work yet. But we’ll get there.

您在表格中键入什么都无所谓,将不会使用。但是我们可以获得这些信息。

This is of course not the frontend for the final users, but it’s a great automatic tool to document interactively all your API.

当然,这不是最终用户的前端,但它是一个很好的自动工具,可以交互式地记录您所有的API。

It can be used by the frontend team (that can also be yourself).

前端团队(也可以是您自己)可以使用它。

It can be used by third party applications and systems.

第三方应用程序和系统可以使用它。

And it can also be used by yourself, to debug, check and test the same application.

您也可以自己使用它来调试、检查和测试同一应用程序。

The password flow

password 流程

Now let’s go back a bit and understand what is all that.

现在,让我们回到过去,了解所有内容。

The password “flow” is one of the ways (“flows”) defined in OAuth2, to handle security and authentication.

password 『流程』是 OAuth2 中定义的用于处理安全性和身份验证的一种方式(『流程』)。

OAuth2 was designed so that the backend or API could be independent of the server that authenticates the user.

OAuth2 的设计使后端或 API 可以独立于对用户进行身份验证的服务器。

But in this case, the same FastAPI application will handle the API and the authentication.

但是在这种情况下,相同的 FastAPI 应用程序将处理 API 和身份验证。

So, let’s review it from that simplified point of view:

因此,让我们从简化的角度进行回顾:

  • The user types his username and password in the frontend, and hits Enter.

    用户在前端输入 usernamepassword,然后按 Enter

  • The frontend (running in the user’s browser) sends that username and password to a specific URL in our API.

    前端(在用户浏览器中运行)将 usernamepassword 发送到我们 API 中的特定 URL。

  • The API checks that username and password, and responds with a “token”.

    API 检查 usernamepassword,并以『令牌』作为响应。

    • A “token” is just a string with some content that we can use later to verify this user.

      『令牌』只是一个包含某些内容的字符串,我们稍后可以使用它来验证此用户。

    • Normally, a token is set to expire after some time.

      通常,令牌设置为在一段时间后过期。

      • So, the user will have to login again at some point later.

        因此,用户稍后将不得不再次登录。

      • And if the token is stolen, the risk is less. It is not like a permanent key that will work forever (in most of the cases).

        如果令牌被盗,则风险较小。它不像将永远有效的永久密钥(在大多数情况下)。

  • The frontend stores that token temporarily somewhere.

    前端将该令牌临时存储在某个地方。

  • The user clicks in the frontend to go to another section of the frontend web app.

    用户单击前端以转到前端 Web 应用程序的另一部分。

  • The frontend needs to fetch some more data from the API.

    前端需要从 API 提取更多数据。

    • But it needs authentication for that specific endpoint.

      但是它需要对该特定端点进行身份验证。

    • So, to authenticate with our API, it sends a header Authorization with a value of Bearer plus the token.

      因此,为了通过我们的 API 进行身份验证,它会发送header Authorization,其值为 Bearer 加上令牌。

    • If the token contains foobar, the content of the Authorization header would be: Bearer foobar.

      如果令牌包含 foobar,则 Authorization header 的内容为:Bearer foobar

FastAPI’s OAuth2PasswordBearer

FastAPIOAuth2PasswordBearer

FastAPI provides several tools, at different levels of abstraction, to implement these security features.

FastAPI 提供了几种工具,它们在不同的抽象级别上可以实现这些安全功能。

In this example we are going to use OAuth2, with the Password flow, using a Bearer token.

在此示例中,我们将使用带有 Bearer 令牌的 OAuth2密码 流。

Info

信息

A “bearer” token is not the only option.

『bearer』令牌不是唯一的选择。

But it’s the best one for our use case.

但这是我们用例的最佳选择。

And it might be the best for most use cases, unless you are an OAuth2 expert and know exactly why there’s another option that suits better your needs.

对于大多数用例来说,这可能是最好的选择,除非您是 OAuth2 专家,并且确切知道为什么还有另一种选项可以更好地满足您的需求。

In that case, FastAPI also provides you with the tools to build it.

在这种情况下,FastAPI 还为您提供了构建它的工具。

OAuth2PasswordBearer is a class that we create passing a parameter of the URL in where the client (the frontend running in the user’s browser) can use to send the username and password and get a token.

OAuth2PasswordBearer 是一个我们创建的类,它传递 URL 的参数,客户端(在用户浏览器中运行的前端)可以在其中使用该参数发送 usernamepassword 并获取令牌。

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


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

It doesn’t create that endpoint / path operation, but declares that that URL is the one that the client should use to get the token. That information is used in OpenAPI, and then in the interactive API documentation systems.

它不会创建该端点 / 路径操作,而是声明该 URL 是客户端应用于获取令牌的 URL。该信息在 OpenAPI 中使用,然后在交互式 API 文档系统中使用。

Info

信息

If you are a very strict “Pythonista” you might dislike the style of the parameter name tokenUrl instead of token_url.

如果您是非常严格的『Pythonista』,则可能不喜欢参数名称 tokenUrl 而不是 token_url 的样式。

That’s because it is using the same name as in the OpenAPI spec. So that if you need to investigate more about any of these security schemes you can just copy and paste it to find more information about it.

这是因为它使用的名称与 OpenAPI 规范中的名称相同。这样,如果您需要更多地研究这些安全方案中的任何一种,就可以复制并粘贴它以找到有关此方案的更多信息。

The oauth2_scheme variable is an instance of OAuth2PasswordBearer, but it is also a “callable”.

oauth2_scheme 变量是 OAuth2PasswordBearer 的实例,但它也是『可调用的』。

It could be called as:

它可以被称为:

oauth2_scheme(some, parameters)

So, it can be used with Depends.

因此,它可以与 Depends 一起使用。

Use it
使用

Now you can pass that oauth2_scheme in a dependency with Depends.

现在,您可以通过带有 Depends 的依赖项传递该 oauth2_scheme

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


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

This dependency will provide a str that is assigned to the parameter token of the path operation function.

这种依赖项将提供一个 str,该 str 被分配给路径操作函数的参数 token

FastAPI will know that it can use this dependency to define a “security scheme” in the OpenAPI schema (and the automatic API docs).

FastAPI 将知道它可以使用此依赖项在 OpenAPI 架构(和自动 API 文档)中定义『安全方案』。

Technical Details

技术细节

FastAPI will know that it can use the class OAuth2PasswordBearer (declared in a dependency) to define the security scheme in OpenAPI because it inherits from fastapi.security.oauth2.OAuth2, which in turn inherits from fastapi.security.base.SecurityBase.

FastAPI 会知道它可以使用类 OAuth2PasswordBearer(在依赖项中声明)在 OpenAPI 中定义安全性方案,因为它继承自 fastapi.security.oauth2.OAuth2,而后者又继承自 fastapi.security.base.SecurityBase

All the security utilities that integrate with OpenAPI (and the automatic API docs) inherit from SecurityBase, that’s how FastAPI can know how to integrate them in OpenAPI.

与 OpenAPI(和自动 API 文档)集成的所有安全实用程序都继承自 SecurityBase,这就是 FastAPI 知道如何将其集成到 OpenAPI 中的方式。

What it does

它能做什么

It will go and look in the request for that Authorization header, check if the value is Bearer plus some token, and will return the token as a str.

它将查找请求中的 Authorization header,检查该值是否为 Bearer 加上一些令牌,并将令牌作为 str 返回。

If it doesn’t see an Authorization header, or the value doesn’t have a Bearer token, it will respond with a 401 status code error (UNAUTHORIZED) directly.

如果没有看到 Authorization header,或者该值没有 Bearer 令牌,它将直接以 401 状态码错误(UNAUTHORIZED)响应。

You don’t even have to check if the token exists to return an error. You can be sure that if your function is executed, it will have a str in that token.

您甚至不必检查令牌是否存在即可返回错误。 您可以确定,如果执行了函数,则该令牌中将带有一个 str

You can try it already in the interactive docs:

您可以在交互式文档中尝试一下:

在这里插入图片描述

We are not verifying the validity of the token yet, but that’s a start already.

我们尚未验证令牌的有效性,但这已经是一个开始。

Recap

回顾

So, in just 3 or 4 extra lines, you already have some primitive form of security.

因此,仅需增加 3 或 4 行,您就已经具有某种原始的安全性形式。

Get Current User

获取当前用户

In the previous chapter the security system (which is based on the dependency injection system) was giving the path operation function a token as a str:

在上一章中,安全系统(基于依赖项注入系统)为路径操作函数提供了一个 token 作为 str

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


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

But that is still not that useful.

但这仍然没有用。

Let’s make it give us the current user.

让它给我们当前的用户。

Create a user model

创建用户模型

First, let’s create a Pydantic user model.

首先,让我们创建一个 Pydantic 用户模型。

The same way we use Pydantic to declare bodies, we can use it anywhere else:

与使用 Pydantic 声明主体的方式相同,我们可以在其他任何地方使用它:

from typing import Optional

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None


def fake_decode_token(token):
    return User(
        username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
    )


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


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

Create a get_current_user dependency

创建 get_current_user 依赖项

Let’s create a dependency get_current_user.

让我们创建一个依赖项 get_current_user

Remember that dependencies can have sub-dependencies?

还记得依赖项可以有子依赖项吗?

get_current_user will have a dependency with the same oauth2_scheme we created before.

get_current_user 将具有与我们之前创建的 oauth2_scheme 相同的依赖项。

The same as we were doing before in the path operation directly, our new dependency get_current_user will receive a token as a str from the sub-dependency oauth2_scheme:

与我们之前直接在路径操作中所做的相同,我们新的依赖项 get_current_user 将从子依赖项 oauth2_scheme 接收一个令牌作为 str

from typing import Optional

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None


def fake_decode_token(token):
    return User(
        username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
    )


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


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

Get the user

获取用户

get_current_user will use a (fake) utility function we created, that takes a token as a str and returns our Pydantic User model:

get_current_user 将使用我们创建的(伪)实用程序函数,该函数将令牌作为 str 并返回我们的 Pydantic User 模型:

from typing import Optional

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None


def fake_decode_token(token):
    return User(
        username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
    )


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


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

Inject the current user

注入当前用户

So now we can use the same Depends with our get_current_user in the path operation:

因此,现在我们可以在路径操作中将相同的 Depends 与我们的 get_current_user 一起使用:

from typing import Optional

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None


def fake_decode_token(token):
    return User(
        username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
    )


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


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

Notice that we declare the type of current_user as the Pydantic model User.

注意,我们将 current_user 的类型声明为 Pydantic 模型 User

This will help us inside of the function with all the completion and type checks.

这将帮助我们使用所有补全和类型检查功能。

Tip

提示

You might remember that request bodies are also declared with Pydantic models.

您可能还记得,请求主体也使用 Pydantic 模型声明。

Here FastAPI won’t get confused because you are using Depends.

在这里,FastAPI 不会混淆,因为您正在使用 Depends

Check

检查

The way this dependency system is designed allows us to have different dependencies (different “dependables”) that all return a User model.

这种依赖系统的设计方式使我们可以拥有不同的依赖项(不同的『被依赖项』),它们都返回一个 User 模型。

We are not restricted to having only one dependency that can return that type of data.

我们不仅限于只能返回一种类型的数据的依赖项。

Other models

其他模型

You can now get the current user directly in the path operation functions and deal with the security mechanisms at the Dependency Injection level, using Depends.

现在,您可以直接在路径操作函数中获取当前用户,并使用 Depends依赖注入级别处理安全性机制。

And you can use any model or data for the security requirements (in this case, a Pydantic model User).

您可以使用任何模型或数据来满足安全性要求(在这种情况下,使用的是 Pydantic 模型 User)。

But you are not restricted to using some specific data model, class or type.

但是您不限于使用某些特定的数据模型、类或类型。

Do you want to have an id and email and not have any username in your model? Sure. You can use these same tools.

您是否要在模型中使用 idemail 而不使用 username? 当然。 您可以使用这些相同的工具。

Do you want to just have a str? Or just a dict? Or a database class model instance directly? It all works the same way.

您是否只想拥有一个 str? 还是仅仅一个 dict? 还是直接一个数据库类模型实例? 所有的工作方式都相同。

You actually don’t have users that log in to your application but robots, bots, or other systems, that have just an access token? Again, it all works the same.

实际上,您没有用户登录到您的应用程序,而是只有访问令牌的机械手、机器人或其他系统? 同样,它们的工作原理相同。

Just use any kind of model, any kind of class, any kind of database that you need for your application. FastAPI has you covered with the dependency injection system.

只需使用应用程序所需的任何模型、任何类、任何数据库即可。 FastAPI 覆盖了依赖项注入系统。

Code size

代码大小

This example might seem verbose. Have in mind that we are mixing security, data models utility functions and path operations in the same file.

这个例子似乎很冗长。 请记住,我们在同一文件中混合了安全性,数据模型实用程序功能和路径操作

But here’s the key point.

但这是关键。

The security and dependency injection stuff is written once.

安全性和依赖项注入内容只编写一次。

And you can make it as complex as you want. And still, have it written only once, in a single place. With all the flexibility.

您可以根据需要使其变得复杂。 而且,它只能在一个地方写一次。 具有所有的灵活性。

But you can have thousands of endpoints (path operations) using the same security system.

但是,使用同一安全系统,您可以有数千个端点(路径操作)。

And all of them (or any portion of them that you want) can take the advantage of re-using these dependencies or any other dependencies you create.

所有这些(或您想要的任何部分)都可以利用重新使用这些依赖项或您创建的任何其他依赖项的优势。

And all these thousands of path operations can be as small as 3 lines:

所有这数千个路径操作可能只有3行:

from typing import Optional

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None


def fake_decode_token(token):
    return User(
        username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
    )


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


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

Recap

回顾

You can now get the current user directly in your path operation function.

现在,您可以直接在路径操作函数中获取当前用户。

We are already halfway there.

我们已经到了一半。

We just need to add a path operation for the user/client to actually send the username and password.

That comes next.

我们只需要为用户 / 客户端添加一个路径操作,即可实际发送 usernamepassword

Simple OAuth2 with Password and Bearer

具有 Password 和 Bearer 的简单 OAuth2

Now let’s build from the previous chapter and add the missing parts to have a complete security flow.

现在,让我们从上一章开始构建,并添加缺少的部分以拥有完整的安全流程。

Get the username and password

获取 usernamepassword

We are going to use FastAPI security utilities to get the username and password.

我们将使用 FastAPI 安全实用程序来获取 usernamepassword

OAuth2 specifies that when using the “password flow” (that we are using) the client/user must send a username and password fields as form data.

OAuth2 指定使用『密码流』(我们正在使用)时,客户端 / 用户必须发送 usernamepassword 字段作为表单数据。

And the spec says that the fields have to be named like that. So user-name or email wouldn’t work.

规范说字段必须这样命名。 因此,user-nameemail 将不起作用。

But don’t worry, you can show it as you wish to your final users in the frontend.

但是不用担心,您可以在前端向最终用户显示它。

And your database models can use any other names you want.

您的数据库模型可以使用您想要的任何其他名称。

But for the login path operation, we need to use these names to be compatible with the spec (and be able to, for example, use the integrated API documentation system).

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

The spec also states that the username and password must be sent as form data (so, no JSON here).

规范还指出,usernamepassword 必须作为表单数据发送(因此,此处没有 JSON)。

scope

The spec also says that the client can send another form field “scope”.

规范还说,客户端可以发送另一个表格字段『scope』。

The form field name is scope (in singular), but it is actually a long string with “scopes” separated by spaces.

表单字段名称为 scope(单数形式),但实际上是一个长字符串,其中『scopes』用空格分隔。

Each “scope” is just a string (without spaces).

每个『scope』只是一个字符串(没有空格)。

They are normally used to declare specific security permissions, for example:

它们通常用于声明特定的安全权限,例如:

  • users:read or users:write are common examples.

    users:readusers:write 是常见示例。

  • instagram_basic is used by Facebook / Instagram.

    instagram_basic 被 Facebook / Instagram 使用。

  • https://www.googleapis.com/auth/drive is used by Google.

    Google 使用了 https://www.googleapis.com/auth/drive

Info

信息

In OAuth2 a “scope” is just a string that declares a specific permission required.

在 OAuth2 中,『scope』只是声明所需的特定权限的字符串。

It doesn’t matter if it has other characters like : or if it is a URL.

它是否有其他字符,例如 : 或者它是一个 URL。

Those details are implementation specific.

这些细节是特定于实现的。

For OAuth2 they are just strings.

对于 OAuth2,它们只是字符串。

Code to get the username and password

获取 usernamepassword 的代码

Now let’s use the utilities provided by FastAPI to handle this.

现在,让我们使用 FastAPI 提供的实用程序来处理此问题。

OAuth2PasswordRequestForm

First, import OAuth2PasswordRequestForm, and use it as a dependency with Depends for the path /token:

首先,导入 OAuth2PasswordRequestForm,并将其与 Depends 一起用作路径 /token 的依赖项:

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel

fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "fakehashedsecret",
        "disabled": False,
    },
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "alice@example.com",
        "hashed_password": "fakehashedsecret2",
        "disabled": True,
    },
}

app = FastAPI()


def fake_hash_password(password: str):
    return "fakehashed" + password


oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


class User(BaseModel):
    username: str
    email: str = None
    full_name: str = None
    disabled: 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):
    # This doesn't provide any security at all
    # Check the next version
    user = get_user(fake_users_db, token)
    return user


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: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


@app.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=400, 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=400, detail="Incorrect username or password")

    return {"access_token": user.username, "token_type": "bearer"}


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

OAuth2PasswordRequestForm is a class dependency that declares a form body with:

OAuth2PasswordRequestForm 是一个类依赖项,它声明具有以下内容的表单主体:

  • The username.

  • The password.

  • An optional scope field as a big string, composed of strings separated by spaces.

    可选的 scope 字段,为大字符串,由用空格分隔的字符串组成。

  • An optional grant_type.

    可选的 grant_type

Tip

提示

The OAuth2 spec actually requires a field grant_type with a fixed value of password, but OAuth2PasswordRequestForm doesn’t enforce it.

OAuth2 规范实际上需要具有固定值 password 的字段 grant_type,但是 OAuth2PasswordRequestForm 并不强制执行。

If you need to enforce it, use OAuth2PasswordRequestFormStrict instead of OAuth2PasswordRequestForm.

如果您需要强制执行,请使用 OAuth2PasswordRequestFormStrict 而不是 OAuth2PasswordRequestForm

  • An optional client_id (we don’t need it for our example).

    一个可选的 client_id(在我们的例子中我们不需要它)。

  • An optional client_secret (we don’t need it for our example).

    一个可选的 client_secret(在我们的示例中我们不需要它)。

Info

信息

The OAuth2PasswordRequestForm is not a special class for FastAPI as is OAuth2PasswordBearer.

OAuth2PasswordRequestFormOAuth2PasswordBearer 都不是 FastAPI 的特殊类。

OAuth2PasswordBearer makes FastAPI know that it is a security scheme. So it is added that way to OpenAPI.

OAuth2PasswordBearer 使 FastAPI 知道这是一个安全方案。因此将它添加到 OpenAPI。

But OAuth2PasswordRequestForm is just a class dependency that you could have written yourself, or you could have declared Form parameters directly.

但是,OAuth2PasswordRequestForm 只是您自己编写的类依赖项,或者您可以直接声明 Form 参数。

But as it’s a common use case, it is provided by FastAPI directly, just to make it easier.

但是,由于这是一个常见的用例,因此它是由 FastAPI 直接提供的,只是为了使其更容易。

Use the form data
使用表单数据

Tip

提示

The instance of the dependency class OAuth2PasswordRequestForm won’t have an attribute scope with the long string separated by spaces, instead, it will have a scopes attribute with the actual list of strings for each scope sent.

类依赖项 OAuth2PasswordRequestForm 的实例将没有带有长字符串用空格分隔的 scope 属性,而是具有 scope 属性,其中包含发送的每个作用域的实际字符串列表。

We are not using scopes in this example, but the functionality is there if you need it.

在此示例中,我们不使用 scopes,但是如果需要,可以使用此功能。

Now, get the user data from the (fake) database, using the username from the form field.

现在,使用表单字段中的 username 从(伪)数据库中获取用户数据。

If there is no such user, we return an error saying “incorrect username or password”.

如果没有这样的用户,我们将返回一个错误消息,提示『用户名或密码不正确』。

For the error, we use the exception HTTPException:

对于错误,我们使用异常 HTTPException

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel

fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "fakehashedsecret",
        "disabled": False,
    },
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "alice@example.com",
        "hashed_password": "fakehashedsecret2",
        "disabled": True,
    },
}

app = FastAPI()


def fake_hash_password(password: str):
    return "fakehashed" + password


oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


class User(BaseModel):
    username: str
    email: str = None
    full_name: str = None
    disabled: 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):
    # This doesn't provide any security at all
    # Check the next version
    user = get_user(fake_users_db, token)
    return user


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: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


@app.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=400, 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=400, detail="Incorrect username or password")

    return {"access_token": user.username, "token_type": "bearer"}


@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user
Check the password
检查密码

At this point we have the user data from our database, but we haven’t checked the password.

至此,我们已经从数据库中获取了用户数据,但尚未检查密码。

Let’s put that data in the Pydantic UserInDB model first.

让我们首先将这些数据放入 Pydantic UserInDB 模型中。

You should never save plaintext passwords, so, we’ll use the (fake) password hashing system.

您永远不应保存纯文本密码,因此,我们将使用(伪)密码哈希系统。

If the passwords don’t match, we return the same error.

如果密码不匹配,我们将返回相同的错误。

Password hashing
密码哈希

“Hashing” means: converting some content (a password in this case) into a sequence of bytes (just a string) that looks like gibberish.

『哈希』的意思是:将某些内容(在这种情况下为密码)转换为看起来像乱七八糟的字节序列(只是一个字符串)。

Whenever you pass exactly the same content (exactly the same password) you get exactly the same gibberish.

每当您传递完全相同的内容(完全相同的密码)时,您都会得到完全相同的乱七八糟的字节序列。

But you cannot convert from the gibberish back to the password.

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

WHY USE PASSWORD HASHING

为什么使用密码哈希

If your database is stolen, the thief won’t have your users’ plaintext passwords, only the hashes.

如果您的数据库被盗,小偷将没有用户的明文密码,只有哈希值。

So, the thief won’t be able to try to use those same passwords in another system (as many users use the same password everywhere, this would be dangerous).

因此,小偷将无法尝试在另一个系统中使用这些相同的密码(由于许多用户在各处都使用相同的密码,因此很危险)。

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel

fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "fakehashedsecret",
        "disabled": False,
    },
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "alice@example.com",
        "hashed_password": "fakehashedsecret2",
        "disabled": True,
    },
}

app = FastAPI()


def fake_hash_password(password: str):
    return "fakehashed" + password


oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


class User(BaseModel):
    username: str
    email: str = None
    full_name: str = None
    disabled: 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):
    # This doesn't provide any security at all
    # Check the next version
    user = get_user(fake_users_db, token)
    return user


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: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


@app.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=400, 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=400, detail="Incorrect username or password")

    return {"access_token": user.username, "token_type": "bearer"}


@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user
About **user_dict
关于 ** user_dict

UserInDB(**user_dict) means:

UserInDB(**user_dict) 的意思是:

Pass the keys and values of the user_dict directly as key-value arguments, equivalent to:

直接将 user_dict 的键和值作为键值参数传递,等效于:

UserInDB(
    username = user_dict["username"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
    disabled = user_dict["disabled"],
    hashed_password = user_dict["hashed_password"],
)

Info

信息

For a more complete explanation of **user_dict check back in the documentation for Extra Models.

有关 **user_dict 的更完整说明,请返回额外模型的文档

Return the token

返回令牌

The response of the token endpoint must be a JSON object.

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

It should have a token_type. In our case, as we are using “Bearer” tokens, the token type should be “bearer”.

它应该有一个 token_type。 在我们的例子中,因为我们使用的是『Bearer』令牌,所以令牌类型应为『bearer』。

And it should have an access_token, with a string containing our access token.

并且应该有一个 access_token,其中包含我们的访问令牌的字符串。

For this simple example, we are going to just be completely insecure and return the same username as the token.

对于这个简单的示例,我们将完全不安全,并返回与令牌相同的 username

Tip

提示

In the next chapter, you will see a real secure implementation, with password hashing and JWT tokens.

在下一章中,您将看到一个真正的安全实现,其中包括密码哈希和 JWT 令牌。

But for now, let’s focus on the specific details we need.

但是,现在,让我们专注于我们需要的特定细节。

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel

fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "fakehashedsecret",
        "disabled": False,
    },
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "alice@example.com",
        "hashed_password": "fakehashedsecret2",
        "disabled": True,
    },
}

app = FastAPI()


def fake_hash_password(password: str):
    return "fakehashed" + password


oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


class User(BaseModel):
    username: str
    email: str = None
    full_name: str = None
    disabled: 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):
    # This doesn't provide any security at all
    # Check the next version
    user = get_user(fake_users_db, token)
    return user


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: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


@app.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=400, 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=400, detail="Incorrect username or password")

    return {"access_token": user.username, "token_type": "bearer"}


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

Tip

By the spec, you should return a JSON with an access_token and a token_type, the same as in this example.

根据规范,您应该返回一个带有 access_tokentoken_type 的 JSON,与本示例相同。

This is something that you have to do yourself in your code, and make sure you use those JSON keys.

这是您必须在代码中完成的工作,并确保使用那些 JSON 密钥。

It’s almost the only thing that you have to remember to do correctly yourself, to be compliant with the specifications.

这几乎是唯一必须记住的事情,您必须自己正确执行操作才能符合规范。

For the rest, FastAPI handles it for you.

其余的,FastAPI 为您处理。

Update the dependencies

更新依赖项

Now we are going to update our dependencies.

现在,我们将更新依赖项。

We want to get the current_user only if this user is active.

如果这个用户是活跃的,我们想获得 current_user

So, we create an additional dependency get_current_active_user that in turn uses get_current_user as a dependency.
因此,我们创建了一个附加的依赖项 get_current_active_user,而该依赖项又将 get_current_user 用作依赖项。

Both of these dependencies will just return an HTTP error if the user doesn’t exists, or if is inactive.

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

So, in our endpoint, we will only get a user if the user exists, was correctly authenticated, and is active:

因此,在我们的端点中,只有在用户存在,经过正确身份验证且处于活动状态时,我们才会获得该用户:

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel

fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "fakehashedsecret",
        "disabled": False,
    },
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "alice@example.com",
        "hashed_password": "fakehashedsecret2",
        "disabled": True,
    },
}

app = FastAPI()


def fake_hash_password(password: str):
    return "fakehashed" + password


oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")


class User(BaseModel):
    username: str
    email: str = None
    full_name: str = None
    disabled: 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):
    # This doesn't provide any security at all
    # Check the next version
    user = get_user(fake_users_db, token)
    return user


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: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


@app.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=400, 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=400, detail="Incorrect username or password")

    return {"access_token": user.username, "token_type": "bearer"}


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

Info

信息

The additional header WWW-Authenticate with value Bearer we are returning here is also part of the spec.

我们在此处返回的附加标头 WWW-Authenticate(值为 Bearer)也是规范的一部分。

Any HTTP (error) status code 401 “UNAUTHORIZED” is supposed to also return a WWW-Authenticate header.

任何 HTTP(错误)状态码 401『UNAUTHORIZED』都应返回 WWW-Authenticate header。

In the case of bearer tokens (our case), the value of that header should be Bearer.

对于不记名令牌(我们的情况),该标头的值应为 Bearer

You can actually skip that extra header and it would still work.

您实际上可以跳过该额外的标题,它仍然可以工作。

But it’s provided here to be compliant with the specifications.

但这是为了符合规范而提供的。

Also, there might be tools that expect and use it (now or in the future) and that might be useful for you or your users, now or in the future.

另外,可能会有工具(现在或将来)期望并使用它,并且可能对您或您的用户现在或将来有用。

That’s the benefit of standards…

这就是标准的好处…

See it in action

实际操作

Open the interactive docs: http://127.0.0.1:8000/docs.

打开交互式文档:http://127.0.0.1:8000/docs。

Authenticate
验证

Click the “Authorize” button.

点击『Authorize』按钮。

Use the credentials:

使用凭据:

User: johndoe

Password: secret

在这里插入图片描述

After authenticating in the system, you will see it like:

在系统中进行身份验证之后,您将看到:

在这里插入图片描述

Get your own user data
获取您自己的用户数据

Now use the operation GET with the path /users/me.

现在,将 GET 操作与路径 /users/me 一起使用。

You will get your user’s data, like:

您将获得用户的数据,例如:

{
  "username": "johndoe",
  "email": "johndoe@example.com",
  "full_name": "John Doe",
  "disabled": false,
  "hashed_password": "fakehashedsecret"
}

在这里插入图片描述

If you click the lock icon and logout, and then try the same operation again, you will get an HTTP 401 error of:

如果单击锁定图标并注销,然后再次尝试相同的操作,则会收到 HTTP 401 错误:

{
  "detail": "Not authenticated"
}
Inactive user
非活动用户

Now try with an inactive user, authenticate with:

现在尝试使用非活动用户,并通过以下方式进行身份验证:

User: alice

Password: secret2

And try to use the operation GET with the path /users/me.

并尝试在路径 / users / me 中使用 GET 操作。

You will get an “inactive user” error, like:

您将收到『inactive user』错误,例如:

{
  "detail": "Inactive user"
}

Recap

回顾

You now have the tools to implement a complete security system based on username and password for your API.

现在,您将拥有用于基于 API 的 usernamepassword 实现完整安全系统的工具。

Using these tools, you can make the security system compatible with any database and with any user or data model.

使用这些工具,可以使安全系统与任何数据库以及任何用户或数据模型兼容。

The only detail missing is that it is not actually “secure” yet.

唯一缺少的细节是它实际上还不是『安全的』。

In the next chapter you’ll see how to use a secure password hashing library and JWT tokens.

在下一章中,您将看到如何使用安全的密码哈希库和 JWT 令牌。

OAuth2 with Password (and hashing), Bearer with JWT tokens

带有密码(和哈希)的 OAuth2,带有 JWT 令牌的 Bearer

Now that we have all the security flow, let’s make the application actually secure, using JWT tokens and secure password hashing.

现在我们已经拥有了所有的安全流程,现在让我们使用 JWT 令牌和安全密码哈希来使应用程序真正安全。

This code is something you can actually use in your application, save the password hashes in your database, etc.

您可以在应用程序中实际使用此代码,在数据库中保存密码哈希,等等。

We are going to start from where we left in the previous chapter and increment it.

我们将从上一章的位置开始,然后增加它。

About JWT

关于 JWT

JWT means “JSON Web Tokens”.

JWT 的意思是『JSON Web Tokens』。

It’s a standard to codify a JSON object in a long dense string without spaces. It looks like this:

这是将 JSON 对象编码为长而密集的字符串且没有空格的标准。 看起来像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

It is not encrypted, so, anyone could recover the information from the contents.

它没有加密,因此任何人都可以从内容中恢复信息。

But it’s signed. So, when you receive a token that you emitted, you can verify that you actually emitted it.

但这是签名的。 因此,当您收到发出的令牌时,可以验证您是否实际发出了令牌。

That way, you can create a token with an expiration of, let’s say, 1 week. And then when the user comes back the next day with the token, you know she/he is still signed into your system.

这样,您可以创建一个有效期为1周的令牌。 然后,当用户第二天带着令牌返回时,您知道她 / 他仍在登录到您的系统中。

And after a week, the token will be expired and the user will not be authorized and will have to sign in again to get a new token. And if the user (or a third party) tried to modify the token to change the expiration, you would be able to discover it, because the signatures would not match.

一周后,令牌将过期,用户将不会获得授权,必须再次登录才能获得新令牌。 而且,如果用户(或第三方)试图修改令牌以更改到期时间,则您将能够发现它,因为签名不匹配。

If you want to play with JWT tokens and see how they work, check https://jwt.io.

如果要使用 JWT 令牌并查看其工作原理,请检查 https://jwt.io

Install PyJWT

安装PyJWT

We need to install PyJWT to generate and verify the JWT tokens in Python:

我们需要安装 PyJWT 来生成和验证 Python 中的 JWT 令牌:

pip install pyjwt

Password hashing

密码哈希

“Hashing” means converting some content (a password in this case) into a sequence of bytes (just a string) that looks like gibberish.

『哈希』是指将某些内容(在这种情况下为密码)转换为看起来像乱七八糟的字节序列(只是一个字符串)。

Whenever you pass exactly the same content (exactly the same password) you get exactly the same gibberish.

每当您传递完全相同的内容(完全相同的密码)时,您都会得到完全相同的乱七八糟的字节序列。

But you cannot convert from the gibberish back to the password.

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

Why use password hashing
为什么使用密码哈希

If your database is stolen, the thief won’t have your users’ plaintext passwords, only the hashes.

如果您的数据库被盗,小偷将没有用户的明文密码,只有哈希值。

So, the thief won’t be able to try to use that password in another system (as many users use the same password everywhere, this would be dangerous).

因此,小偷将无法尝试在另一个系统中使用该密码(由于许多用户在各处都使用相同的密码,因此很危险)。

Install passlib

安装 passlib

PassLib is a great Python package to handle password hashes.

PassLib 是一个很棒的 Python 程序包,用于处理密码哈希。

It supports many secure hashing algorithms and utilities to work with them.

它支持许多安全的哈希算法和实用程序来使用它们。

The recommended algorithm is “Bcrypt”.

推荐的算法是『Bcrypt』。

So, install PassLib with Bcrypt:

因此,安装 PassLib 来使用 Bcrypt:

pip install passlib[bcrypt]

Tip

提示

With passlib, you could even configure it to be able to read passwords created by Django, a Flask security plug-in or many others.

使用 passlib,您甚至可以配置它以读取由 DjangoFlask 安全性插件或许多其他工具创建的密码。

So, you would be able to, for example, share the same data from a Django application in a database with a FastAPI application. Or gradually migrate a Django application using the same database.

因此,您将能够例如与 FastAPI 应用程序共享数据库中来自 Django 应用程序的相同数据。 或者使用相同的数据库逐渐迁移 Django 应用程序。

And your users would be able to login from your Django app or from your FastAPI app, at the same time.

而且您的用户将能够同时从 Django 应用或 FastAPI 应用登录。

Hash and verify the passwords

哈希并验证密码

Import the tools we need from passlib.

passlib 导入我们需要的工具。

Create a PassLib “context”. This is what will be used to hash and verify passwords.

创建一个 PassLib『上下文』。 这将用于哈希和验证密码。

Tip

提示

The PassLib context also has functionality to use different hashing algorithms, including deprecate old ones only to allow verifying them, etc.

PassLib 上下文还具有使用不同哈希算法的功能,包括仅淘汰旧算法以允许对其进行验证等。

For example, you could use it to read and verify passwords generated by another system (like Django) but hash any new passwords with a different algorithm like Bcrypt.

例如,您可以使用它来读取和验证由另一个系统(例如 Django)生成的密码,但使用其他算法(例如 Bcrypt)对任何新密码进行哈希处理。

And be compatible with all of them at the same time.

并同时与所有它们兼容。

Create a utility function to hash a password coming from the user.

创建一个实用程序函数以哈希来自用户的密码。

And another utility to verify if a received password matches the hash stored.

还有另一个实用程序,用于验证收到的密码是否与存储的哈希匹配。

And another one to authenticate and return a user.

另一个用于认证并返回用户。

from datetime import datetime, timedelta

import jwt
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt import PyJWTError
from passlib.context import CryptContext
from pydantic import BaseModel

# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30


fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
        "disabled": False,
    }
}


class Token(BaseModel):
    access_token: str
    token_type: str


class TokenData(BaseModel):
    username: str = None


class User(BaseModel):
    username: str
    email: str = None
    full_name: str = None
    disabled: bool = None


class UserInDB(User):
    hashed_password: str


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")

app = FastAPI()


def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password):
    return pwd_context.hash(password)


def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


def authenticate_user(fake_db, username: str, password: str):
    user = get_user(fake_db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user


def create_access_token(*, data: dict, expires_delta: 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(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except PyJWTError:
        raise credentials_exception
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    return user


async def get_current_active_user(current_user: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=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"}


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


@app.get("/users/me/items/")
async def read_own_items(current_user: User = Depends(get_current_active_user)):
    return [{"item_id": "Foo", "owner": current_user.username}]

Note

注意

If you check the new (fake) database fake_users_db, you will see how the hashed password looks like now: "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW".

如果检查新的(伪)数据库 fake_users_db,您将看到哈希密码现在看起来像:"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"

Handle JWT tokens

处理 JWT 令牌

Import the modules installed.

导入已安装的模块。

Create a random secret key that will be used to sign the JWT tokens.

创建一个随机密钥,该密钥将用于对 JWT 令牌进行签名。

To generate a secure random secret key use the command:

要生成安全的随机密钥,请使用以下命令:

openssl rand -hex 32

And copy the output to the variable SECRET_KEY (don’t use the one in the example).

并将输出复制到变量 SECRET_KEY(在示例中不要使用)。

Create a variable ALGORITHM with the algorithm used to sign the JWT token and set it to "HS256".

使用用于签名 JWT 令牌的算法创建变量 ALGORITHM,并将其设置为 "HS256"

Create a variable for the expiration of the token.

创建一个令牌到期的变量。

Define a Pydantic Model that will be used in the token endpoint for the response.

定义将在令牌端点中用于响应的 Pydantic 模型。

Create a utility function to generate a new access token.

创建一个实用程序函数以生成一个新的访问令牌。

from datetime import datetime, timedelta

import jwt
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt import PyJWTError
from passlib.context import CryptContext
from pydantic import BaseModel

# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30


fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
        "disabled": False,
    }
}


class Token(BaseModel):
    access_token: str
    token_type: str


class TokenData(BaseModel):
    username: str = None


class User(BaseModel):
    username: str
    email: str = None
    full_name: str = None
    disabled: bool = None


class UserInDB(User):
    hashed_password: str


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")

app = FastAPI()


def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password):
    return pwd_context.hash(password)


def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


def authenticate_user(fake_db, username: str, password: str):
    user = get_user(fake_db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user


def create_access_token(*, data: dict, expires_delta: 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(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except PyJWTError:
        raise credentials_exception
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    return user


async def get_current_active_user(current_user: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=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"}


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


@app.get("/users/me/items/")
async def read_own_items(current_user: User = Depends(get_current_active_user)):
    return [{"item_id": "Foo", "owner": current_user.username}]

Update the dependencies

更新依赖项

Update get_current_user to receive the same token as before, but this time, using JWT tokens.

更新 get_current_user 以接收与以前相同的令牌,但是这次使用 JWT 令牌。

Decode the received token, verify it, and return the current user.

解码收到的令牌,对其进行验证,然后返回当前用户。

If the token is invalid, return an HTTP error right away.

如果令牌无效,请立即返回 HTTP 错误。

from datetime import datetime, timedelta

import jwt
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt import PyJWTError
from passlib.context import CryptContext
from pydantic import BaseModel

# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30


fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
        "disabled": False,
    }
}


class Token(BaseModel):
    access_token: str
    token_type: str


class TokenData(BaseModel):
    username: str = None


class User(BaseModel):
    username: str
    email: str = None
    full_name: str = None
    disabled: bool = None


class UserInDB(User):
    hashed_password: str


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")

app = FastAPI()


def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password):
    return pwd_context.hash(password)


def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


def authenticate_user(fake_db, username: str, password: str):
    user = get_user(fake_db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user


def create_access_token(*, data: dict, expires_delta: 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(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except PyJWTError:
        raise credentials_exception
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    return user


async def get_current_active_user(current_user: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=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"}


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


@app.get("/users/me/items/")
async def read_own_items(current_user: User = Depends(get_current_active_user)):
    return [{"item_id": "Foo", "owner": current_user.username}]

Update the /token path operation

更新 / token 路径操作

Create a timedelta with the expiration time of the token.

用令牌的到期时间创建一个 timedelta

Create a real JWT access token and return it.

创建一个真实的 JWT 访问令牌并返回它。

from datetime import datetime, timedelta

import jwt
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt import PyJWTError
from passlib.context import CryptContext
from pydantic import BaseModel

# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30


fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
        "disabled": False,
    }
}


class Token(BaseModel):
    access_token: str
    token_type: str


class TokenData(BaseModel):
    username: str = None


class User(BaseModel):
    username: str
    email: str = None
    full_name: str = None
    disabled: bool = None


class UserInDB(User):
    hashed_password: str


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")

app = FastAPI()


def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password):
    return pwd_context.hash(password)


def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


def authenticate_user(fake_db, username: str, password: str):
    user = get_user(fake_db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user


def create_access_token(*, data: dict, expires_delta: 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(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except PyJWTError:
        raise credentials_exception
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    return user


async def get_current_active_user(current_user: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=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"}


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


@app.get("/users/me/items/")
async def read_own_items(current_user: User = Depends(get_current_active_user)):
    return [{"item_id": "Foo", "owner": current_user.username}]
Technical details about the JWT “subject” sub
有关 JWT『子项』sub 的技术细节

The JWT specification says that there’s a key sub, with the subject of the token.

JWT 规范指出,存在一个带有令牌主题的关键 sub

It’s optional to use it, but that’s where you would put the user’s identification, so we are using it here.

您可以选择使用它,但这是您放置用户标识的地方,因此我们在这里使用它。

JWT might be used for other things apart from identifying a user and allowing him to perform operations directly on your API.

JWT 除了可以识别用户并允许他直接在您的 API 上执行操作之外,还可以用于其他用途。

For example, you could identify a “car” or a “blog post”.

例如,您可以标识『汽车』或『博客文章』。

Then you could add permissions about that entity, like “drive” (for the car) or “edit” (for the blog).

然后,您可以添加有关该实体的权限,例如『引擎』(对于汽车)或者『编辑』(对于博客)。

And then, you could give that JWT token to a user (or bot), and he could use it to perform those actions (drive the car, or edit the blog post) without even needing to have an account, just with the JWT token your API generated for that.

然后,您可以将该 JWT 令牌提供给用户(或漫游器),他甚至可以仅使用 JWT 令牌就可以使用该 JWT 令牌执行这些操作(驾驶汽车或编辑博客文章),而无需开设帐户。您为此生成的 API。

Using these ideas, JWT can be used for way more sophisticated scenarios.

使用这些想法,JWT 可以用于更复杂的场景。

In those cases, several of those entities could have the same ID, let’s say foo (a user foo, a car foo, and a blog post foo).

在这种情况下,其中一些实体可能具有相同的 ID,例如 foo(用户 foo,汽车 foo 和博客 foo)。

So, to avoid ID collisions, when creating the JWT token for the user, you could prefix the value of the sub key, e.g. with username:. So, in this example, the value of sub could have been: username:johndoe.

因此,为避免 ID 冲突,在为用户创建 JWT 令牌时,您可以在 sub 键的值前添加前缀,例如与 username:。因此,在此示例中,sub 的值可能是:username:johndoe

The important thing to have in mind is that the sub key should have a unique identifier across the entire application, and it should be a string.

要记住的重要一点是,sub 键在整个应用程序中应具有唯一的标识符,并且应为字符串。

Check it

检查

Run the server and go to the docs: http://127.0.0.1:8000/docs.

运行服务器,然后转到文档:http://127.0.0.1:8000/docs。

You’ll see the user interface like:

您将看到如下用户界面:

在这里插入图片描述

Authorize the application the same way as before.

与以前一样对应用程序进行授权。

Using the credentials:

使用凭据:

Username: johndoe Password: secret

Check

检查

Notice that nowhere in the code is the plaintext password “secret”, we only have the hashed version.

请注意,代码中没有任何地方是明文密码『secret』,我们只有哈希版本。

在这里插入图片描述

Call the endpoint /users/me/, you will get the response as:

调用端点 / users / me /,您将获得以下响应:

{
  "username": "johndoe",
  "email": "johndoe@example.com",
  "full_name": "John Doe",
  "disabled": false
}

在这里插入图片描述

If you open the developer tools, you could see how the data sent and only includes the token, the password is only sent in the first request to authenticate the user and get that access token, but not afterwards:

如果打开开发人员工具,您将看到发送数据的方式以及仅包含令牌的情况,仅在第一个请求中发送密码以验证用户身份并获取该访问令牌,但随后将不发送密码:

在这里插入图片描述

Note

注意

Notice the header Authorization, with a value that starts with Bearer.

注意标题 Authorization,其值以 Bearer 开头。

Advanced usage with scopes

scopes的高级用法

OAuth2 has the notion of “scopes”.

OAuth2 具有『scopes』的概念。

You can use them to add a specific set of permissions to a JWT token.

您可以使用它们向 JWT 令牌添加一组特定的权限。

Then you can give this token to a user directly or a third party, to interact with your API with a set of restrictions.

然后,您可以将此令牌直接提供给用户或第三方,以便在一系列限制下与您的 API 进行交互。

You can learn how to use them and how they are integrated into FastAPI later in the Advanced User Guide.

您可以稍后在高级用户指南中学习如何使用它们以及如何将它们集成到 FastAPI 中。

Recap

回顾

With what you have seen up to now, you can set up a secure FastAPI application using standards like OAuth2 and JWT.

到目前为止,您可以使用 OAuth2 和 JWT 等标准来建立安全的 FastAPI 应用程序。

In almost any framework handling the security becomes a rather complex subject quite quickly.

在几乎所有处理安全性的框架中,安全性都很快成为一个相当复杂的主题。

Many packages that simplify it a lot have to make many compromises with the data model, database, and available features. And some of these packages that simplify things too much actually have security flaws underneath.

许多大大简化了它的软件包必须在数据模型,数据库和可用功能上做出许多妥协。其中一些简化太多的软件包实际上在下面具有安全漏洞。


FastAPI doesn’t make any compromise with any database, data model or tool.

FastAPI 不会对任何数据库,数据模型或工具造成任何损害。

It gives you all the flexibility to choose the ones that fit your project the best.

它使您可以灵活地选择最适合您的项目的项目。

And you can use directly many well maintained and widely used packages like passlib and pyjwt, because FastAPI doesn’t require any complex mechanisms to integrate external packages.

而且,由于 FastAPI 不需要任何复杂的机制来集成外部软件包,因此您可以直接使用许多维护良好且使用广泛的软件包,例如 passlibpyjwt

But it provides you the tools to simplify the process as much as possible without compromising flexibility, robustness, or security.

但是它为您提供了在不影响灵活性,健壮性或安全性的前提下,尽可能简化流程的工具。

And you can use and implement secure, standard protocols, like OAuth2 in a relatively simple way.

您可以以相对简单的方式使用和实现安全的标准协议,例如 OAuth2。

You can learn more in the Advanced User Guide about how to use OAuth2 “scopes”, for a more fine-grained permission system, following these same standards. OAuth2 with scopes is the mechanism used by many big authentication providers, like Facebook, Google, GitHub, Microsoft, Twitter, etc. to authorize third party applications to interact with their APIs on behalf of their users.

loper tools, you could see how the data sent and only includes the token, the password is only sent in the first request to authenticate the user and get that access token, but not afterwards:

如果打开开发人员工具,您将看到发送数据的方式以及仅包含令牌的情况,仅在第一个请求中发送密码以验证用户身份并获取该访问令牌,但随后将不发送密码:

[外链图片转存中…(img-zafZqnYO-1590629146929)]

Note

注意

Notice the header Authorization, with a value that starts with Bearer.

注意标题 Authorization,其值以 Bearer 开头。

Advanced usage with scopes

scopes的高级用法

OAuth2 has the notion of “scopes”.

OAuth2 具有『scopes』的概念。

You can use them to add a specific set of permissions to a JWT token.

您可以使用它们向 JWT 令牌添加一组特定的权限。

Then you can give this token to a user directly or a third party, to interact with your API with a set of restrictions.

然后,您可以将此令牌直接提供给用户或第三方,以便在一系列限制下与您的 API 进行交互。

You can learn how to use them and how they are integrated into FastAPI later in the Advanced User Guide.

您可以稍后在高级用户指南中学习如何使用它们以及如何将它们集成到 FastAPI 中。

Recap

回顾

With what you have seen up to now, you can set up a secure FastAPI application using standards like OAuth2 and JWT.

到目前为止,您可以使用 OAuth2 和 JWT 等标准来建立安全的 FastAPI 应用程序。

In almost any framework handling the security becomes a rather complex subject quite quickly.

在几乎所有处理安全性的框架中,安全性都很快成为一个相当复杂的主题。

Many packages that simplify it a lot have to make many compromises with the data model, database, and available features. And some of these packages that simplify things too much actually have security flaws underneath.

许多大大简化了它的软件包必须在数据模型,数据库和可用功能上做出许多妥协。其中一些简化太多的软件包实际上在下面具有安全漏洞。


FastAPI doesn’t make any compromise with any database, data model or tool.

FastAPI 不会对任何数据库,数据模型或工具造成任何损害。

It gives you all the flexibility to choose the ones that fit your project the best.

它使您可以灵活地选择最适合您的项目的项目。

And you can use directly many well maintained and widely used packages like passlib and pyjwt, because FastAPI doesn’t require any complex mechanisms to integrate external packages.

而且,由于 FastAPI 不需要任何复杂的机制来集成外部软件包,因此您可以直接使用许多维护良好且使用广泛的软件包,例如 passlibpyjwt

But it provides you the tools to simplify the process as much as possible without compromising flexibility, robustness, or security.

但是它为您提供了在不影响灵活性,健壮性或安全性的前提下,尽可能简化流程的工具。

And you can use and implement secure, standard protocols, like OAuth2 in a relatively simple way.

您可以以相对简单的方式使用和实现安全的标准协议,例如 OAuth2。

You can learn more in the Advanced User Guide about how to use OAuth2 “scopes”, for a more fine-grained permission system, following these same standards. OAuth2 with scopes is the mechanism used by many big authentication providers, like Facebook, Google, GitHub, Microsoft, Twitter, etc. to authorize third party applications to interact with their APIs on behalf of their users.

您可以在高级用户指南中了解更多有关如何使用 OAuth2 『scopes』的信息,以遵循这些相同的标准来获得更细粒度的权限系统。具有范围的 OAuth2 是许多大型身份验证提供程序使用的机制,例如 Facebook、Google、GitHub、Microsoft、Twitter 等,用于授权第三方应用程序代表其用户与其 API 进行交互。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值