教程:向 Python Flask Web 应用添加登录
本教程是以下教程系列的第三部分,该教材系列演示如何从头开始构建 Python Flask Web 应用,并使用 Microsoft 标识平台集成身份验证。 在本教程中,你将添加代码以在构建的应用中对用户进行身份验证。
- 导入所需的模块和配置
- 创建 Flask Web 应用实例
- 为本地开发配置 ProxyFix 中间件
- 添加代码以使用户登录和注销
- 定义 Web 应用的入口点
导入所需的包和配置
要构建的 Web 应用使用基于 MSAL Python 构建的 identity.web
包对 Web 应用中的用户进行身份验证。 要导入 identity.web
包、Flask 框架、Flask 模块、Flask 会话和上一教程中定义的应用配置,请向 app.py 添加以下代码:
import identity.web
import requests
from flask import Flask, redirect, render_template, request, session, url_for
from flask_session import Session
import app_config
在此代码片段中,导入 redirect
、render_template
、request
、session
和 url_for
:用于处理 Flask 中的 Web 请求和会话的函数和对象。 你还可以导入 app_config
,其中包含应用的配置设置。
创建 Flask Web 应用实例
导入所需模块后,我们将使用 app-config
中的配置初始化 Web 应用。 要创建 Web 应用的实例,请将以下代码片段添加到 app.py
:
app = Flask(__name__)
app.config.from_object(app_config)
assert app.config["REDIRECT_PATH"] != "/", "REDIRECT_PATH must not be /"
Session(app)
在上面的代码片段中,你将初始化新的 Flask 应用程序并使用 app.config.from_object(app_config)
加载配置设置。 使用 from_object
,应用就会继承 (app_config)
中指定的配置。
你还可以执行断言检查,以确保应用的重定向路径未设置为根路径(“/”)。 Session(app)
会初始化应用的会话管理,使你能够处理会话并存储多个请求中的用户身份验证状态等数据。
为本地开发配置 ProxyFix 中间件
由于示例 Web 应用在本地主机上运行,因此我们使用 ProxyFix
中间件修复请求标头中的 URL 方案和主机信息。 将以下代码添加到 app.py
以应用 ProxyFix:
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
初始化身份验证对象
接下来,通过创建 [identity.web.Auth](https://identity-library.readthedocs.io/en/latest/#identity.web.Auth)
类的实例来初始化身份验证对象。 你还可以在初始化 Auth 对象时在构造函数中传递参数 session
、authority
、client_id
和 client_credential
,如下所示:
app.jinja_env.globals.update(Auth=identity.web.Auth) # Useful in template for B2C
auth = identity.web.Auth(
session=session,
authority=app.config["AUTHORITY"],
client_id=app.config["CLIENT_ID"],
client_credential=app.config["CLIENT_SECRET"],
)
在此代码片段中,app.jinja_env.globals.update(Auth=identity.web.Auth)
添加一个名为 Auth
的新全局变量,并为其赋值 identity.web.Auth
。 这使得 Auth
可在 Flask 应用程序呈现的所有模板中访问。
用户登录
在此应用中生成的授权流是双重的。 在第一重中,你将调用 auth.log_in
函数以登录用户,如下所示:
@app.route("/login")
def login():
return render_template("login.html", version=__version__, **auth.log_in(
scopes=app_config.SCOPE, # Have user consent to scopes during log-in
redirect_uri=url_for("auth_response", _external=True), # Optional. If present, this absolute URL must match your app's redirect_uri registered in Microsoft Entra admin center
prompt="select_account", # Optional.
))
当用户导航到应用中的 /login
URL 时,Flask 将调用处理呈现 login.html
模板的请求的视图函数。 在 login()
内,你将使用用户在登录过程中应同意的范围列表调用 auth.log_in
函数。 还可以在参数中提供 redirect_uri
,它应该与 Azure 管理中心内该应用的重定向 URI 匹配。
你可以选择添加参数(如 prompt
),该参数通过请求重新验证身份、获取用户同意或在具有活动会话的帐户之间选择帐户来控制登录提示的行为。
在授权流的第二重中,你将调用 redirect_uri 控制器内的 auth.complete_log_in
函数来处理身份验证响应,如下所示:
@app.route(app_config.REDIRECT_PATH)
def auth_response():
result = auth.complete_log_in(request.args)
if "error" in result:
return render_template("auth_error.html", result=result)
return redirect(url_for("index"))
complete_log_in()
函数将传入的 auth_response
字典作为查询参数。 如果成功,该函数将使用 redirect(url_for("index"))
将用户重定向到“索引”路由。 这意味着用户已成功登录,其信息作为包含已验证 ID 令牌中的声明的字典提供。
如果结果包含由条件 if "error" in result:
决定的错误,则向用户呈现 "auth_error.html"
模板。
注销用户
若要从 Flask 应用程序中注销用户,请调用 auth.log_out()
方法,具体如下所示:
@app.route("/logout")
def logout():
return redirect(auth.log_out(url_for("index", _external=True)))
当用户导航到应用中的 /logout
URL 路由时,Flask 会调用注销函数,以将其从当前应用注销。 你还可以指定用户在注销时应重定向到的页面。在该代码片段中,我们使用 url_for("index", _external=True).
将用户重定向到应用的主页
定义 Web 应用的入口点
实现登录和注销逻辑后,通过创建 index()
函数,将入口点添加到应用的主页,如下所示:
@app.route("/")
def index():
if not (app.config["CLIENT_ID"] and app.config["CLIENT_SECRET"]):
# This check is not strictly necessary.
# You can remove this check from your production code.
return render_template('config_error.html')
if not auth.get_user():
return redirect(url_for("login"))
return render_template('index.html', user=auth.get_user(), version=__version__)
当用户导航到应用的根 URL(“/”)时,将调用 index()
函数。 它在呈现应用的主页之前处理配置检查并验证用户身份。 它会检查配置中是否缺少客户端 ID 和客户端密码,以及缺失一个还是两个值,Flask 将呈现 "config_error.html"
模板。
该函数还调用 auth.get_user()
来验证用户是否已进行身份验证。 如果未对用户进行身份验证,则会将用户重定向到 "login"
路由。 如果已经过身份验证,Flask 将呈现“index.html”模板,并传递用户对象(从 auth.get_user()
检索到的)进行呈现。