跳到主要内容

身份验证 Authentication

您可以使用GitHub、Google、Azure、Auth0、LinkedIn等第三方身份提供者在Flet应用中实现用户身份验证("登录使用X"按钮)。

身份提供者必须支持OAuth 2.0授权码流程以获取API访问令牌。

内置的Flet登录凭证和用户管理计划在未来版本中推出。 如果您需要在应用中创建和管理用户帐户,您可以自行实现,或者使用Auth0身份提供者,该提供者提供大量免费功能。

Flet身份验证功能:

  • 与Flet桌面、Web和移动应用一起工作。
  • 在一个应用中使用多个身份验证提供者。
  • 内置的OAuth提供者,可自动获取用户详细信息:
    • GitHub
    • Azure
    • Google
    • Auth0
  • 可选地获取组。
  • 自动令牌刷新。
  • 使用保存的令牌登录("记住我")。
  • 自定义OAuth提供者。

登录流程概述

  • 配置OAuth提供程序(内置或通用),包括客户端ID、客户端密钥和重定向URL。
  • 调用page.login(provider)来启动OAuth Web流程。
  • 用户被重定向到OAuth提供程序的网站。
  • 用户在提供程序的网站上登录并同意使用所请求的范围访问服务API。
  • 提供程序的网站将用户重定向到Flet的OAuth回调URL,并提供授权码。
  • Flet使用授权码交换令牌,并调用page.on_login事件处理程序。
  • Flet应用程序可以从page.auth.token属性中检索API令牌,并从page.auth.user中获取用户详细信息。

配置OAuth提供程序

Flet内置了以下OAuth提供程序:

  • GitHub
  • Azure
  • Google
  • Auth0

此外,您还可以配置通用的OAuth提供程序,并提供授权、令牌和用户信息的端点。

在本指南中,我们将使用GitHub帐户配置Flet登录页面。

要将Flet身份验证与GitHub集成,首先需要注册一个新的GitHub OAuth应用程序个人资料设置开发者设置OAuth应用)。

授权回调URL应该采用以下格式:

{application-url}/api/oauth/redirect

在OAuth应用程序详细信息页面上,点击“生成新的客户端秘钥”按钮。 将“客户端ID”和“客户端秘钥”值复制到一个安全的地方 - 你将在Flet应用程序中需要它们。

使用OAuth提供程序进行登录

import os

import flet as ft
from flet.auth.providers import GitHubOAuthProvider

GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID")
assert GITHUB_CLIENT_ID, "set GITHUB_CLIENT_ID environment variable"
GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET")
assert GITHUB_CLIENT_SECRET, "set GITHUB_CLIENT_SECRET environment variable"

def main(page: ft.Page):
provider = GitHubOAuthProvider(
client_id=GITHUB_CLIENT_ID,
client_secret=GITHUB_CLIENT_SECRET,
redirect_url="http://localhost:8550/api/oauth/redirect",
)

def login_click(e):
page.login(provider)

def on_login(e):
print("登录错误:", e.error)
print("访问令牌:", page.auth.token.access_token)
print("用户ID:", page.auth.user.id)

page.on_login = on_login
page.add(ft.ElevatedButton("使用GitHub登录", on_click=login_click))

ft.app(target=main, port=8550, view=ft.WEB_BROWSER)
警告

注意,我们从环境变量中获取了OAuth应用的客户端ID和客户端秘钥。 不要将任何机密信息嵌入到源代码中,以避免意外暴露给公众!

在运行应用之前,在命令行中设置秘密环境变量:

$ export GITHUB_CLIENT_ID="<client_id>"
$ export GITHUB_CLIENT_SECRET="<client_secret>"

运行程序并点击"使用GitHub登录"按钮。GitHub会在以下位置打开授权应用页面:

  • 桌面应用 - 在新的浏览器窗口或标签中打开。
  • Web应用 - 在新的弹出窗口中打开(请确保弹出窗口拦截器已关闭)。
  • 移动应用 - 在应用内的网页浏览器中打开。

重定向URL

在注册GitHub OAuth应用时,我们使用了http://localhost:8550/api/oauth/redirect作为重定向URL。 请注意,它使用了固定的端口8550。要在固定端口上运行Flet应用,请在flet.app调用中使用port参数:

ft.app(target=main, port=8550)

范围

大多数OAuth提供程序允许应用程序请求一个或多个范围,以限制应用程序对用户帐户的访问。

默认情况下,内置的Flet提供程序请求访问用户配置文件的范围,但您可以在登录方法中请求其他范围,比如上面的示例中的public_repo

page.login(
provider,
scope=["public_repo"])

page.login()方法有许多参数可以控制认证过程:

  • fetch_user(bool)- 是否将用户详细信息获取到page.auth.user。默认为True
  • fetch_groups(bool)- 是否将用户组获取到page.auth.user.groups。默认为False
  • scope - 要请求的范围列表。
  • saved_token - page.auth.token的JSON快照,用于恢复授权。令牌可以使用page.auth.token.to_json()序列化,然后加密并保存在page.client_storage中。见下文。
  • on_open_authorization_url - 打开授权URL的回调函数。见下文。
  • complete_page_html - “您已成功认证。现在关闭此页面”页面的自定义HTML内容。
  • redirect_to_page(bool)- 仅在Flet Web应用程序中使用,当在同一浏览器选项卡中打开授权页面时。

page.login()调用的结果是Authorization类的一个实例,具有以下字段:

  • token - 用于访问提供程序API的OAuth令牌。见下文。
  • user - 用户详细信息,包括一个必需的id字段和其他特定于OAuth提供程序的字段。
  • provider - 用于授权的OAuth提供程序的实例。

对最后一个授权的引用保存在page.auth属性中。

如果您的应用程序允许使用多个OAuth提供程序进行授权,您可以将授权保存在会话中,例如:

page.session["github_auth"] = page.login(github_provider)
page.session["google_auth"] = page.login(google_provider)

检查身份验证结果

在成功或失败的授权后,将调用page.on_login事件处理程序。

事件处理程序参数eLoginEvent类的实例,具有以下属性:

  • error(str)- OAuth错误。
  • error_description(str)- OAuth错误描述。

如果error为空字符串,则授权成功。

您可以使用此事件处理程序来切换登录/注销的用户界面,例如:

import os

import flet
from flet import ElevatedButton, LoginEvent, Page
from flet.auth.providers import GitHubOAuthProvider

def main(page: Page):
provider = GitHubOAuthProvider(
client_id=os.getenv("GITHUB_CLIENT_ID"),
client_secret=os.getenv("GITHUB_CLIENT_SECRET"),
redirect_url="http://localhost:8550/api/oauth/redirect",
)

def login_button_click(e):
page.login(provider, scope=["public_repo"])

def on_login(e: LoginEvent):
if not e.error:
toggle_login_buttons()

def logout_button_click(e):
page.logout()

def on_logout(e):
toggle_login_buttons()

def toggle_login_buttons():
login_button.visible = page.auth is None
logout_button.visible = page.auth is not None
page.update()

login_button = ElevatedButton("使用GitHub登录", on_click=login_button_click)
logout_button = ElevatedButton("注销", on_click=logout_button_click)
toggle_login_buttons()
page.on_login = on_login
page.on_logout = on_logout
page.add(login_button, logout_button)

flet.app(target=main, port=8550, view=flet.WEB_BROWSER)

访问用户详情

如果使用page.login()方法并设置fetch_user=True(默认值),则会将用户配置文件分配给page.auth.user

所有内置的OAuth提供者都实现了user.id属性 - 唯一的用户标识符 - 其值取决于提供者(数字、Guid或电子邮件),可以在应用程序中用作用户键。

其余的用户配置文件属性取决于提供者,并且可以使用索引器进行访问。例如,要打印GitHub用户的一些属性:

print("姓名:", page.auth.user["name"])
print("登录名:", page.auth.user["login"])
print("电子邮件:", page.auth.user["email"])

使用OAuth令牌

在成功授权后,page.auth.token将包含OAuth令牌,可以用于访问提供者的API。令牌对象具有以下属性:

  • access_token - 作为API请求头中的授权令牌使用的访问令牌。
  • scope - 令牌的范围。
  • token_type - 访问令牌类型,例如Bearer
  • expires_in - 可选的访问令牌过期的秒数。
  • expires_at - 可选的访问令牌过期的时间(time.time() + expires_in)。
  • refresh_token - 可选的刷新令牌,用于在旧令牌过期时获取新的访问令牌。

通常,只需要使用page.auth.token.access_token来调用提供者的API,例如列出用户的GitHub存储库:

import requests
headers = {"Authorization": "Bearer {}".format(page.auth.token.access_token)}
repos_resp = requests.get("https://api.github.com/user/repos", headers=headers)
user_repos = json.loads(repos_resp.text)
for repo in user_repos:
print(repo["full_name"])
备注

不要在代码中保存对page.auth.token的引用,而是在每次需要获取访问令牌时调用page.auth.tokenpage.auth.token是一个属性,如果/当令牌过期时,它会自动刷新OAuth令牌。

正确的代码:

access_token = page.auth.token.access_token

错误代码:

token = page.auth.token
# some other code
access_token = token.access_token # token could expire by this moment

保存和恢复认证令牌

要实现持久登录(登录页面上的“记住我”复选框),您可以将认证令牌保存在客户端存储中,并在用户下次打开 Flet 应用时使用它进行登录。

将认证令牌序列化为 JSON:

jt = page.auth.token.to_json()
警告

在将数据发送到客户端存储之前,请对敏感数据进行加密。

Flet 包含了使用对称算法(加密和解密使用相同的密钥)加密文本数据的实用方法。它们使用 cryptography 包中的 Fernet 实现,该实现是 AES 128 加上一些额外的强化措施,再加上使用 PBKDF2 从用户口令派生加密密钥。

将 JSON 令牌加密:

import os
from flet.security import encrypt, decrypt

secret_key = os.getenv("MY_APP_SECRET_KEY")
# 返回 base64 编码的字符串
ejt = encrypt(jt, secret_key)
警告

请注意,我们从环境变量中获取了一个秘钥(也称为口令、密码等)。不要将任何机密信息嵌入源代码中,以避免意外暴露给公众!

在运行应用程序之前,在命令行中设置秘钥:

$ export MY_APP_SECRET_KEY="<secret>"

现在,加密后的值可以存储在客户端存储中:

page.client_storage.set("myapp.auth_token", ejt)

用户下次打开应用程序时,您可以从客户端存储中读取加密令牌,并且如果存在的话,解密它并在 page.login() 方法中使用:

ejt = page.client_storage.get("myapp.auth_token")
if ejt:
jt = decrypt(ejt, secret_key)
page.login(provider, saved_token=jt)

查看完整的应用程序示例

退出登录

调用 page.logout() 会重置 page.auth 引用并触发 page.on_logout 事件处理程序。

您可以在退出登录方法中删除保存的令牌,例如:

def logout_button_click(e):
page.client_storage.remove(AUTH_TOKEN_KEY)
page.logout()

查看完整的应用示例

自定义授权流程

默认情况下,OAuth 授权流程在新的浏览器窗口/标签页(桌面应用程序)、浏览器弹出窗口(Web)或应用内 Web 视图(移动设备)中进行。

在授权流程完成后,用户将被重定向到 Flet 的 OAuth 回调页面(/api/oauth/redirect),该页面尝试使用 JavaScript 关闭浏览器窗口/标签页,并向用户提供关闭窗口的手动说明,如果 JavaScript 关闭失败。

此部分仅适用于 Flet 桌面和 Web 应用程序,因为移动应用程序中的应用内 Web 视图可以由 Flet 在不依赖 JavaScript 的情况下关闭。

您可以在 page.login() 方法中自定义“授权完成”页面的内容,例如:

complete_page_html = """
<!DOCTYPE html>
<html>
<head>
<title>已登录到 MyApp</title>
</head>
<body>
<script type="text/javascript">
window.close();
</script>
<p>您已成功登录!您现在可以关闭此标签页或窗口。</p>
</body>
</html>
"""

page.login(
provider,
complete_page_html=complete_page_html,
)

您还可以更改 Web 应用程序,以在同一标签页中打开提供者的授权页面,这可能更加熟悉您的用户,并避免他们处理弹出窗口拦截程序:

page.login(
provider,
on_open_authorization_url=lambda url: page.launch_url(url, web_window_name="_self"),
redirect_to_page=True
)

要在新标签页中打开流程(注意 _self 替换为 _blank):

page.login(
provider,
on_open_authorization_url=lambda url: page.launch_url(url, web_window_name="_blank")
)

配置自定义的 OAuth 提供者

您可以使用 flet.auth.oauth_provider.OAuthProvider 类在应用程序中配置任何兼容 OAuth 的身份验证提供者。

按照 LinkedIn 授权码流程指南 中的说明,我们可以获取配置 LinkedIn OAuth 提供者所需的所有参数,以允许您的 Flet 应用程序的用户使用其 LinkedIn 帐户登录:

import os

import flet
from flet import ElevatedButton, Page
from flet.auth import OAuthProvider

def main(page: Page):
provider = OAuthProvider(
client_id=os.getenv("LINKEDIN_CLIENT_ID"),
client_secret=os.getenv("LINKEDIN_CLIENT_SECRET"),
authorization_endpoint="https://www.linkedin.com/oauth/v2/authorization",
token_endpoint="https://www.linkedin.com/oauth/v2/accessToken",
user_endpoint="https://api.linkedin.com/v2/me",
user_scopes=["r_liteprofile", "r_emailaddress"],
user_id_fn=lambda u: u["id"],
redirect_url="http://localhost:8550/api/oauth/redirect",
)

def login_click(e):
page.login(provider)

def on_login(e):
if e.error:
raise Exception(e.error)
print("用户ID:", page.auth.user.id)
print("访问令牌:", page.auth.token.access_token)

page.on_login = on_login
page.add(ElevatedButton("使用LinkedIn登录", on_click=login_click))

flet.app(target=main, port=8550, view=flet.WEB_BROWSER)

必需的提供者设置:

  • client_id
  • client_secret
  • authorization_endpoint
  • token_endpoint
  • redirect_url

与其他示例类似,客户端ID和客户端密钥从环境变量中获取。