背景

  • 假设在某个域中拥有后端 API(127.0.0.1:8080)
  • 并且在另一个域或同一域的不同路径(或移动应用程序)中有一个前端(127.0.0.1:8081)
  • 并且希望有一种方法让前端使用用户名和密码与后端进行身份验证
  • 可以使用 OAuth2 通过 FastAPI 来构建它,通过 FastAPI 提供的工具来处理安全性

OAuth2 的授权模式

  • 授权码授权模式 Authorization Code Grant
  • 隐式授权模式 Implicit Grant
  • 密码授权模式 Resource Owner Password Credentials Grant
  • 客户端凭证授权模式 Client Credentials Grant

这里讲 FastAPI 的是第三种

密码授权模式的简易流程图

  1. 用户在客户端输入用户名、密码
  2. 客户端携带用户名、密码去请求授权服务器,访问获取 token 的接口
  3. 授权服务器验证用户名、密码(身份验证)
  4. 验证通过后,返回这个用户的 token 到客户端
  5. 客户端存储 token,在后续发送请求携带该 token,就能通过身份验证了

FastAPI 中使用 OAuth2 的简单栗子

import uvicorn
from fastapi import FastAPI, Depends
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} if __name__ == '__main__':
uvicorn.run(app="49_bearer:app", reload=True, host="127.0.0.1", port=8080)

代码解析

  • OAuth2 旨在使后端或 API 可以独立于对用户进行身份验证的服务器
  • 但在这种情况下,同一个 FastAPI 应用程序将同时处理 API 和身份验证
  • 前端请求 /items 的之前要先进行身份验证,也就是用户名和密码,这个验证的路径就是 tokenUrl,是相对路径,POST请求
  • oauth2_scheme 中接收一个 str 类型的 token,就是当验证通过后,要返回给客户端的一个令牌(常说的 token)
  • 方便下次请求携带这个 token 就可以通过身份认证,这个 token 有过期时间,过期后需要重新验证

OAuth2PasswordBearer

  • 使用 OAuth2、密码授权模式、Bearer Token(不记名 token),就是通过 OAuth2PasswordBearer 来完成
  • OAuth2PasswordBearer 是接收 URL 作为参数的一个类
  • 客户端会向该 URL 发送 username 和 password 参数(通过表单的格式发送),然后得到一个 token 值
  • OAuth2PasswordBearer 并不会创建相应的 URL 路径操作,只是指明了客户端用来获取 token 的目标 URL

tokenUrl 是相对路径

  • 如果 API 位于 https://example.com/,那么它将引用 https://example.com/token
  • 如果API 位于 https://example.com/api/v1/,那么它将引用 https://example.com/api/v1/token

oauth2_scheme

该变量是 OAuth2PasswordBearer 的一个实例,但它也是一个可调用对象,所以它可以用于依赖项

async def read_items(token: str = Depends(oauth2_scheme)):

OAuth2PasswordBearer 会做什么

  • 客户端发送请求的时候,FastAPI 会检查请求的 Authorization 头信息,如果没有找到 Authorization 头信息
  • 或者头信息的内容不是 Bearer token,它会返回 401 状态码( UNAUTHORIZED )

传递 token 的请求结果

目前因为没有对 token 做验证,所以 token 传什么值都可以验证通过

看看 OAuth2PasswordBearer 的源码

查看 Swagger API 文档

多了个 Authorize 按钮,点击它

可以看到一个包含用户名、密码还有其他可选字段的授权表单

上述代码的问题

还没有获取 token 的路径操作

完善 OAuth2

#!usr/bin/env python
# -*- coding:utf-8 _*-
"""
# author: 小菠萝测试笔记
# blog: https://www.cnblogs.com/poloyy/
# time: 2021/10/6 12:05 下午
# file: 49_bearer.py
"""
from typing import Optional import uvicorn
from fastapi import FastAPI, Depends, 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() oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # 模拟 hash 加密算法
def fake_hase_password(password: str) -> str:
return "fakehashed" + password # 返回给客户端的 User Model,不需要包含密码
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None # 继承 User,用于密码验证,所以要包含密码
class UserInDB(User):
hashed_password: str # OAuth2 获取 token 的请求路径
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# 1、获取客户端传过来的用户名、密码
username = form_data.username
password = form_data.password
# 2、模拟从数据库中根据用户名查找对应的用户
user_dict = fake_users_db.get(username)
if not user_dict:
# 3、若没有找到用户则返回错误码
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="用户名或密码不正确") # 4、找到用户
user = UserInDB(**user_dict)
# 5、将传进来的密码模拟 hash 加密
hashed_password = fake_hase_password(password)
# 6、如果 hash 后的密码和数据库中存储的密码不相等,则返回错误码
if not hashed_password == user.hashed_password:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="用户名或密码不正确") # 7、用户名、密码验证通过后,返回一个 JSON
return {"access_token": user.username, "token_type": "bearer"} # 模拟从数据库中根据用户名查找用户
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict) # 模拟验证 token,验证通过则返回对应的用户信息
def fake_decode_token(token):
user = get_user(fake_users_db, token)
return user # 根据当前用户的 token 获取用户,token 已失效则返回错误码
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = fake_decode_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user # 判断用户是否活跃,活跃则返回,不活跃则返回错误码
async def get_current_active_user(user: User = Depends(get_current_user)):
if user.disabled:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid User")
return user # 获取当前用户信息
@app.get("/user/me")
async def read_user(user: User = Depends(get_current_active_user)):
return user # 正常的请求
@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
return {"token": token} if __name__ == '__main__':
uvicorn.run(app="49_bearer:app", reload=True, host="127.0.0.1", port=8080)

/token 路径操作函数的响应

# 7、用户名、密码验证通过后,返回一个 JSON
return {"access_token": user.username, "token_type": "bearer"}
  • 获取 token 的接口的响应必须是一个 JSON 对象(返回一个 dict 即可)
  • 它应该有一个 token_type,当使用 Bearer toklen 时,令牌类型应该是 bearer
  • 它应该有一个 access_token,一个包含访问 token 的字符串
  • 对于上面简单的例子,返回的 token 是用户名,这是不安全,只是作为栗子好理解一点

返回 401 的HTTPException

# 根据当前用户的 token 获取用户,token 已失效则返回错误码
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = fake_decode_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user
  • 任何 HTTP(错误)状态码为 401 UNAUTHORIZED 都应该返回 WWW-Authenticate 的 Header
  • 在此处返回的带有值 Bearer 的 WWW-Authenticate Header 也是 OAuth2 规范的一部分
  • 在 Beaer token 的情况下,该值应该是 Bearer
  • 当然,这并不是必须的,但建议符合规范

查看 Swagger API Authorize

验证通过

请求 /user/me 的结果

请求头带上了 'Authorization: Bearer johndoe'

logout 后再次请求,查看结果

logout 之后,请求头没有 'Authorization: Bearer johndoe' 所以验证就失败啦

验证一个不活跃的用户

authenticate 表单填入

  • username:alice
  • password:secret2

请求 /users/me

得到的响应

{
"detail": "Inactive user"
}

存在的问题

目前的 token 和验证方式并不安全,下一篇中将介绍 JWT token

FastAPI(58)- 使用 OAuth2PasswordBearer 的简单栗子的更多相关文章

  1. FastAPI(59)- 详解使用 OAuth2PasswordBearer + JWT 认证

    JWT JSON Web Tokens 它是一个将 JSON 对象编码为密集且没有空格的长字符串的标准 使用 JWT token 和安全密码 hash 使应用程序真正安全 JWT 小栗子 eyJhbG ...

  2. FastAPI 学习之路(二十八)使用密码和 Bearer 的简单 OAuth2

    OAuth2 规定在使用(我们打算用的)「password 流程」时,客户端/用户必须将 username 和 password 字段作为表单数据发送.我们看下在我们应该去如何实现呢. 我们写一个登录 ...

  3. Binder机制-简单用法(一)

    Binder算是android里面比较难懂的部分了,但是非常重要,基本上,当我们深入到进程交互的阶段,Binder都是一个绕不开的槛,所以我也希望帮助大家更浅显地了解到这个知识点.笔者想通过3篇博文简 ...

  4. FastAPI 学习之路(二十七)安全校验

    你写API接口肯定你是希望是有权限的人才能访问,没有权限的人是不能访问的,那么我们应该如何去处理呢,我们可以用的验证方式有很多,我们这次分享的是用:OAuth2来认证.那么我们看下,需要怎么才能实现呢 ...

  5. FastAPI快速查阅

    官方文档主要侧重点是循序渐进地学习FastAPI, 不利于有其他框架使用经验的人快速查阅 故本文与官方文档不一样, 并补充了一些官方文档没有的内容 安装 包括安装uvicorn $pip instal ...

  6. 【Android UI设计与开发】4.底部菜单栏(一)Fragment介绍和简单实现

    TabActivity在Android4.0以后已经被完全弃用,取而代之的是Fragment.Fragment是Android3.0新增的概念,Fragment翻译成中文是碎片的意思,不过却和Acti ...

  7. if最简单的用法

    /* Name:if最简单的用法-1 Copyright:By.不懂网络 Author: Yangbin Date:2014年2月9日 03:00:58 Description:if最简单的用法,真则 ...

  8. java模式:模板模式的简单理解

    1.模板模式就是用虚类作为基类将几个要执行差不多操作中相同的部分提取出来,不同的部分各自实现! 2.下面给出简单栗子: 我要进行的操作是将大象和狐狸放入冰箱,放入大象和狐狸有相同的步骤:开冰箱和关冰箱 ...

  9. python - 常用模块栗子

    前言  内容摘自Python参考手册(第4版) 及 自己测试后得出.仅作为一个简单记录,方便使用查询之用. 另 dir(func)及func.__doc__可以查看对象属性及说明文档. 序列Seque ...

随机推荐

  1. CentOS 6.x 系统中安装原生 Hadoop 2

    2020年整理博客发现原文地址已经失效,推荐学习地址厦门大学数据库实验室 本教程适合于在 CentOS 6.x 系统中安装原生 Hadoop 2,适用于Hadoop 2.7.1, Hadoop 2.6 ...

  2. Timer和TimerTask(转载)

    下面内容转载自: http://blog.csdn.net/xieyuooo/article/details/8607220 其实就Timer来讲就是一个调度器,而TimerTask呢只是一个实现了r ...

  3. 多线程编程<一>

    1 /** 2 * 通过制定synchronized限定符,可以同步用于对象的一个或多个方法.当调用同步的方法时,对象会被加锁,直到方法返回. 3 * @author Burke 4 * 5 */ 6 ...

  4. ORB_SLAM2 Tracking流程

  5. Focal Loss(RetinaNet)笔记 一种减小类别不平衡影响的方法

    Paper: https://arxiv.org/abs/1708.02002 还参考了:https://www.jianshu.com/p/8e501a159b28 其中p是预测属于某类的概率.

  6. 云效x钉钉:让研发工作更简单

    云效x钉钉:让研发工作更简单,奔走相告,云效&钉钉集成实现组织架构.成员同步以及消息通知啦! 我们知道云效致力于智能化.安全可追溯.高效.简单.灵活,「云效新一代企业级DevOps平台」阿里云 ...

  7. shell中的引号

    单引号: 所见即所得 原封不动输出 双引号: 与单引号类似 特殊符号进行解析 ( $ $() `` ! ) 无引号: 与双引号类似 支持通配符( {} * ) 反引号: 优先执行 优先执行里面的命令, ...

  8. Django使用富文本编辑器ckediter

    1 - 安装 pip install django-ckeditor 2 - 注册APP ckeditor 3 - 由于djang-ckeditor在ckeditor-init.js文件中使用了JQu ...

  9. Abp Vnext3 vue-admin-template(一用户登录)

    Git地址https://github.com/PanJiaChen/vue-admin-template/blob/master/README-zh.md 官方文档https://panjiache ...

  10. 忘记root密码的情况下如何给指定账户开通远程访问

    1.跳过验证使用root登录 net stop mysql //停止MYSQL服务 打开第一个cmd窗口,切换到mysql的bin目录,运行命令: mysqld --defaults-file=&qu ...