為你的 Python 網頁應用程式新增驗證 (Authentication)
本指南將向你展示如何將 Logto 整合到你的 Python 網頁應用程式中。
- 此範例使用 Flask,但概念對其他框架相同。
- Python 範例專案可在我們的 Python SDK repo 中找到。
- Logto SDK 利用協程,記得在呼叫非同步函數時使用
await
。
先決條件
- 一個 Logto Cloud 帳戶或 自行架設的 Logto。
- 已建立的 Logto 傳統網頁應用程式。
安裝
在專案根目錄執行:
pip install logto # 或 `poetry add logto` 或其他你使用的工具
整合
初始化 LogtoClient
首先,建立一個 Logto 配置:
from logto import LogtoClient, LogtoConfig
client = LogtoClient(
LogtoConfig(
endpoint="https://you-logto-endpoint.app", # 替換為你的 Logto endpoint
appId="replace-with-your-app-id",
appSecret="replace-with-your-app-secret",
),
)
你可以在管理控制台的應用程式詳細資訊頁面找到並複製「App Secret」:

同時將預設的記憶體儲存替換為持久性儲存,例如:
from logto import LogtoClient, LogtoConfig, Storage
from flask import session
from typing import Union
class SessionStorage(Storage):
def get(self, key: str) -> Union[str, None]:
return session.get(key, None)
def set(self, key: str, value: Union[str, None]) -> None:
session[key] = value
def delete(self, key: str) -> None:
session.pop(key, None)
client = LogtoClient(
LogtoConfig(...),
storage=SessionStorage(),
)
詳情請參閱 Storage。
配置重定向 URI
在深入細節之前,以下是終端使用者體驗的快速概覽。登入流程可簡化如下:
- 你的應用程式呼叫登入方法。
- 使用者被重定向至 Logto 登入頁面。對於原生應用程式,系統瀏覽器會被開啟。
- 使用者登入後被重定向回你的應用程式(配置為重定向 URI)。
關於基於重導的登入
- 此驗證流程遵循 OpenID Connect (OIDC) 協議,Logto 強制執行嚴格的安全措施以保護使用者登入。
- 如果你有多個應用程式,可以使用相同的身分提供者 (IdP, Identity provider)(Logto)。一旦使用者登入其中一個應用程式,Logto 將在使用者訪問另一個應用程式時自動完成登入流程。
欲了解更多關於基於重導登入的原理和優勢,請參閱 Logto 登入體驗解析。
在以下的程式碼片段中,我們假設你的應用程式運行在 http://localhost:3000/
。
配置重定向 URI
切換到 Logto Console 的應用程式詳細資訊頁面。新增一個重定向 URI http://localhost:3000/callback
。
就像登入一樣,使用者應被重定向到 Logto 以登出共享會話。完成後,將使用者重定向回你的網站會很不錯。例如,將 http://localhost:3000/
新增為登出後重定向 URI 區段。
然後點擊「儲存」以保存更改。
實作登入與登出路由
在你的網頁應用程式中,新增一個路由來正確處理使用者的登入請求。我們以 /sign-in
為例:
@app.route("/sign-in")
async def sign_in():
# 獲取登入 URL 並將使用者重定向至該 URL
return redirect(await client.signIn(
redirectUri="http://localhost:3000/callback",
))
將 http://localhost:3000/callback
替換為你在 Logto Console 中為此應用程式設定的回呼 URL。
如果你希望在第一個畫面顯示註冊頁面,可以將 interactionMode
設定為 signUp
:
@app.route("/sign-in")
async def sign_in():
return redirect(await client.signIn(
redirectUri="http://localhost:3000/callback",
interactionMode="signUp", # 在第一個畫面顯示註冊頁面
))
現在,無論何時使用者訪問 http://localhost:3000/sign-in
,都會啟動新的登入嘗試並將使用者重定向至 Logto 登入頁面。
注意 建立登入路由並不是啟動登入嘗試的唯一方法。你可以隨時使用
signIn
方法獲取登入 URL 並將使用者重定向至該 URL。
在使用者發出登出請求後,Logto 會清除會話中的所有使用者驗證資訊。
要清理 Python 會話和 Logto 會話,可以實現如下的登出路由:
@app.route("/sign-out")
async def sign_out():
return redirect(
# 成功登出後將使用者重定向至首頁
await client.signOut(postLogoutRedirectUri="http://localhost:3000/")
)
處理驗證 (Authentication) 狀態
在 Logto SDK 中,我們可以使用 client.isAuthenticated()
來檢查驗證 (Authentication) 狀態,如果使用者已登入,值將為 true,否則值將為 false。
這裡我們也實作了一個簡單的首頁作為示範:
- 如果使用者未登入,顯示登入按鈕;
- 如果使用者已登入,顯示登出按鈕。
@app.route("/")
async def home():
if client.isAuthenticated() is False:
return "未驗證 <a href='/sign-in'>登入</a>"
return "已驗證 <a href='/sign-out'>登出</a>"
檢查點:測試你的應用程式
現在,你可以測試你的應用程式:
- 執行你的應用程式,你會看到登入按鈕。
- 點擊登入按鈕,SDK 會初始化登入流程並將你重定向到 Logto 登入頁面。
- 登入後,你將被重定向回應用程式並看到登出按鈕。
- 點擊登出按鈕以清除權杖存儲並登出。
獲取使用者資訊
顯示使用者資訊
要顯示使用者資訊,你可以使用 getIdTokenClaims
方法或 fetchUserInfo
方法來獲取使用者資訊。getIdTokenClaims
返回 ID 權杖 (ID token) 中包含的使用者資訊,而 fetchUserInfo
則從 userinfo 端點獲取使用者資訊。
如你所見,我們使用 @authenticated
裝飾器將使用者資訊上下文帶入 Flask 應用程式的 API。
from functools import wraps
from flask import g, jsonify, redirect
from samples.client import client
def authenticated(shouldRedirect: bool = False, fetchUserInfo: bool = False):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
if client.isAuthenticated() is False:
if shouldRedirect:
return redirect("/sign-in")
return jsonify({"error": "Not authenticated"}), 401
# 將使用者資訊存儲在 Flask 應用程式上下文中
g.user = (
await client.fetchUserInfo()
if fetchUserInfo
else client.getIdTokenClaims()
)
return await func(*args, **kwargs)
return wrapper
return decorator
例如,要在 API 中顯示使用者資訊,你可以使用以下代碼:
@app.route("/protected/userinfo")
@authenticated(shouldRedirect=True, fetchUserInfo=True)
async def protectedUserinfo():
try:
return (
"<h2>User info</h2>"
+ g.user.model_dump_json(indent=2, exclude_unset=True).replace("\n", "<br>")
+ navigationHtml
)
except LogtoException as e:
return "<h2>Error</h2>" + str(e) + "<br>" + navigationHtml
我們的數據模型基於 pydantic,因此你可以使用 model_dump_json
將數據模型轉換為 JSON。
添加 exclude_unset=True
將從 JSON 輸出中排除未設置的欄位,使輸出更精確。
例如,如果我們在登入時未請求 email
權限範圍 (scope),則 email
欄位將從 JSON 輸出中排除。然而,如果我們請求了 email
權限範圍,但使用者沒有電子郵件地址,則 email
欄位將包含在 JSON 輸出中,值為 null
。
要了解更多關於權限範圍和宣告 (claims) 的資訊,請參閱 獲取使用者資訊。
請求額外的宣告 (Claims)
你可能會發現從 client.getIdTokenClaims()
返回的物件中缺少一些使用者資訊。這是因為 OAuth 2.0 和 OpenID Connect (OIDC) 的設計遵循最小權限原則 (PoLP, Principle of Least Privilege),而 Logto 是基於這些標準構建的。
預設情況下,僅返回有限的宣告 (Claims)。如果你需要更多資訊,可以請求額外的權限範圍 (Scopes) 以存取更多宣告。
「宣告 (Claim)」是對主體所做的斷言;「權限範圍 (Scope)」是一組宣告。在目前的情況下,宣告是關於使用者的一部分資訊。
以下是權限範圍與宣告關係的非規範性範例:
「sub」宣告表示「主體 (Subject)」,即使用者的唯一識別符(例如使用者 ID)。
Logto SDK 將始終請求三個權限範圍:openid
、profile
和 offline_access
。
要請求額外的權限範圍,你可以將權限範圍傳遞給 LogtoConfig
物件。例如:
from logto import UserInfoScope
client = LogtoClient(
LogtoConfig(
# ...其他配置
scopes = [
UserInfoScope.email,
UserInfoScope.phone,
],
),
storage=SessionStorage(),
)
然後你可以在 client.getIdTokenClaims()
的返回值中訪問額外的宣告:
idTokenClaims = await client.getIdTokenClaims();
需要網路請求的宣告 (Claims)
為了防止 ID 權杖 (ID token) 膨脹,某些宣告 (Claims) 需要透過網路請求來獲取。例如,即使在權限範圍 (Scopes) 中請求了 custom_data
宣告,它也不會包含在使用者物件中。要存取這些宣告,你可以使用 client.fetchUserInfo()
方法:
(await client.fetchUserInfo()).custom_data
權限範圍和宣告 (Scopes and claims)
Logto 使用 OIDC 權限範圍 (Scopes) 和宣告 (Claims) 慣例 來定義從 ID 權杖 (ID token) 和 OIDC 使用者資訊端點 (userinfo endpoint) 獲取使用者資訊的權限範圍和宣告。無論是「權限範圍 (Scope)」還是「宣告 (Claim)」,都是 OAuth 2.0 和 OpenID Connect (OIDC) 規範中的術語。
以下是支援的權限範圍 (Scopes) 及其對應的宣告 (Claims):
openid
宣告名稱 | 類型 | 描述 | 需要使用者資訊嗎? |
---|---|---|---|
sub | string | 使用者的唯一識別符 | 否 |
profile
宣告名稱 | 類型 | 描述 | 需要使用者資訊嗎? |
---|---|---|---|
name | string | 使用者的全名 | 否 |
username | string | 使用者的用戶名 | 否 |
picture | string | 使用者個人資料圖片的 URL。此 URL 必須指向圖像文件(例如 PNG、JPEG 或 GIF 圖像文件),而不是包含圖像的網頁。請注意,此 URL 應特別參考適合在描述使用者時顯示的個人資料照片,而不是使用者拍攝的任意照片。 | 否 |
created_at | number | 使用者創建的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。 | 否 |
updated_at | number | 使用者資訊最後更新的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。 | 否 |
其他 標準宣告 包括 family_name
、given_name
、middle_name
、nickname
、preferred_username
、profile
、website
、gender
、birthdate
、zoneinfo
和 locale
也將包含在 profile
權限範圍中,無需請求使用者資訊端點。與上述宣告的不同之處在於,這些宣告僅在其值不為空時返回,而上述宣告在值為空時將返回 null
。
與標準宣告不同,created_at
和 updated_at
宣告使用毫秒而非秒。
email
宣告名稱 | 類型 | 描述 | 需要使用者資訊嗎? |
---|---|---|---|
string | 使用者的電子郵件地址 | 否 | |
email_verified | boolean | 電子郵件地址是否已驗證 | 否 |
phone
宣告名稱 | 類型 | 描述 | 需要使用者資訊嗎? |
---|---|---|---|
phone_number | string | 使用者的電話號碼 | 否 |
phone_number_verified | boolean | 電話號碼是否已驗證 | 否 |
address
請參閱 OpenID Connect Core 1.0 以獲取地址宣告的詳細資訊。
custom_data
宣告名稱 | 類型 | 描述 | 需要使用者資訊嗎? |
---|---|---|---|
custom_data | object | 使用者的自訂資料 | 是 |
identities
宣告名稱 | 類型 | 描述 | 需要使用者資訊嗎? |
---|---|---|---|
identities | object | 使用者的連結身分 | 是 |
sso_identities | array | 使用者的連結 SSO 身分 | 是 |
roles
宣告名稱 | 類型 | 描述 | 需要使用者資訊嗎? |
---|---|---|---|
roles | string[] | 使用者的角色 | 否 |
urn:logto:scope:organizations
宣告名稱 | 類型 | 描述 | 需要使用者資訊嗎? |
---|---|---|---|
organizations | string[] | 使用者所屬的組織 ID | 否 |
organization_data | object[] | 使用者所屬的組織資料 | 是 |
urn:logto:scope:organization_roles
宣告名稱 | 類型 | 描述 | 需要使用者資訊嗎? |
---|---|---|---|
organization_roles | string[] | 使用者所屬的組織角色,格式為 <organization_id>:<role_name> | 否 |
考慮到效能和資料大小,如果「需要使用者資訊嗎?」為「是」,則表示該宣告不會顯示在 ID 權杖中,而會在 使用者資訊端點 回應中返回。
API 資源和組織
我們建議先閱讀 🔐 角色型存取控制 (RBAC, Role-Based Access Control),以瞭解 Logto RBAC 的基本概念以及如何正確設定 API 資源。
配置 Logto 客戶端
一旦你設定了 API 資源,就可以在應用程式中配置 Logto 時新增它們:
client = LogtoClient(
LogtoConfig(
# ...other configs
resources=["https://shopping.your-app.com/api", "https://store.your-app.com/api"], # 新增 API 資源 (API resources)
),
)
每個 API 資源都有其自身的權限(權限範圍)。
例如,https://shopping.your-app.com/api
資源具有 shopping:read
和 shopping:write
權限,而 https://store.your-app.com/api
資源具有 store:read
和 store:write
權限。
要請求這些權限,你可以在應用程式中配置 Logto 時新增它們:
client = LogtoClient(
LogtoConfig(
# ...other configs
scopes=["shopping:read", "shopping:write", "store:read", "store:write"],
resources=["https://shopping.your-app.com/api", "https://store.your-app.com/api"],
),
)
你可能會注意到權限範圍是獨立於 API 資源定義的。這是因為 OAuth 2.0 的資源標示符 (Resource Indicators) 指定請求的最終權限範圍將是所有目標服務中所有權限範圍的笛卡兒積。
因此,在上述情況中,權限範圍可以從 Logto 的定義中簡化,兩個 API 資源都可以擁有 read
和 write
權限範圍而不需要前綴。然後,在 Logto 配置中:
client = LogtoClient(
LogtoConfig(
# ...other configs
scopes=["read", "write"],
resources=["https://shopping.your-app.com/api", "https://store.your-app.com/api"],
),
)
對於每個 API 資源,它將請求 read
和 write
權限範圍。
請求未在 API 資源中定義的權限範圍是可以的。例如,即使 API 資源中沒有可用的 email
權限範圍,你也可以請求 email
權限範圍。不可用的權限範圍將被安全地忽略。
成功登入後,Logto 將根據使用者的角色向 API 資源發出適當的權限範圍。
為 API 資源獲取存取權杖 (Access token)
要獲取特定 API 資源的存取權杖 (Access token),你可以使用 GetAccessToken
方法:
accessToken = await client.getAccessToken("https://shopping.your-app.com/api")
# 或
claims = await client.getAccessTokenClaims("https://shopping.your-app.com/api")
此方法將返回一個 JWT 存取權杖 (Access token),當使用者擁有相關權限時,可以用來存取 API 資源。如果當前快取的存取權杖 (Access token) 已過期,此方法將自動嘗試使用重新整理權杖 (Refresh token) 獲取新的存取權杖 (Access token)。
獲取組織權杖 (Organization tokens)
如果你對組織 (Organization) 不熟悉,請閱讀 🏢 組織(多租戶,Multi-tenancy) 以開始了解。
在配置 Logto client 時,你需要新增 core.UserScopeOrganizations
權限範圍 (scope):
from logto import LogtoClient, LogtoConfig, UserInfoScope
client = LogtoClient(
LogtoConfig(
# ...other configs
scopes=[UserInfoScope.organizations],
),
)
使用者登入後,你可以為使用者獲取組織權杖 (organization token):
# 將參數替換為有效的組織 (Organization) ID。
# 使用者的有效組織 (Organization) ID 可以在 ID 權杖 (ID token) 宣告 (claim) `organizations` 中找到。
organizationToken = await client.getOrganizationToken(organization_id)
# 或
organizationTokenClaims = await client.getOrganizationTokenClaims(organization_id)