专业的编程技术博客社区

网站首页 > 博客文章 正文

FastAPI教程:11 身份验证和授权(fastapi 安全)

baijin 2024-09-01 13:19:10 博客文章 9 ℃ 0 评论

预览

有时网站是完全开放的() 任何访问者都可以访问任何页面。 但是,如果网站的任何内容可能被修改, 某些终结点将仅限于某些人员或组。 如果有人可以更改亚马逊上的页面, 想象一下会出现的奇怪物品, 以及一些人会突然获得的惊人销售。 不幸的是,对某些人来说,利用其余部分是人类的天性, 为其活动支付隐性税款的人。

我们是否应该让我们的加密网站对任何用户开放 访问任何端点? 不! 几乎任何大型 Web 服务最终都需要处理:

  • 是谁?
  • ():你想要什么?

如果身份验证代码有自己的新层, 比如说在网络和服务之间? 还是应该由 Web 或服务层本身处理所有内容? 本章深入探讨身份验证技术和 把它们放在哪里。

经常 网络安全的描述 似乎比他们需要的更令人困惑。 攻击者可能真的非常非常狡猾, 对策可能并不简单。

注意

正如我不止一次提到的那样, 官方的 FastAPI 文档非常出色。 如果本书章节,请尝试部分 并没有像你想要的那样很好地解释事情。

因此,让我们逐步进行。 我将从仅用于的简单技术开始 将身份验证挂接到 Web 端点进行测试, 但不会在公共网站上站起来。

插曲 1:是否需要身份验证?

同样,与有关: 你是谁?。 为了实现这一点, 某处需要映射 一些秘密信息 到唯一标识。 有很多方法可以做到这一点,但复杂性变化。 让我们从小处着手,逐步提高。

通常,有关Web开发的书籍和文章 立即跳转到详细信息 认证和授权, 有时会混淆他们。 他们有时会跳过第一个问题: 你真的需要吗?

您可以允许完全匿名访问您的所有 网站的页面。 但这会让你对 拒绝服务攻击等漏洞利用。 虽然有些保护措施,如速率限制 可以在网络之外实现 服务器(见第13章), 几乎所有公共 API 提供程序至少需要一些身份验证。

除了安全性之外,网站还想知道他们的表现:

  • 有多少独立访客?
  • 最受欢迎的页面是什么?
  • 某些更改会增加观看次数吗?
  • 哪些页面序列很常见?

这些需要对特定访问者进行身份验证。 否则,只能获得总数。

注意

如果您的网站需要身份验证或授权, 然后对它的访问应该是加密的(HTTPS与HTTP), 防止攻击者提取机密数据 从纯文本。 请参阅第 13 章 有关设置 HTTPS 的详细信息。

身份验证方法

有太多的Web身份验证方法和工具:

  • : 使用经典的 HTTP 基本身份验证和摘要式身份验证。
  • : 一个不透明的长字符串,伴随着。
  • : 一组用于身份验证和授权的标准。
  • 编码格式包含 加密签名的用户信息

在以下两节中, 我将回顾前两种方法并展示 您传统上如何实现它们。 但是我会在填写 API 之前停止并 数据库代码。 相反,这些后面的部分 使用 OAuth2 和 JWT 全面实施更现代的方案。

全局身份验证:共享密钥

最简单的身份验证方法是传递一个机密 通常只有 Web 服务器知道。 如果匹配,则表示您加入。 如果您的 API 站点向公众公开,这是不安全的 使用 HTTP 而不是 HTTPS。 如果它隐藏在本身打开的前端站点后面, 他们可以交流 使用共享常量机密。 但是,如果您的前端网站被黑客入侵, 然后。 但是,让我们看看 FastAPI 如何处理简单的身份验证。

创建一个名为 的新顶级文件。 确保您没有其他 FastAPI 服务器 仍然从前几章中不断变化的 文件之一运行。 示例 11-1 实现了一个服务器 只需返回任何用户名和密码 被发送到它使用 HTTP 基本身份验证 — 一种来自 Web 原始时代的方法。

使用 HTTP 基本身份验证获取用户信息:auth.py

 import   uvicorn 
 from   fastapi   import   Depends  ,   FastAPI 
 from   fastapi.security   import   HTTPBasic  ,   HTTPBasicCredentials 

 app   =   FastAPI  () 

 basic   =   HTTPBasic  () 

 @app  .  get  (  "/who"  ) 
 def   get_user  ( 
     creds  :   HTTPBasicCredentials   =   Depends  (  basic  )): 
     return   {  "username"  :   creds  .  username  ,   "password"  :   creds  .  password  } 

 if   __name__   ==   "__main__"  : 
     uvicorn  .  run  (  "auth:app"  ,   reload  =  True  ) 

在例 11-2 中, 告诉 Httpie 发出此基本身份验证请求 (这需要 参数 -a )。 在这里,让我们使用名称“me”和密码“secret”:

使用 Httpie 进行测试

$ http -q -a me:secret localhost:8000/who
{
    "password": "secret",
    "username": "me"
}

使用示例 11-3 中的请求包进行测试类似, 使用 auth 参数:

使用请求进行测试

 >>>   import   requests 
 >>>   r   =   requests  .  get  (  "http://localhost:8000/who"  , 
     auth  =  (  "me"  ,   "secret"  )) 
 >>>   r  .  json  () 
 {  'username'  :   'me'  ,   'password'  :   'secret'  } 

您也可以使用自动文档页面对其进行测试 (), 如图 11-1 所示:


单击右侧的向下箭头, 然后是试用按钮, ,然后执行按钮。 您将看到一个要求提供用户名和密码的表单。 键入任何内容。 它将命中该服务器终结点并显示这些值 在响应中。

这些测试表明您可以获得用户名和密码 到服务器并返回 (尽管这些实际上都没有检查任何东西)。 服务器中的某些内容需要验证此名称和密码 匹配一些批准的值。 因此,在示例 11-4 中, 我将在 Web 服务器中包含单个机密用户名和密码。 您现在传入的用户名和密码需要与它们匹配 (每个都是), 否则你会得到一个例外。 HTTP状态码401正式称为未授权, 但这实际上意味着。

注意

而不是记住所有的HTTP状态代码, 您可以导入 FastAPI 的状态模块 (它本身是直接从Starlette导入的)。 所以你可以使用更具解释性的 status_code=HTTP_401_UNAUTHORIZED 在 下面的代码而不是 一个普通的status_code=401。

将机密用户名和密码添加到 auth.py

 import   uvicorn 
 from   fastapi   import   Depends  ,   FastAPI 
 from   fastapi.security   import   HTTPBasic  ,   HTTPBasicCredentials 

 app   =   FastAPI  () 

 secret_user  :   str   =   "newphone" 
 secret_password  :   str   =   "whodis?" 

 basic  :   HTTPBasicCredentials   =   HTTPBasic  () 

 @app  .  get  (  "/who"  ) 
 def   get_user  ( 
     creds  :   HTTPBasicCredentials   =   Depends  (  basic  ))   ->   dict  : 
     if   (  creds  .  username   ==   secret_user   and 
         creds  .  password   ==   secret_password  ): 
         return   {  "username"  :   creds  .  username  , 
             "password"  :   creds  .  password  } 
     raise   HTTPException  (  status_code  =  401  ,   detail  =  "Hey!"  ) 

 if   __name__   ==   "__main__"  : 
     uvicorn  .  run  (  "auth:app"  ,   reload  =  True  ) 

猜错用户名和密码将获得轻微的 401 责备 在例 11-5 中:

使用 Httpie 和不匹配的用户名/密码进行测试

$ http -a me:secret localhost:8000/who
HTTP/1.1 401 Unauthorized
content-length: 17
content-type: application/json
date: Fri, 03 Mar 2023 03:25:09 GMT
server: uvicorn

{
    "detail": "Hey!"
}

像以前一样,使用魔术组合返回它们, 在例 11-6 中:

使用 Httpie 和正确的用户名/密码进行测试

$ http -q -a newphone:whodis? localhost:8000/who
{
    "password": "whodis?",
    "username": "newphone"
}

简单的个人身份验证

上一节介绍了如何使用共享密钥 以控制访问。 这是一种广泛的方法, 不是很安全。 它不会告诉您有关单个访问者的任何信息, 只是他或她(或有知觉的AI) 知道秘密。

许多网站希望:

  • 以某种方式定义单个访客。
  • 在访问某些端点时识别特定访问者 (身份验证)。
  • 可能为某些访问者分配不同的权限 和端点 (授权)。
  • 可能保存每个访问者的特定信息 (利息、购买等)。

如果您的“访客”是人类,您可能希望他们提供 用户名或电子邮件和密码。 如果它们是外部程序, 您可能希望他们提供 API 密钥和机密。

注意

从这里开始, 我将用户只是来引用 到用户选择的名称或 一封电子邮件。

要验证真实的个人用户而不是一个虚假的用户, 您需要执行更多操作:

  • 传递用户值(名称和密码) 到 API 服务器端点 作为 HTTP 标头。
  • 使用HTTPS而不是HTTP,以避免任何人窥探 这些标头的文本。
  • 为其他字符串。结果不是 “可去哈希” — 您无法从中获取原始密码 它的哈希值。
  • 使真正的数据库存储一个包含用户名和 哈希密码(从不原始纯文本密码)。
  • 对新输入的密码进行哈希处理并比较结果 与 数据库中的哈希密码。
  • 如果用户名和散列密码匹配, 传递匹配的用户对象 堆栈向上。 如果没有匹配项,则返回 None 或引发异常。
  • 在服务图层中, 触发任何指标/日志记录/任何内容 与单个用户身份验证相关。
  • 在 Web 图层中, 将经过身份验证的用户信息发送到任何 需要它的函数。

我将在以下各节中向您展示如何执行所有这些操作, 使用最近的工具,如OAuth2和JWT。

更高级的个人认证

如果要对个人进行身份验证, 然后,您必须在某个地方存储一些个人信息 - 例如包含以下记录的数据库 至少一个密钥(用户名或 API 密钥), 和密钥(密码或 API 密钥)。 您的网站访问者将 在访问受保护的 URL 时提供这些, 你需要数据库中的一些东西来匹配它们。

官方 FastAPI 安全文档 (和) 有关于如何设置的自上而下的描述 多个用户的身份验证, 使用本地数据库。 但 在他们的示例 Web 函数中, 他们伪造了实际的数据库访问。

在这里,我将做相反的事情: 从数据层开始 并努力工作。 我们将定义如何定义用户/访客, 存储和访问。 然后我们将工作到 Web 层, 以及如何传递用户标识, 评价 并经过身份验证。

OAuth2

OAuth 2.0,代表“开放授权”, 是旨在允许网站或应用程序的标准 代表用户访问其他 Web 应用托管的资源。

auth0.com

在早期信任网络时代,您可以提供 您网站的登录名和密码 (我们称之为B) 到另一个网站(当然是 A) 并让它为您访问 B 上的内容。 这将赋予 A 对 B 的, 尽管它被信任只能访问它是什么 应该的。 B 和资源的例子是这样的 推特关注者,脸书好友,电子邮件联系人, 等等。 当然,这不会持续太久, 因此,各种公司和团体聚集在一起定义OAuth。 它最初设计只是为了允许网站 A 访问网站 B 上的特定(不是全部)资源。

是一种流行但复杂的标准, 用途超出了上面的 A/B 示例。 关于它有很多解释,从到。

注意

曾经有一个, 但它不再使用。 一些原始的 OAuth2 建议现已推出 已弃用(计算机语言不使用)。 地平线上是,甚至更远的雾气是。

OAuth 针对不同情况提供各种。 我将在此处使用。 本节将演练一个实现, 一次一个平均大小的步骤。

首先,您需要安装一些第三方 Python 包:

  • JWT 处理:pip install python-jose[密码学]
  • 安全密码处理:点安装密码库
  • 表单处理:pip install python-multipart

以下部分从用户数据模型开始 和数据库管理, 并将熟悉的层添加到服务和 Web, OAuth 弹出的地方。

用户模型

让我们从一个非常小的用户模型定义开始 在示例 11-7 中。 这些将在所有图层中使用。

用户定义:模型/用户.py

 from   pydantic   import   BaseModel 

 class   User  (  BaseModel  ): 
     name  :   str 
     hash  :   str 

User 对象包含任意名称加上哈希字符串 (散列密码,而不是原始纯文本密码), 并且是保存在数据库中的内容。 我们需要两者来验证访问者。

用户数据层

示例 11-8 包含用户数据库代码。

注意

该代码创建用户(活动用户)和 xuser(已删除的用户)表。 开发人员通常会将布尔删除字段添加到用户表中 指示用户不再处于活动状态,没有 实际上从表中删除了记录。 我更喜欢将已删除用户的数据移动到另一个表。 这样可以避免重复检查已删除的字段 在所有用户查询中。 它还可以帮助加快查询速度: 为字段创建索引 就像布尔值没有好处一样。

数据层:数据/用户.py

 from   model.user   import   User 
 from   .init   import   (  conn  ,   curs  ,   get_db  ,   IntegrityError  ) 
 from   error   import   Missing  ,   Duplicate 

 curs  .  execute  (  """create table if not exists 
                 user( 
                   name text primary key, 
                   hash text)"""  ) 
 curs  .  execute  (  """create table if not exists 
                 xuser( 
                   name text primary key, 
                   hash text)"""  ) 

 def   row_to_model  (  row  :   tuple  )   ->   User  : 
     name  ,   hash   =   row 
     return   User  (  name  =  name  ,   hash  =  hash  ) 

 def   model_to_dict  (  user  :   User  )   ->   dict  : 
     return   user  .  dict  () 

 def   get_one  (  name  :   str  )   ->   User  : 
     qry   =   "select * from user where name=:name" 
     params   =   {  "name"  :   name  } 
     curs  .  execute  (  qry  ,   params  ) 
     row   =   curs  .  fetchone  () 
     if   row  : 
         return   row_to_model  (  row  ) 
     else  : 
         raise   Missing  (  msg  =  f  "User   {  name  }   not found"  ) 

 def   get_all  ()   ->   list  [  User  ]: 
     qry   =   "select * from user" 
     curs  .  execute  (  qry  ) 
     return   [  row_to_model  (  row  )   for   row   in   curs  .  fetchall  ()] 

 def   create  (  user  :   User  ,   table   =   "user"   |   "xuser"   =   "user"  ): 
     """Add <user> to user or xuser table""" 
     qry   =   f  """insert into   {  table  } 
         (name, hash) 
         values 
         (:name, :hash)""" 
     params   =   model_to_dict  (  user  ) 
     try  : 
         curs  .  execute  (  qry  ,   params  ) 
     except   IntegrityError  : 
         raise   Duplicate  (  msg  = 
             f  "  {  table  }  : user   {  user  .  name  }   already exists"  ) 

 def   modify  (  name  :   str  ,   user  :   User  )    ->   User  : 
     qry   =   """update user set 
              name=:name, hash=:hash 
              where name=:name0""" 
     params   =   { 
         "name"  :   user  .  name  , 
         "hash"  :   user  .  hash  , 
         "name0"  :   name  } 
     curs  .  execute  (  qry  ,   params  ) 
     if   curs  .  rowcount   ==   1  : 
         return   get_one  (  user  .  name  ) 
     else  : 
         raise   Missing  (  msg  =  f  "User   {  name  }   not found"  ) 

 def   delete  (  name  :   str  )   ->   None  : 
     """Drop user with <name> from user table, add to xuser table""" 
     user   =   get_one  (  name  ) 
     qry   =   "delete from user where name = :name" 
     params   =   {  "name"  :   name  } 
     curs  .  execute  (  qry  ,   params  ) 
     if   curs  .  rowcount   !=   1  : 
         raise   Missing  (  msg  =  f  "User   {  name  }   not found"  ) 
     create  (  user  ,   table  =  "xuser"  ) 

用户虚假数据层

示例 11-9 中的模块用于测试 排除数据库但需要一些用户数据。

假层:假/用户.py

 from   model.user   import   User 
 from   error   import   Missing  ,   Duplicate 

 # (no hashed password checking in this module) 
 fakes   =   [ 
     User  (  name  =  "kwijobo"  , 
          hash  =  "abc"  ), 
     User  (  name  =  "ermagerd"  , 
          hash  =  "xyz"  ), 
     ] 

 def   find  (  name  :   str  )   ->   User   |   None  : 
     for   e   in   fakes  : 
         if   e  .  name   ==   name  : 
             return   e 
     return   None 

 def   check_missing  (  name  :   str  ): 
     if   not   find  (  name  ): 
         raise   Missing  (  msg  =  f  "Missing user   {  name  }  "  ) 

 def   check_duplicate  (  name  :   str  ): 
     if   find  (  name  ): 
         raise   Duplicate  (  msg  =  f  "Duplicate user   {  name  }  "  ) 

 def   get_all  ()   ->   list  [  User  ]: 
     """Return all users""" 
     return   fakes 

 def   get_one  (  name  :   str  )   ->   User  : 
     """Return one user""" 
     check_missing  (  name  ) 
     return   find  (  name  ) 

 def   create  (  user  :   User  )   ->   User  : 
     """Add a user""" 
     check_duplicate  (  user  .  name  ) 
     return   user 

 def   modify  (  name  :   str  ,   user  :   User  )   ->   User  : 
     """Partially modify a user""" 
     check_missing  (  name  ) 
     return   user 

 def   delete  (  name  :   str  )   ->   None  : 
     """Delete a user""" 
     check_missing  (  name  ) 
     return   None 

用户服务层

示例 11-10 定义了 用户。 与其他服务层的区别 模块是 OAuth2 和 JWT 函数的添加。 我认为将它们放在这里比在 Web 层中更干净, 虽然有一些OAuth2 Web层 功能在即将推出中。 CRUD 函数目前仍处于直通状态, 但可以调味 未来的指标。 请注意,就像生物和探险家服务一样, 这支持运行时使用假货 或用于访问用户数据的真实数据层。

服务层:服务/用户.py

 from   datetime   imoport   timedelta 
 import   os 
 from   jose   import   jwt 
 from   model.user   import   User 

 if   os  .  getenv  (  "CRYPTID_UNIT_TEST"  ): 
     from   fake   import   user   as   data 
 else  : 
     from   data   import   user   as   data 

 # --- New auth stuff 

 from   passlib.context   import   CryptContext 

 # Change SECRET_KEY for production! 
 SECRET_KEY   =   "keep-it-secret-keep-it-safe" 
 ALGORITHM   =   "HS256" 
 pwd_context   =   CryptContext  (  schemes  =  [  "bcrypt"  ],   deprecated  =  "auto"  ) 

 def   verify_password  (  plain  :   str  ,   hash  :   str  )   ->   bool  : 
     """Hash <plain> and compare with <hash> from the database""" 
     return   pwd_context  .  verify  (  plain  ,   hash  ) 

 def   get_hash  (  plain  :   str  )   ->   str  : 
     """Return the hash of a <plain> string""" 
     return   pwd_context  .  hash  (  plain  ) 

 def   get_jwt_username  (  token  :  str  )   ->   str   |   None  : 
     """Return username from JWT access <token>""" 
     try  : 
         payload   =   jwt  .  decode  (  token  ,   SECRET_KEY  ,   algorithms  =  [  ALGORITHM  ]) 
         if   not   (  username   :=   payload  .  get  (  "sub"  )): 
             return   None 
     except   jwt  .  JWTError  : 
         return   None 
     return   username 

 def   get_current_user  (  token  :   str  )   ->   User   |   None  : 
     """Decode an OAuth access <token> and return the User""" 
     if   not   (  username   :=   get_jwt_username  (  token  )): 
         return   None 
     if   (  user   :=   lookup_user  (  username  )): 
         return   user 
     return   None 

 def   lookup_user  (  name  :   str  )   ->   User   |   None  : 
     """Return a matching User fron the database for <name>""" 
     if   (  user   :=   data  .  get  (  username  )): 
         return   user 
     return   None 

 def   auth_user  (  name  :   str  ,   plain  :   str  )   ->   User   |   None  : 
     """Authenticate user <name> and <plain> password""" 
     if   not   (  user   :=   lookup_user  (  name  )): 
         return   None 
     if   not   verify_password  (  plain  ,   user  .  hash  ): 
         return   None 
     return   user 

 def   create_access_token  (  data  :   dict  , 
     expires  :   timedelta   |   None   =   None 
 ): 
     """Return a JWT access token""" 
     src   =   data  .  copy  () 
     now   =   datetime  .  utcnow  () 
     expires   =   timedelta  (  minutes  =  15  )   if   not   expires 
     src  .  update  ({  "exp"  :   now   +   expires  }) 
     encoded_jwt   =   jwt  .  encode  (  src  ,   SECRET_KEY  ,   algorithm  =  ALGORITHM  ) 
     return   encoded_jwt 

 # --- CRUD passthrough stuff 

 def   get_all  ()   ->   list  [  User  ]: 
     return   data  .  get_all  () 

 def   get_one  (  name  )   ->   User  : 
     return   data  .  get_one  (  name  ) 

 def   create  (  user  :   User  )   ->   User  : 
     return   data  .  create  (  user  ) 

 def   modify  (  name  :   str  ,   user  :   User  )   ->   User  : 
     return   data  .  modify  (  name  ,   user  ) 

 def   delete  (  name  :   str  )   ->   None  : 
     return   data  .  delete  (  name  ) 

用户 Web 图层

示例 11-11 定义基本用户模块 在 Web 图层中。 它使用示例 11-10 中模块中的新身份验证代码。

Web 层:Web/用户.py

 import   os 
 from   fastapi   import   APIRouter  ,   HTTPException 
 from   model.user   import   User 
 if   os  .  getenv  (  "CRYPTID_UNIT_TEST"  ): 
     from   fake   import   user   as   service 
 else  : 
     from   service   import   user   as   service 
 from   error   import   Missing  ,   Duplicate 

 router   =   APIRouter  (  prefix   =   "/user"  ) 

 # --- new auth stuff 

 # This dependency makes a post to "/user/token" 
 # (from a form containing a username and password) 
 # return an access token. 
 oauth2_dep   =   OAuth2PasswordBearer  (  tokenUrl  =  "token"  ) 

 def   unauthed  (): 
     raise   HTTPException  ( 
         status_code  =  401  , 
         detail  =  "Incorrect username or password"  , 
         headers  =  {  "WWW-Authenticate"  :   "Bearer"  }, 

 # This endpoint is directed to by any call that has the 
 @   oauth2_dep  ()   dependency  : 
 @router  .  post  (  "/token"  ) 
 async   def   create_access_token  ( 
     form_data  :   OAuth2PasswordRequestForm   =    Depends  () 
 ): 
     """Get username and password from OAuth form, 
         return access token""" 
     user   =   service  .  auth_user  (  form_data  .  username  ,   form_data  .  password  ) 
     if   not   user  : 
         unauthed  () 
     expires   =   timedelta  (  minutes  =  ACCESS_TOKEN_EXPIRE_MINUTES  ) 
     access_token   =   service  .  create_access_token  ( 
         data  =  {  "sub"  :   user  .  username  },   expires  =  expires 
     ) 
     return   {  "access_token"  :   access_token  ,   "token_type"  :   "bearer"  } 

 @app  .  get  (  "/token"  ) 
 def   get_access_token  (  token  :   str   =   Depends  (  oauth2_token  ))   ->   dict  : 
     """Return the current access token""" 
     return   {  "token"  :   token  } 

 # --- previous CRUD stuff 

 @router  .  get  (  "/"  ) 
 def   get_all  ()   ->   list  [  User  ]: 
     return   service  .  get_all  () 

 @router  .  get  (  "/  {name}  "  ) 
 def   get_one  (  name  )   ->   User  : 
     try  : 
         return   service  .  get_one  (  name  ) 
     except   Missing   as   exc  : 
         raise   HTTPException  (  status_code  =  404  ,   detail  =  exc  .  msg  ) 

 @router  .  post  (  "/"  ,   status_code  =  201  ) 
 def   create  (  user  :   User  )   ->   User  : 
     try  : 
         return   service  .  create  (  creature  ) 
     except   Duplicate   as   exc  : 
         raise   HTTPException  (  status_code  =  409  ,   detail  =  exc  .  msg  ) 

 @router  .  patch  (  "/"  ) 
 def   modify  (  name  :   str  ,   user  :   User  )   ->   User  : 
     try  : 
         return   service  .  modify  (  name  ,   user  ) 
     except   Missing   as   exc  : 
         raise   HTTPException  (  status_code  =  404  ,   detail  =  exc  .  msg  ) 

 @router  .  delete  (  "/  {name}  "  ) 
 def   delete  (  name  :   str  )   ->   None  : 
     try  : 
         return   service  .  delete  (  name  ) 
     except   Missing   as   exc  : 
         raise   HTTPException  (  status_code  =  404  ,   detail  =  exc  .  msg  ) 

测试!

此新用户组件的单元和完整测试 与您已经看到的那些非常相似 生物和探险家。 而不是使用墨水和纸张 这里, 您可以在本书随附的随附内容中查看它们 网站。

顶层

上一节定义了一个新的路由器变量 对于以 /user 开头的网址, 所以示例 11-X 添加 这个子路由器。

顶层:main.py

 from   fastapi   import   FastAPI 
 from   web   import   explorer  ,   creature  ,   user 

 app   =   FastAPI  () 
 app  .  include_router  (  explorer  .  router  ) 
 app  .  include_router  (  creature  .  router  ) 
 app  .  include_router  (  user  .  router  ) 

当 uvicorn 自动重新加载时,/用户/...端点 现在应该可用。

这很有趣, 对于乐趣的一些延伸定义。 给定刚刚创建的所有用户代码, 让我们给它做点什么。

身份验证步骤

查看前面各节中的代码堆:

  • 如果终结点具有依赖项 oauth2_dep() (在), 包含用户名和密码字段的表单是 生成并发送到客户端。
  • 客户填写并提交此表格后, 用户名和密码 (使用与已存储在 本地数据库) 与本地匹配 数据库。
  • 如果匹配,则生成访问令牌 (以 JWT 格式)并返回。
  • 此访问令牌作为 后续请求中的授权 HTTP 标头。 此 JWT 令牌在本地服务器上解码为用户名 等细节。无需在 再次数据库。
  • 用户名经过身份验证,服务器可以 随心所欲。

服务器可以对这些来之不易的身份验证信息做什么?

  • 生成指标(此用户、此终端节点,这次) 帮助研究正在查看的内容、由谁查看、查看多长时间等等。
  • 保存用户特定的信息。

JWT (JSON Web Token)

本节包含有关 JWT 的一些详细信息。 你真的不需要他们使用所有早期的代码 在本章中, 但如果你有点好奇...

是一种编码方案,而不是身份验证方法。 低级别详细信息在 中定义。 它可用于传达身份验证信息 对于 OAuth2(和其他方法), 我将在这里展示这一点。

JWT 是具有三个点分隔部分的可读字符串:

  • :使用的加密算法和令牌类型
  • :...
  • : ...

每个部分由一个 JSON 字符串组成, 以 格式编码。 下面是一个示例(拆分为适合此页面的点):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

作为在 URL 中也可以安全使用的纯 ASCII 字符串, 它可以作为URL的一部分传递给Web服务器, 查询参数, HTTP头, 饼干 等等。

智威汤逊优势:

  • 避免数据库查找

因为没有解决方案适合所有问题, 弊:

  • 缺少数据库查找意味着无法检测到 直接撤销授权。

第三方身份验证:OIDC (OpenId Connect)

您经常会看到允许您登录的网站 带有一些ID和密码, 或让您通过其他站点的帐户登录, 比如谷歌、Facebook/Meta、LinkedIn或许多其他公司。 这些经常使用称为的标准, 它建立在OAuth2之上。 当您连接到启用了 OIDC 的外部时 站点,您将获得 OAuth2 访问令牌 (如本章中的示例), 但也是一个 。

官方 FastAPI 文档不包含示例代码 用于与 OIDC 集成,但如果您想尝试一下, 有一些 第三方软件包 (特定于快速 API 且更通用) 这将节省时间 滚动您自己的实现:

  • fastapi-oidc
  • fastapi-第三方身份验证
  • fastapi_resource_server
  • 奥特利布
  • 伊斯兰会议组织
  • OIDC 客户端
  • OIDC-OP
  • OpenID-Connect

包括多个代码示例, 以及蒂安杰洛(塞巴斯蒂安·拉米雷斯)的评论 快速API-OIDC示例 将来将包含在官方文档和教程中。

授权

身份验证处理(身份), 和 授权处理: 哪些资源(Web 终结点) 您是否允许访问,以何种方式访问? 和的组合数量可能非常大。

在这本书中,探险家和生物一直是主要资源。 查找资源管理器,或列出所有资源管理器, 通常比添加或修改现有更“开放”。 如果网站应该是某些数据的可靠接口, 那么写访问应该比读取访问更受限制。 因为,grr,人。

如果每个端点都完全打开, 你不需要授权, 并且可以跳过此部分。 最简单的授权可能是 一个简单的二进制与不是 - 对于本书中的示例, 您可能需要管理员授权才能添加、删除或修改 探险家或生物。 如果您的数据库有很多条目, 您可能还想限制 get_all() 函数 具有非管理员的进一步权限。 随着网站变得越来越复杂, 权限可能会变得更加细粒度。

让我们看一下授权案例的进展。 我将使用用户表 (其中名称可以是电子邮件、用户名或 API 密钥) (“对”表是关系数据库的方式 匹配来自两个单独表的条目):

  • 如果您只想跟踪管理员访问者, 其余的保持匿名:
    • 经过身份验证的用户名的管理表。 在本章前面的代码示例中, 您会从管理表中查找名称, 如果匹配,则比较散列密码 从用户表中。
  • 如果进行身份验证, 但您只需要为某些端点授权管理员:
    • 对每个人进行身份验证,如前面的示例所示 (来自用户表), ,然后检查“管理”表以查看此用户是否也是管理员。
  • 对于多种类型的权限 (如只读、读、写):
    • 权限定义表。
    • 配对的用户权限表 用户和权限。 这有时称为。
  • 如果权限组合很复杂, 添加级别并定义(独立的权限集):
    • 角色表。
    • 将用户和角色条目配对的用户角色表。 这有时称为 (基于角色的访问控制)。

中间件

FastAPI 允许在 Web 层插入代码,该代码:

  • 截获请求
  • 对请求执行某些操作
  • 将请求传递给路径函数
  • 截获补丁函数返回的响应
  • 对响应执行某些操作
  • 将响应返回给调用方

它类似于Python装饰器对函数的作用。 它“包裹”。

在某些情况下,您可以使用枯萎中间件或 使用 Depends() 进行依赖注入。 中间件更方便解决更多全局安全问题,例如 CORS,这带来了...

科尔斯

(跨源资源共享)涉及通信 在其他受信任的服务器和您的网站之间。 如果您的网站将所有前端和后端代码集中在一个地方, 那就没问题了。 但是现在,拥有JavaScript前端是很常见的。 与用 FastAPI 等内容编写的后端通信。 这些服务器将不具有相同的:

  • 协议:http 或 https
  • 域:互联网域,如 google.com 或本地主机
  • 端口:该域上的数字 TCP/IP 端口,如 80、443 或 8000。

后端如何从一盒中知道可信任的前端 发霉的萝卜,还是胡子旋转的攻击者? 这是 CORS 的工作,它指定后端信任的内容, 最突出的是:

  • 起源
  • HTTP 方法
  • HTTP 标头
  • CORS 缓存超时

您可以在 Web 级别挂钩到 CORS。 示例 11-X 显示如何仅允许一个前端服务器, (带域 ) 以及任何 HTTP 标头或方法:

激活 CORS 中间件

 from   fastapi   import   FastAPI  ,   Request 
 from   fastapi.middleware.cors   import   CORSMiddleware 

 app   =   FastAPI  () 

 app  .  add_middleware  ( 
     CORSMiddleware  , 
     allow_origins  =  [  "https://ui.cryptids.com"  ,], 
     allow_credentials  =  True  , 
     allow_methods  =  [  "*"  ], 
     allow_headers  =  [  "*"  ], 
     ) 

 @app  .  get  (  "/test_cors"  ) 
 def   test_cors  (  request  :   Request  ): 
     print  (  request  ) 

完成后,任何其他尝试联系您的域 后端站点将直接被拒绝。

第三方套餐

您现在已经阅读了一些如何编码的示例 使用 FastAPI 的身份验证和授权解决方案。 但也许你不需要自己做所有事情。 FastAPI生态系统正在快速增长,并且可能会有 是可用的软件包,为您完成大量工作。

以下是一些未经测试的示例。 无法保证此列表中的任何包 随着时间的推移,仍将存在并支持, 但它们可能值得一看:

  • 快餐用户
  • fastapi-jwt-auth
  • 快速登录
  • fastapi-auth0
  • 身份验证
  • fastapi-user-auth
  • 法斯塔皮-奥特兹
  • 法斯塔皮-奥帕
  • fastapi-key-auth
  • 快速身份验证中间件
  • 法斯塔皮-智威汤逊
  • fastapi_auth2
  • 法斯塔皮-索
  • 采邑

回顾

这是比大多数章节都沉重的章节。 它展示了一些可以对访问者进行身份验证的方法, 并授权他们做某些事情。 这是网络安全的两个方面, 然后是关于CORS的一些讨论。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表