使用 Azure Active Directory B2C 在你自己的 Python Web 应用程序中启用身份验证
本文将介绍如何在你自己的 Python Web 应用程序中添加 Azure Active Directory B2C (Azure AD B2C) 身份验证。 你需要允许用户使用 Azure AD B2C 用户流登录、注销、更新个人资料和重置密码。 本文使用适用于 Python 的 Microsoft 身份验证库 (MSAL) 来简化向 Python Web 应用程序添加身份验证的过程。
本文的目的是将你在使用 Azure AD B2C 在示例 Python Web 应用程序中配置身份验证中使用的示例应用程序替换为你自己的 Python 应用程序。
本文使用 Python 3.9+ 和 Flask 2.1 创建一个基本的 Web 应用。 应用程序的视图使用 Jinja2 模板。
先决条件
- 完成使用 Azure AD B2C 在示例 Python Web 应用程序中配置身份验证中的步骤。 需创建 Azure AD B2C 用户流,并在 Azure 门户中注册 Web 应用程序。
- 安装 Python 3.9 或更高版本
- Visual Studio Code 或其他代码编辑器
- 安装 Visual Studio Code 的 Python 扩展
步骤 1:创建 Python 项目
在文件系统上,为本教程创建一个项目文件夹,例如
my-python-web-app
。在终端中,将目录切换到 Python 应用文件夹,例如
cd my-python-web-app
。运行以下命令,以根据当前解释器创建并激活名为
.venv
的虚拟环境。在终端中运行以下命令以更新虚拟环境中的 pip:
python -m pip install --upgrade pip
若要启用 Flask 调试功能,请将 Flask 开发环境切换到
development
模式。 有关调试 Flask 应用的详细信息,请查看 Flask 文档。通过在 VS Code 中运行
code .
命令,或者打开 VS Code 并选择“文件”>“打开文件夹”来打开项目文件夹。
步骤 2:安装应用依赖项
在Web 应用根文件夹下,创建 requirements.txt
文件。 requirements 文件列出了要使用 pip install 安装的包。 将以下内容添加到 requirements.txt 文件中:
Flask>=2
werkzeug>=2
flask-session>=0.3.2,<0.5
requests>=2,<3
msal>=1.7,<2
在终端中,运行以下命令来安装依赖项:
步骤 3:生成应用 UI 组件
Flask 是一种轻量级 Web 应用程序 Python 框架,为 URL 路由和页面呈现提供基础知识。 它利用 Jinja2 作为模板引擎来呈现应用的内容。 有关详细信息,请查看模板设计器文档。 在本部分,你将添加所需的模板来提供 Web 应用的基本功能。
步骤 3.1:创建基础模板
Flask 中的基础页面模板包含一系列页面的所有共享部分,包括对 CSS 文件、脚本文件等的引用。 基础模板还定义一个或多个块标记,扩展基础模板的其他模板预期会替代这些标记。 在基础模板和扩展的模板中,块标记都由 {% block <name> %}
和 {% endblock %}
描述。
在 Web 应用的根文件夹中,创建 templates
文件夹。 在模板文件夹中,创建名为 base.html
的文件并添加以下内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{% block metadata %}{% endblock %}
<title>{% block title %}{% endblock %}</title>
<!-- Bootstrap CSS file reference -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('index')}}">Python Flask demo</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ url_for('index')}}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('graphcall')}}">Graph API</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
<br />
{% block content %}
{% endblock %}
<hr />
<footer>
<p>Powered by MSAL Python {{ version }}</p>
</footer>
</div>
</body>
</html>
步骤 3.2:创建 Web 应用模板
在模板文件夹下添加以下模板。 这些模板扩展 base.html
模板:
index.html:Web 应用的主页。 模板使用以下逻辑:如果用户未登录,则呈现“登录”按钮。 如果用户正在登录,则呈现访问令牌的声明、指向编辑个人资料的链接,并调用图形 API。
{% extends "base.html" %} {% block title %}Home{% endblock %} {% block content %} <h1>Microsoft Identity Python Web App</h1> {% if user %} <h2>Claims:</h2> <pre>{{ user |tojson(indent=4) }}</pre> {% if config.get("ENDPOINT") %} <li><a href='/graphcall'>Call Microsoft Graph API</a></li> {% endif %} {% if config.get("B2C_PROFILE_AUTHORITY") %} <li><a href='{{_build_auth_code_flow(authority=config["B2C_PROFILE_AUTHORITY"])["auth_uri"]}}'>Edit Profile</a></li> {% endif %} <li><a href="/logout">Logout</a></li> {% else %} <li><a href='{{ auth_url }}'>Sign In</a></li> {% endif %} {% endblock %}
graph.html:演示如何调用 REST API。
{% extends "base.html" %} {% block title %}Graph API{% endblock %} {% block content %} <a href="javascript:window.history.go(-1)">Back</a> <!-- Displayed on top of a potentially large JSON response, so it will remain visible --> <h1>Graph API Call Result</h1> <pre>{{ result |tojson(indent=4) }}</pre> <!-- Just a generic json viewer --> {% endblock %}
auth_error.html:处理身份验证错误。
{% extends "base.html" %} {% block title%}Error{% endblock%} {% block metadata %} {% if config.get("B2C_RESET_PASSWORD_AUTHORITY") and "AADB2C90118" in result.get("error_description") %} <!-- See also https://learn.microsoft.com/azure/active-directory-b2c/active-directory-b2c-reference-policies#linking-user-flows --> <meta http-equiv="refresh" content='0;{{_build_auth_code_flow(authority=config["B2C_RESET_PASSWORD_AUTHORITY"])["auth_uri"]}}'> {% endif %} {% endblock %} {% block content %} <h2>Login Failure</h2> <dl> <dt>{{ result.get("error") }}</dt> <dd>{{ result.get("error_description") }}</dd> </dl> <a href="{{ url_for('index') }}">Homepage</a> {% endblock %}
步骤 4:配置 Web 应用
在 Web 应用的根文件夹中,创建名为 app_config.py
的文件。 此文件包含有关 Azure AD B2C 标识提供者的信息。 Web 应用使用此信息与 Azure AD B2C 建立信任关系、允许用户登录和注销、获取令牌和验证令牌。 将以下内容添加到该文件中:
import os
b2c_tenant = "fabrikamb2c"
signupsignin_user_flow = "B2C_1_signupsignin1"
editprofile_user_flow = "B2C_1_profileediting1"
resetpassword_user_flow = "B2C_1_passwordreset1" # Note: Legacy setting.
authority_template = "https://{tenant}.b2clogin.cn/{tenant}.partner.onmschina.cn/{user_flow}"
CLIENT_ID = "Enter_the_Application_Id_here" # Application (client) ID of app registration
CLIENT_SECRET = "Enter_the_Client_Secret_Here" # Application secret.
AUTHORITY = authority_template.format(
tenant=b2c_tenant, user_flow=signupsignin_user_flow)
B2C_PROFILE_AUTHORITY = authority_template.format(
tenant=b2c_tenant, user_flow=editprofile_user_flow)
B2C_RESET_PASSWORD_AUTHORITY = authority_template.format(
tenant=b2c_tenant, user_flow=resetpassword_user_flow)
REDIRECT_PATH = "/getAToken"
# This is the API resource endpoint
ENDPOINT = '' # Application ID URI of app registration in Azure portal
# These are the scopes you've exposed in the web API app registration in the Azure portal
SCOPE = [] # Example with two exposed scopes: ["demo.read", "demo.write"]
SESSION_TYPE = "filesystem" # Specifies the token cache should be stored in server-side session
按照在示例 Python Web 应用中配置身份验证一文的配置示例 Web 应用部分中所述,使用 Azure AD B2C 环境设置更新上面的代码。
步骤 5:添加 Web 应用代码
在本部分,你将添加 Flask 视图函数和 MSAL 库身份验证方法。 在项目的根文件夹下添加名为 app.py
的文件,其中包含以下代码:
import uuid
import requests
from flask import Flask, render_template, session, request, redirect, url_for
from flask_session import Session # https://pythonhosted.org/Flask-Session
import msal
import app_config
app = Flask(__name__)
app.config.from_object(app_config)
Session(app)
# This section is needed for url_for("foo", _external=True) to automatically
# generate http scheme when this sample is running on localhost,
# and to generate https scheme when it is deployed behind reversed proxy.
# See also https://flask.palletsprojects.com/en/1.0.x/deploying/wsgi-standalone/#proxy-setups
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
@app.route("/anonymous")
def anonymous():
return "anonymous page"
@app.route("/")
def index():
#if not session.get("user"):
# return redirect(url_for("login"))
if not session.get("user"):
session["flow"] = _build_auth_code_flow(scopes=app_config.SCOPE)
return render_template('index.html', auth_url=session["flow"]["auth_uri"], version=msal.__version__)
else:
return render_template('index.html', user=session["user"], version=msal.__version__)
@app.route("/login")
def login():
# Technically we could use empty list [] as scopes to do just sign in,
# here we choose to also collect end user consent upfront
session["flow"] = _build_auth_code_flow(scopes=app_config.SCOPE)
return render_template("login.html", auth_url=session["flow"]["auth_uri"], version=msal.__version__)
@app.route(app_config.REDIRECT_PATH) # Its absolute URL must match your app's redirect_uri set in AAD
def authorized():
try:
cache = _load_cache()
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
session.get("flow", {}), request.args)
if "error" in result:
return render_template("auth_error.html", result=result)
session["user"] = result.get("id_token_claims")
_save_cache(cache)
except ValueError: # Usually caused by CSRF
pass # Simply ignore them
return redirect(url_for("index"))
@app.route("/logout")
def logout():
session.clear() # Wipe out user and its token cache from session
return redirect( # Also logout from your tenant's web session
app_config.AUTHORITY + "/oauth2/v2.0/logout" +
"?post_logout_redirect_uri=" + url_for("index", _external=True))
@app.route("/graphcall")
def graphcall():
token = _get_token_from_cache(app_config.SCOPE)
if not token:
return redirect(url_for("login"))
graph_data = requests.get( # Use token to call downstream service
app_config.ENDPOINT,
headers={'Authorization': 'Bearer ' + token['access_token']},
).json()
return render_template('graph.html', result=graph_data)
def _load_cache():
cache = msal.SerializableTokenCache()
if session.get("token_cache"):
cache.deserialize(session["token_cache"])
return cache
def _save_cache(cache):
if cache.has_state_changed:
session["token_cache"] = cache.serialize()
def _build_msal_app(cache=None, authority=None):
return msal.ConfidentialClientApplication(
app_config.CLIENT_ID, authority=authority or app_config.AUTHORITY,
client_credential=app_config.CLIENT_SECRET, token_cache=cache)
def _build_auth_code_flow(authority=None, scopes=None):
return _build_msal_app(authority=authority).initiate_auth_code_flow(
scopes or [],
redirect_uri=url_for("authorized", _external=True))
def _get_token_from_cache(scope=None):
cache = _load_cache() # This web app maintains one cache per session
cca = _build_msal_app(cache=cache)
accounts = cca.get_accounts()
if accounts: # So all account(s) belong to the current signed-in user
result = cca.acquire_token_silent(scope, account=accounts[0])
_save_cache(cache)
return result
app.jinja_env.globals.update(_build_auth_code_flow=_build_auth_code_flow) # Used in template
if __name__ == "__main__":
app.run()
步骤 6:运行 Web 应用
在终端中,通过输入以下命令来运行应用,这会运行 Flask 开发服务器。 默认情况下,开发服务器会查找 app.py
。 然后,打开浏览器并导航到 Web 应用 URL:http://localhost:5000
。
[可选] 调试应用
使用调试功能可以在特定的代码行上暂停正在运行的程序。 暂停程序时,可以检查变量、在“调试控制台”面板中运行代码,或者利用调试中所述的功能。 若要使用 Visual Studio Code 调试器,请查看 VS Code 文档。
若要更改主机名和/或端口号,请使用 launch.json
文件的 args
数组。 以下示例演示如何将主机名配置为 localhost
并将端口号配置为 5001
。 请注意,如果更改主机名或端口号,则必须更新重定向 URI 或应用程序。 有关详细信息,请查看注册 Web 应用程序步骤。
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Flask",
"type": "python",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "app.py",
"FLASK_ENV": "development"
},
"args": [
"run",
"--host=localhost",
"--port=5001"
],
"jinja": true,
"justMyCode": true
}
]
}