与Write with Fauna程序有关。

本文重点介绍使用 Fauna 的内置身份验证功能和 PyOTP 在使用 Python 和 Flask 构建的 Web 应用程序中实现双因素身份验证,以生成和验证一次性密码。

Fauna是一个灵活、对开发人员友好的事务性云数据库,作为安全数据 API 交付,提供两个接口:GraphQL 和 Fauna 查询语言 (FQL),提供存储集合、索引和其他数据库(多-租赁)。它还提供内置的用户身份验证和会话管理功能。要了解有关 Fauna 的更多信息,请访问官方文档。

本文包括将 Fauna 的身份验证功能集成到 Flask Web 应用程序的分步指南。您还将为用户身份验证添加另一层安全性,并使用 PyOTP 对具有多个因素的用户进行身份验证。要了解有关 PyOTP 的更多信息,请访问官方文档。

设置动物数据库

创建动物数据库

您需要在 Fauna 的仪表板中为 Web 应用程序创建数据库。如果您之前没有在 Fauna 上创建帐户,请在Fauna 的网站上创建一个。

在仪表板中,单击NEW DATABASE按钮,为您的数据库提供一个名称,然后按SAVE按钮。

[](https://res.cloudinary.com/practicaldev/image/fetch/s--7OwVgx9b--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/ugrnwcg326xc0cobtx0d.png)

创建数据库集合

您需要创建一个 Fauna 集合来存储刚创建的数据库中收集的数据。集合类似于包含相似特征的 SQL 表,例如,包含数据库中用户信息的用户集合。

要创建集合,请导航到 Fauna 侧栏(屏幕左侧)上的Collections选项卡,单击NEW COLLECTION按钮,为要创建的集合提供名称,然后按SAVE按钮。

[](https://res.cloudinary.com/practicaldev/image/fetch/s--cmDS8xFE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/lcosgw75bwfj2mcvj5rv.png)

创建集合索引

您需要为数据库的集合创建索引。动物区系索引允许您根据特定属性浏览存储在数据库集合中的数据。

要创建索引,请前往 Fauna 侧栏(屏幕左侧)上的Indexes选项卡,单击NEW INDEX按钮,提供所需信息,然后按SAVE按钮。

[](https://res.cloudinary.com/practicaldev/image/fetch/s--ZwJfaQFP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/ovdyex0lg1n6sotfzj5p.png)

生成数据库安全密钥

现在,您需要创建一个security key以使用任何Fauna 的驱动程序将您的数据库连接到任何应用程序。前往 Fauna 侧边栏(屏幕左侧)上的Security选项卡,单击NEW KEY按钮,提供所需信息,然后按SAVE按钮。

[](https://res.cloudinary.com/practicaldev/image/fetch/s--O0Ba1Qbw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/rjyqcly4uecbilj4l43v.png)

完成此操作后,Fauna 将显示您的Secret Key。您应该在 Fauna 生成密钥后立即复制密钥并将其存储在安全且易于检索的地方,因为 Fauna 只会显示一次。

将 Fauna 集成到 Python 中

接下来,您需要获取 Fauna 的 Python 驱动程序。您可以从PIP安装它,在您的终端中使用一行。

pip install faunadb

进入全屏模式 退出全屏模式

在此之后,您应该运行Fauna Python 驱动程序文档中提供的示例代码。

from faunadb import query as q  
from faunadb.objects import Ref  
from faunadb.client import FaunaClient   

client = FaunaClient(secret="your-secret-here")  

indexes = client.query(q.paginate(q.indexes()))  

print(indexes)

进入全屏模式 退出全屏模式

上面的代码显示了 Fauna Python 驱动程序如何使用其Secret Key连接到数据库并打印与其关联的索引。运行此代码的结果应如下图所示。

[](https://res.cloudinary.com/practicaldev/image/fetch/s--cyHxscQK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/01gniyxiqxievoizvjby.png)

带动物群的单因素身份验证

现在您已经成功地设置了您的 Fauna 数据库并与它集成了一个 Python 脚本,让我们开始使用 Web 应用程序并实现 Fauna 的内置身份验证。应用程序后端将使用Flask构建,而用户界面将使用Flask-Bootstrap构建。

您将为该应用程序设计四 (4) 个网页。他们是:

  • 索引/登陆页面

  • 注册页面

  • 登录页面

  • 认证成功页面

第一步:安装项目的需求

我们需要安装 Flask 库和 Flask-Bootstrap。我们将按照之前使用 PIP 安装 Fauna 的方式进行操作。在您的终端中,键入:

pip install Flask Flask-Bootstrap4

进入全屏模式 退出全屏模式

第 2 步:设置 Flask 服务器

创建一个名为app.py的文件,并将后续代码保存在其中:

from flask import *
from flask_bootstrap import Bootstrap

app = Flask(__name__)
Bootstrap(app)


@app.route("/")
def index():
    return "Hello World!"


if __name__ == "__main__":
    app.run(debug=True)

进入全屏模式 退出全屏模式

当您运行app.py文件并打开浏览器时,您将收到类似于下图的响应:

[](https://res.cloudinary.com/practicaldev/image/fetch/s--XRkSOUSw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/t8a12zr54ds4oftm1xb8.png)

[](https://res.cloudinary.com/practicaldev/image/fetch/s--2xAZKXMy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/jxj5ablohqpg7kqculba.png)

第三步:设计登陆页面

登录页面是您的用户查看应用程序信息的地方,其中包含指向应用程序内各种路线的导航链接。

首先,在与app.py相同的文件夹中创建一个名为templates的文件夹。Flask 使用templates文件夹来存储服务器在应用程序中呈现的 HTML 文件。您的项目文件夹应类似于下图:

[](https://res.cloudinary.com/practicaldev/image/fetch/s--wF90_nBC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/96dwv4rqelsa6im0y8vg.png)

创建另一个名为index.html的文件,该文件将存储在templates文件夹中,并在其中保存以下代码:

{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Fauna Two-Factor Authentication (2FA) Demo</h2>
      </div>
    </div>
    <div class="col-lg-9 text-center">
      <h5>This is a Python + Flask demo application implementing two-factor authentication using Fauna's built-in authentication and PyOTP.</h5>
    </div>
    <div class="col-lg-5 text-center">
      <a href="{{ url_for('register') }}" class="btn btn-success m-3">GET STARTED</a>
    </div>
  </div>
</div>
{% endblock %}

进入全屏模式 退出全屏模式

您还需要更新app.py文件以实现登录页面中引用的register路由。使用以下代码更新app.py文件:

@app.route("/")
def index():
    return render_template("index.html")


@app.route("/register/")
def register():
    return "Register Page!"


@app.route("/login/")
def login():
    return "Login Page!"

进入全屏模式 退出全屏模式

当您运行app.py文件时,您将在浏览器中收到如下图所示的响应:

[](https://res.cloudinary.com/practicaldev/image/fetch/s--LA3fYnDv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/y9loz3z6p9kbkyzw7x2q.png)

第四步:建立注册页面

注册页面是您的用户在应用程序上创建新帐户的地方。

要创建 Fauna 将帮助管理身份验证的新用户配置文件,您需要在您的集合中创建一个新用户文档,其中包含凭据参数中的用户电子邮件地址和密码。以下是演示此功能的示例 Python 代码:

client.query(
    q.create(
        q.collection("users"), {
            "credentials": {"password": "secret password"},
            "data": {"email": "test@test.com"}
        }
    )
)

进入全屏模式 退出全屏模式

需要更新app.py文件中的register路由来渲染注册页面模板;使用以下代码更新register路由:

@app.route("/register/")
def register():
    return render_template("register.html")

进入全屏模式 退出全屏模式

创建另一个名为register.html的文件,该文件将存储在templates文件夹中,并在其中保存以下代码:

{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Fauna Two-Factor Authentication (2FA) Demo</h2>
        <h5>Create an account to get started!</h5>
      </div>
    </div>

    <div class="col-lg-6">
      {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
      {% for category, message in messages %}
      <div class="alert alert-{{ category }}" role="alert">
        {{ message }}
      </div>
      {% endfor %}
      {% endif %}
      {% endwith %}
      <form method="POST">
        <div class="form-group">
          <label for="email">Email Address</label>
          <input type="email" class="form-control" id="email" name="email" placeholder="Enter Email Address" required>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input type="password" class="form-control" id="password" name="password" placeholder="Choose Password" required>
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-success">Create Account</button>
          <a href="{{ url_for('login') }}"><small class="form-text text-success">Already have an account? Login</small></a>
        </div>
      </form>
    </div>
  </div>
</div>
{% endblock %}

进入全屏模式 退出全屏模式

当您运行app.py文件时,您的注册页面应如下图所示:

[](https://res.cloudinary.com/practicaldev/image/fetch/s--fB11IVkX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/pcnmosfj5l5mrjrmbkcl.png)

您需要导入应用程序所需的库。使用以下代码更新app.py文件导入:

from faunadb import query as q  
from faunadb.objects import Ref  
from faunadb.client import FaunaClient  
from faunadb.errors import BadRequest, Unauthorized

进入全屏模式 退出全屏模式

您还需要配置应用程序配置(Fauna 密钥、应用程序密钥)。使用以下代码更新app.py文件:

app.config["SECRET_KEY"] = "APP_SECRET_KEY"  
client = FaunaClient(secret="FAUNA_SECRET_KEY")

进入全屏模式 退出全屏模式

接下来,您将更新register路由以记录新用户创建详细信息。使用以下代码更新app.py文件:

@app.route("/register/", methods=["GET", "POST"])
def register():
    if request.method == "POST":
        email = request.form.get("email").strip().lower()
        password = request.form.get("password")

        try:
            result = client.query(
                q.create(
                    q.collection("users"), {
                        "credentials": {"password": password},
                        "data": {"email": email}
                    }
                )
            )
        except BadRequest as e:
            flash("The account you are trying to create already exists!", "danger")
            return redirect(url_for("register"))

        flash(
            "You have successfully created your account, you can proceed to login!", "success")
        return redirect(url_for("register"))

    return render_template("register.html")

进入全屏模式 退出全屏模式

[](https://res.cloudinary.com/practicaldev/image/fetch/s--ZqCkgmxn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/r3wa51lwpfx1t1xmkfbc.png)

步骤 5:构建身份验证页面

身份验证页面是用户向应用程序提供其凭据并授予授权的地方。

要使用 Fauna 验证存储的用户配置文件,您需要提供用户的电子邮件地址和密码,然后使用登录功能。以下是演示此功能的示例 Python 代码:

client.query(
    q.login(
        q.match(q.index("users_by_email"), "test@test.com"), {
            "password": "secret password"}
    )
)

进入全屏模式 退出全屏模式

创建一个名为login.html的文件,该文件将存储在templates文件夹中,并在其中保存以下代码:

{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Fauna Two-Factor Authentication (2FA) Demo</h2>
        <h5>Login to access your account!</h5>
      </div>
    </div>

    <div class="col-lg-6">
      {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
      {% for category, message in messages %}
      <div class="alert alert-{{ category }}" role="alert">
        {{ message }}
      </div>
      {% endfor %}
      {% endif %}
      {% endwith %}
      <form method="POST">
        <div class="form-group">
          <label for="email">Email Address</label>
          <input type="email" class="form-control" id="email" name="email" placeholder="Enter Email Address" required>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input type="password" class="form-control" id="password" name="password" placeholder="Enter Account Password" required>
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-success">Access Account</button>
          <a href="{{ url_for('register') }}"><small class="form-text text-success">Don't have an account? Register</small></a>
        </div>
      </form>
    </div>
  </div>
</div>
{% endblock %}

进入全屏模式 退出全屏模式

接下来,您将更新login路由以将 Fauna 的内置身份验证集成到我们的应用程序中。使用以下代码更新app.py文件:

@app.route("/login/", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        email = request.form.get("email").strip().lower()
        password = request.form.get("password")

        try:
            result = client.query(
                q.login(
                    q.match(q.index("users_by_email"), email), {
                        "password": password}
                )
            )
        except BadRequest as e:
            flash(
                "You have supplied invalid login credentials, please try again!", "danger")
            return redirect(url_for("login"))

        session["user_secret"] = result["secret"]
        return redirect(url_for("auth_success"))

    return render_template("login.html")


@app.route("/auth-success/")
def auth_success():
    return "<h1>Successfully authenticated account using Fauna!</h1>"

进入全屏模式 退出全屏模式

[](https://res.cloudinary.com/practicaldev/image/fetch/s--L_sY-LBa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/9zbt3p5kes6tnocjaurv.png)

[](https://res.cloudinary.com/practicaldev/image/fetch/s--OmvdDMWt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/9m1vluvwsml35x7xklpy.png)

第六步:在Flask中实现授权规则

您可能希望auth_success路由只能由成功验证自己的用户访问。任何人都可以访问该页面并获得所需的响应,但不应该这样。

您需要将functools库导入应用程序,以便为其路由创建自定义包装器。使用以下代码更新app.py文件导入:

from functools import wraps

进入全屏模式 退出全屏模式

接下来,您将使用以下代码更新app.py文件,以定义自定义包装器以确保用户通过身份验证:

def login_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if "user_secret" in session:
            try:
                user_client = FaunaClient(secret=session["user_secret"])
                result = user_client.query(
                    q.current_identity()
                )
            except Unauthorized as e:
                flash("Your login session has expired, please login again!", "danger")
                return redirect(url_for("login"))
        else:
            flash("You need to be logged in before you can access here!", "danger")
            return redirect(url_for("login"))
        return f(*args, **kwargs)

    return decorated

进入全屏模式 退出全屏模式

现在您已经定义了自定义包装器。您需要在需要它的路线中实现它。使用以下代码更新您的auth_success路线:

@app.route("/auth-success/")
@login_required
def auth_success():
    return "<h1>Successfully authenticated account using Fauna!</h1>"

进入全屏模式 退出全屏模式

[](https://res.cloudinary.com/practicaldev/image/fetch/s--bujU4NTs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/drewu5mrbfwpjb44y5qs.png)

使用 PyOTP 的双因素身份验证

使用 Fauna 设置身份验证后,您希望集成两因素身份验证。在本教程中,您将使用PyOTP,这是一个生成和验证一次性密码的 Python 库。

您将为该应用程序设计两 (2) 个额外的网页。他们是:

  • 2FA 注册页面

  • 2FA验证页面

第 1 步:安装 PyOTP 库

在您的终端中,键入:

pip install pyotp

进入全屏模式 退出全屏模式

第 2 步:更新旧的身份验证流程

您需要更新之前的身份验证流程以适应新的两因素身份验证更新。在注册新用户帐户并重定向他们以在他们的仪表板之前验证他们的 OTP 时,您将生成一个 2FA 种子令牌。

首先,将 PyOTP 库导入到应用程序的代码中。使用以下代码更新app.py文件导入:

import pyotp

进入全屏模式 退出全屏模式

接下来,使用以下代码更新app.py文件中register路由中的 Fauna 查询,以在用户配置文件旁边生成 2FA 种子令牌:

try:
    result = client.query(
        q.create(
            q.collection("users"), {
                "credentials": {"password": password},
                "data": {
                    "email": email,
                    "auth_enrolled": False,
                    "auth_secret": pyotp.random_base32()
                }
            }
        )
    )
except BadRequest as e:
    flash("The account you are trying to create already exists!", "danger")
    return redirect(url_for("register"))

进入全屏模式 退出全屏模式

[](https://res.cloudinary.com/practicaldev/image/fetch/s--FITtMU7U--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads. s3.amazonaws.com/uploads/articles/s95v2zf5t8lhmy9bhsyz.png)

auth_enrolled参数存储当前用户是否注册,而auth_secret生成并验证 2FA 令牌。

您还将更新login路由以将用户重定向到 2FA 验证页面,而不是之前的auth_success路由。

session["user_secret"] = result["secret"]  
session["verified_2fa"] = False  
return redirect(url_for("verify_2fa"))

进入全屏模式 退出全屏模式

您还需要更新app.py文件以实现verify_2fa路由,应用程序将在成功的单因素身份验证后将用户重定向到。使用以下代码更新app.py文件:

@app.route("/2fa/verify/")
@login_required
def verify_2fa():
    return "Verify 2FA Page!"

进入全屏模式 退出全屏模式

接下来,您将在应用程序中实施一些授权规则,例如阻止未注册 2FA 的用户访问verify_2fa路由。为此,您需要一个处理此过程的自定义包装器。使用以下代码更新app.py文件:

def get_user_details(user_secret):
    user_client = FaunaClient(secret=user_secret)
    user = user_client.query(
        q.current_identity()
    )
    user_details = client.query(
        q.get(
            q.ref(q.collection("users"), user.id())
        )
    )
    return user_details


def auth_enrolled(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        user_details = get_user_details(session["user_secret"])
        if not user_details["data"]["auth_enrolled"]:
            return redirect(url_for("enroll_2fa"))
        return f(*args, **kwargs)

    return decorated


def auth_not_enrolled(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        user_details = get_user_details(session["user_secret"])
        if user_details["data"]["auth_enrolled"]:
            return redirect(url_for("verify_2fa"))
        return f(*args, **kwargs)

    return decorated

进入全屏模式 退出全屏模式

现在您已经定义了自定义包装器。您需要在需要它的路线中实现它。使用以下代码更新您的verify_2fa路线:

@app.route("/2fa/verify/")
@login_required
@auth_enrolled
def verify_2fa():
    return "Verify 2FA Page!"


@app.route("/2fa/enroll/")
@login_required
@auth_not_enrolled
def enroll_2fa():
    return "Enroll 2FA Page!"

进入全屏模式 退出全屏模式

第 3 步:构建 2FA 注册页面

2FA 注册页面是向用户提供他们的 2FA 秘密令牌的地方,以便在他们的身份验证器应用程序上注册并在 Web 应用程序上进行测试。

您需要更新app.py文件中的enroll_2fa路由以呈现注册页面模板;使用以下代码更新enroll_2fa路由:

@app.route("/2fa/enroll/")
@login_required
@auth_not_enrolled
def enroll_2fa():
    user_details = get_user_details(session["user_secret"])
    secret_key = user_details["data"]["auth_secret"]

    return render_template("enroll_2fa.html", secret=secret_key)

进入全屏模式 退出全屏模式

创建另一个名为enroll_2fa.html的文件,该文件将存储在templates文件夹中,并将以下代码保存在其中:

{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Fauna Two-Factor Authentication (2FA) Demo</h2>
        <h5>Set Up TOTP 2FA!</h5>
      </div>
    </div>

    <div class="col-lg-5">
      <form>
        <div>
          <h5>Instructions!</h5>
          <ul>
            <li>Download <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&gl=US" target="_blank">Google Authenticator</a> on your mobile.</li>
            <li>Create a new profile with the <strong>SECRET KEY</strong> below.</li>
            <li>Submit the generated OTP in the form for authentication.</li>
          </ul>
        </div>
        <div class="form-group">
          <label for="secret">SECRET KEY</label>
          <input type="text" class="form-control" id="secret" value="{{ secret }}" readonly>
        </div>
        <div class="text-center">
          <button type="button" class="btn btn-success mb-3" onclick="copySecret()">Copy Secret Key</button>
        </div>
      </form>
    </div>
    <div class="col-lg-7 m-auto">
      {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
      {% for category, message in messages %}
      <div class="alert alert-{{ category }}" role="alert">
        {{ message }}
      </div>
      {% endfor %}
      {% endif %}
      {% endwith %}
      <form method="POST">
        <div class="form-group">
          <label for="otp">Enter Generated OTP</label>
          <input type="number" class="form-control" id="otp" name="otp" required>
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-success">Test 2FA Authentication</button>
        </div>
      </form>
    </div>
  </div>
</div>

<script>
  function copySecret() {
    /* Get the text field */
    var copyText = document.getElementById("secret");

    /* Select the text field */
    copyText.select();
    copyText.setSelectionRange(0, 99999); /*For mobile devices*/

    /* Copy the text inside the text field */
    document.execCommand("copy");

    alert("Successfully copied Secret Key!");
  }
</script>
{% endblock %}

进入全屏模式 退出全屏模式

[](https://res.cloudinary.com/practicaldev/image/fetch/s--c0f_J7JX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/5ei3ultvs8qbgnwciu23.png)

接下来,您将更新enroll_2fa路由以验证提交的 2FA 令牌,如果验证成功,则将用户帐户标记为已注册。使用以下代码更新app.py文件:

@app.route("/2fa/enroll/", methods=["GET", "POST"])
@login_required
@auth_not_enrolled
def enroll_2fa():
    user_details = get_user_details(session["user_secret"])
    secret_key = user_details["data"]["auth_secret"]

    if request.method == "POST":
        otp = int(request.form.get("otp"))

        if pyotp.TOTP(secret_key).verify(otp):
            user_details["data"]["auth_enrolled"] = True
            client.query(
                q.update(
                    q.ref(q.collection("users"), user_details["ref"].id()), {
                        "data": user_details["data"]
                    }
                )
            )
            flash("You have successfully enrolled 2FA for your profile, please authenticate yourself once more!", "success")
            return redirect(url_for("verify_2fa"))
        else:
            flash("The OTP provided is invalid, it has either expired or was generated using a wrong SECRET!", "danger")
            return redirect(url_for("enroll_2fa"))

    return render_template("enroll_2fa.html", secret=secret_key)

进入全屏模式 退出全屏模式

第 4 步:构建 2FA 验证页面

2FA 验证页面是用户在使用密码对自己进行身份验证后立即验证其当前登录会话的地方。

需要更新app.py文件中的verify_2fa路由来渲染验证页面模板;使用以下代码更新verify_2fa路由:

@app.route("/2fa/verify/")
@login_required
def verify_2fa():
    return render_template("verify_2fa.html")

进入全屏模式 退出全屏模式

创建另一个名为verify_2fa.html的文件,该文件将存储在templates文件夹中,并将以下代码保存在其中:

{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Fauna Two-Factor Authentication (2FA) Demo</h2>
        <h5>Set Up TOTP 2FA!</h5>
      </div>
    </div>

    <div class="col-lg-5">
      <h5>Instructions!</h5>
      <ul>
        <li>Download <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&gl=US" target="_blank">Google Authenticator</a> on your mobile.</li>
        <li>Enroll your account.</li>
        <li>Submit the generated OTP in the form for authentication.</li>
      </ul>
    </div>
    <div class="col-lg-7">
      {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
      {% for category, message in messages %}
      <div class="alert alert-{{ category }}" role="alert">
        {{ message }}
      </div>
      {% endfor %}
      {% endif %}
      {% endwith %}
      <form method="POST">
        <div class="form-group">
          <label for="otp">Enter Generated OTP</label>
          <input type="number" class="form-control" id="otp" name="otp" required>
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-success">Authenticate Account</button>
          <a href="{{ url_for('logout') }}" class="btn btn-danger">Logout</a>
        </div>
      </form>
    </div>
  </div>
</div>
{% endblock %}

进入全屏模式 退出全屏模式

您还需要更新app.py文件以实现 2FA 验证页面中引用的logout路由。使用以下代码更新app.py文件:

@app.route("/logout/")
def logout():
    user_client = FaunaClient(secret=session["user_secret"])
    result = user_client.query(
        q.logout(True)
    )
    session.clear()
    return redirect(url_for("index"))

进入全屏模式 退出全屏模式

[](https://res.cloudinary.com/practicaldev/image/fetch/s--1N8ppG3L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/m2h3560bz0azjsamcj4o.png)

接下来,您将更新verify_2fa路由以验证提交的 2FA 令牌并完成用户帐户的双因素身份验证。使用以下代码更新app.py文件:

@app.route("/2fa/verify/", methods=["GET", "POST"])
@login_required
@auth_enrolled
def verify_2fa():
    if request.method == "POST":
        otp = int(request.form.get("otp"))

        user_details = get_user_details(session["user_secret"])
        secret_key = user_details["data"]["auth_secret"]

        if pyotp.TOTP(secret_key).verify(otp):
            session["verify_2fa"] = True
            return redirect(url_for("auth_success"))
        else:
            flash("The OTP provided is invalid, it has either expired or was generated using a wrong SECRET!", "danger")
            return redirect(url_for("verify_2fa"))
    return render_template("verify_2fa.html")

进入全屏模式 退出全屏模式

第五步:在Flask中实现授权规则

您可能希望auth_success路由只能由已使用密码和 2FA 成功验证自己的用户访问。

为此,您需要一个处理此过程的自定义包装器。使用以下代码更新app.py文件:

def auth_verified(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if not session.get("verify_2fa"):
            return redirect(url_for("verify_2fa"))
        return f(*args, **kwargs)

    return decorated

进入全屏模式 退出全屏模式

您现在已经定义了自定义包装器。您需要在需要它的路线中实现它。使用以下代码更新您的auth_success路线:

@app.route("/auth-success/")
@login_required
@auth_verified
def auth_success():
    return "<h1>Successfully authenticated account using Fauna and PyOTP!</h1>"

进入全屏模式 退出全屏模式

[](https://res.cloudinary.com/practicaldev/image/fetch/s--4XKJ6aPy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads。 s3.amazonaws.com/uploads/articles/9fh6a7i54zugye0fk0me.png)

结论

在本文中,您使用 Python 和 Flask 构建了一个 Web 应用程序,然后使用 Fauna 的内置用户身份和会话管理功能以及用于生成和验证一次性密码的 PyOTP 实现了双重身份验证。

希望进一步构建演示应用程序、查看示例代码或改进其功能?访问GitHub 存储库。我还创建了一个Github Gist,它使用 Python 显示了 Fauna 的用户身份和会话管理功能。如果您有任何问题,请随时在 Twitter 上与我联系:@LordGhostX。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐