背景

  • 假设在某个域中拥有后端 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. BeanUtils中的自动类型转换(二)

    javabean package entity; import java.util.Date; /** * 一个测试用: * student,javaBean * @author mzy * 一个标准 ...

  2. Linux c高级

    目录 一.Linux 1.1.嵌入式 1.2.什么是Linux 1.3.Linux发行版 1.4.Linux体系结构 1.5.虚拟4G内存 1.6.shell 命令 1.7.软件包的管理 1.8.图形 ...

  3. mybatis第一个程序随笔

    今天继续学习了解如何写一个mybatis程序 创建了Dao层 1.1 创建一个UserDao接口 1.2 创建UserMapper.xml文件 在mybaits中文手册查找配置信息 <?xml ...

  4. Java并发之AQS原理解读(二)

    上一篇: Java并发之AQS原理解读(一) 前言 本文从源码角度分析AQS独占锁工作原理,并介绍ReentranLock如何应用. 独占锁工作原理 独占锁即每次只有一个线程可以获得同一个锁资源. 获 ...

  5. Spring源码浅析之bean实例的创建过程(二)

    在上一篇内容中,介绍了doGetBean方法的源码内容,知道了bean在创建的过程中,有三个范围,单例.多例.Scope,里面都使用到了createBean.下面本篇文章的主要内容,就是围绕creat ...

  6. leaflet加载离线OSM(OpenStreetMap)

    本文为博主原创,如需转载需要署名出处. leaflet作为广为应用的开源地图操作的API,是非常受欢迎,轻量级的代码让使用者更容易操作. 废话不多说,下面直接给出范例. 首先在这个网站下载leafle ...

  7. 磁盘“Seagate”没有被推出,因为一个或多个程序可能正在使用它。

    推出移动硬盘失败,解决方案: 执行 lsof /Volumes/Seagate/ 可以看到哪些进程在占用磁盘 $ lsof /Volumes/Seagate/ COMMAND PID USER FD ...

  8. vue-class和style样式绑定

    前言 操作元素的 class 样式列表和 style 内联样式为数据绑定是前端开发中一个常见的需求,这些样式都属于元素的属性 attribute ,因此我们可以通过 v-bind 来动态绑定元素的样式 ...

  9. Windos下通过Wpcap抓包实现两个网卡桥接

    目录 1. 背景: 2. 需要的技术手段: 3. 实现逻辑: 4. 应用实例: 1. 背景: 一台电脑允许接多个网口,当然大部分只有一个网口其余都是USB扩展而来,而每个网口之间需要配置不同的网段IP ...

  10. JAVA安全基础之代理模式(二)

    JAVA安全基础之代理模式(二) 上篇讲到静态代理模式,这时候我们发现,一个代理类只能为一个类服务,如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐.所以就有了动态代理 动态代理 动态代理的 ...