為你的 Flutter 應用程式新增驗證 (Authentication)
本教程將向你展示如何將 Logto 整合到你的 Flutter 應用程式中。
- SDK 套件可在 pub.dev 和 Logto GitHub 儲存庫 上取得。
- 範例專案使用 Flutter material 建立。你可以在 pub.dev 上找到它。
- 此 SDK 與 iOS、Android 和 Web 平台上的 Flutter 應用程式相容。其他平台的相容性尚未測試。
先決條件
- 一個 Logto Cloud 帳戶或 自行託管的 Logto。
- 已創建的 Logto 原生應用程式。
- Flutter 或 Dart 開發環境。
安裝
- pub.dev
- GitHub
你可以使用 pub 套件管理器直接安裝 logto_dart_sdk package
。在你的專案根目錄下執行以下命令:
flutter pub add logto_dart_sdk
或者將以下內容新增到你的 pubspec.yaml
檔案中:
dependencies:
logto_dart_sdk: ^3.0.0
然後執行:
flutter pub get
如果你希望分叉自己的 SDK 版本,可以直接從 GitHub 克隆儲存庫。
git clone https://github.com/logto-io/dart
設定
SDK 版本相容性
Logto SDK 版本 | Dart SDK 版本 | Dart 3.0 相容性 |
---|---|---|
< 2.0.0 | >= 2.17.6 < 3.0.0 | false |
>= 2.0.0 < 3.0.0 | >= 3.0.0 | true |
>= 3.0.0 | >= 3.6.0 | true |
flutter_secure_storage 設定
在底層,這個 SDK 使用 flutter_secure_storage 來實現跨平台的持久安全權杖存儲。
- iOS 使用 Keychain
- Android 使用 AES 加密
配置 Android 版本
在專案的 android/app/build.gradle
文件中將 android:minSdkVersion 設定為 >= 18
。
android {
...
defaultConfig {
...
minSdkVersion 18
...
}
}
在 Android 上禁用自動備份
預設情況下,Android 會在 Google Drive 上備份資料。這可能會導致例外 java.security.InvalidKeyException:Failed
解包密鑰。為避免此情況,
-
要禁用自動備份,請到應用程式的 manifest 文件中,將
android:allowBackup
和android:fullBackupContent
屬性設為false
。AndroidManifest.xml<manifest ... >
...
<application
android:allowBackup="false"
android:fullBackupContent="false"
...
>
...
</application>
</manifest> -
從
FlutterSecureStorage
中排除sharedprefs
。如果你需要保留應用程式的
android:fullBackupContent
而不是禁用它,你可以從備份中排除sharedprefs
目錄。 更多詳情請參閱 Android 文檔。在你的 AndroidManifest.xml 文件中,將 android:fullBackupContent 屬性添加到
<application>
元素,如下例所示。此屬性指向包含備份規則的 XML 文件。AndroidManifest.xml<application ...
android:fullBackupContent="@xml/backup_rules">
</application>在
res/xml/
目錄中創建一個名為@xml/backup_rules
的 XML 文件。在此文件中,使用<include>
和<exclude>
元素添加規則。以下範例備份所有共享偏好設定,除了 device.xml:@xml/backup_rules<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
</full-backup-content>
請查看 flutter_secure_storage 以獲取更多詳情。
flutter_web_auth_2 設定
在底層,這個 SDK 使用 flutter_web_auth_2 來驗證使用者與 Logto。此套件提供了一種簡單的方法,使用系統 webview 或瀏覽器來驗證使用者與 Logto。
此插件在 iOS 12+ 和 macOS 10.15+ 上使用 ASWebAuthenticationSession
,在 iOS 11 上使用 SFAuthenticationSession
,在 Android 上使用 Chrome Custom Tabs
,並在 Web 上開啟新窗口。
-
iOS:無需額外設定
-
Android:在 Android 上註冊回調 URL
為了從 Logto 的登入網頁捕獲回調 URL,你需要將你的登入 redirectUri 註冊到
AndroidManifest.xml
文件中。AndroidManifest.xml<manifest>
<application>
<activity
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
android:exported="true">
<intent-filter android:label="flutter_web_auth_2">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="YOUR_CALLBACK_URL_SCHEME_HERE" />
</intent-filter>
</activity>
</application>
</manifest> -
網頁瀏覽器:創建一個端點來處理回調 URL
如果你使用的是網頁平台,你需要創建一個端點來處理回調 URL,並使用
postMessage
API 將其發送回應用程式。callback.html<!doctype html>
<title>驗證完成</title>
<p>驗證已完成。如果未自動發生,請關閉窗口。</p>
<script>
function postAuthenticationMessage() {
const message = {
'flutter-web-auth-2': window.location.href,
};
if (window.opener) {
window.opener.postMessage(message, window.location.origin);
window.close();
} else if (window.parent && window.parent !== window) {
window.parent.postMessage(message, window.location.origin);
} else {
localStorage.setItem('flutter-web-auth-2', window.location.href);
window.close();
}
}
postAuthenticationMessage();
</script>
請查看 flutter_web_auth_2 套件中的設定指南以獲取更多詳情。
整合
初始化 LogtoClient
匯入 logto_dart_sdk
套件並在應用程式的根目錄初始化 LogtoClient
實例。
import 'package:logto_dart_sdk/logto_dart_sdk.dart';
import 'package:http/http.dart' as http;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(title: 'Logto Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late LogtoClient logtoClient;
void render() {
// 狀態變更
}
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>"
);
void _init() {
logtoClient = LogtoClient(
config: logtoConfig,
httpClient: http.Client(), // 可選的 http client
);
render();
}
void initState() {
super.initState();
_init();
}
// ...
}
實作登入功能
在深入細節之前,以下是終端使用者體驗的快速概覽。登入流程可簡化如下:
- 你的應用程式呼叫登入方法。
- 使用者被重定向至 Logto 登入頁面。對於原生應用程式,系統瀏覽器會被開啟。
- 使用者登入後被重定向回你的應用程式(配置為重定向 URI)。
關於基於重導的登入
- 此驗證流程遵循 OpenID Connect (OIDC) 協議,Logto 強制執行嚴格的安全措施以保護使用者登入。
- 如果你有多個應用程式,可以使用相同的身分提供者 (IdP, Identity provider)(Logto)。一旦使用者登入其中一個應用程式,Logto 將在使用者訪問另一個應用程式時自動完成登入流程。
欲了解更多關於基於重導登入的原理和優勢,請參閱 Logto 登入體驗解析。
開始之前,你需要在管理控制台為你的應用程式新增一個 redirect URI。
讓我們切換到 Logto Console 的應用程式詳細資訊頁面。新增一個重定向 URI io.logto://callback
,然後點擊「儲存變更」。

- 對於 iOS,redirect URI 的 scheme 並不重要,因為
ASWebAuthenticationSession
類別會監聽 redirect URI,而不論其是否已註冊。 - 對於 Android,redirect URI 的 scheme 必須在
AndroidManifest.xml
文件中註冊。
配置好 redirect URI 後,我們在頁面上新增一個登入按鈕,該按鈕將調用 logtoClient.signIn
API 來啟動 Logto 登入流程:
class _MyHomePageState extends State<MyHomePage> {
// ...
final redirectUri = 'io.logto://callback';
Widget build(BuildContext context) {
// ...
Widget signInButton = TextButton(
onPressed: () async {
await logtoClient.signIn(redirectUri);
render();
},
child: const Text('Sign In'),
);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
signInButton,
],
),
),
);
}
}
實作登出功能
讓我們切換到 Logto Console 的應用程式詳細資訊頁面。新增一個登出後重新導向 URI
io.logto://callback
,然後點擊「儲存變更」。

登出後重新導向 URI 是一個 OAuth 2.0 概念,表示登出後應重新導向的位置。
現在讓我們在主頁面上新增一個登出按鈕,以便使用者可以從你的應用程式登出。
class _MyHomePageState extends State<MyHomePage> {
// ...
final postSignOutRedirectUri = 'io.logto//home';
Widget build(BuildContext context) {
// ...
Widget signOutButton = TextButton(
onPressed: () async {
await logtoClient.signOut(postSignOutRedirectUri);
render();
},
child: const Text('Sign Out'),
);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
signInButton,
signOutButton,
],
),
),
);
}
}
處理驗證 (Authentication) 狀態
Logto SDK 提供了一個非同步方法來檢查驗證 (Authentication) 狀態。該方法是 logtoClient.isAuthenticated
。如果使用者已驗證,該方法返回布林值 true
,否則返回 false
。
在範例中,我們根據驗證 (Authentication) 狀態有條件地渲染登入和登出按鈕。現在讓我們更新 Widget 中的 render
方法來處理狀態變更:
class _MyHomePageState extends State<MyHomePage> {
// ...
bool? isAuthenticated = false;
void render() {
setState(() async {
isAuthenticated = await logtoClient.isAuthenticated;
});
}
Widget build(BuildContext context) {
// ...
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
isAuthenticated == true ? signOutButton : signInButton,
],
),
),
);
}
}
檢查點:測試你的應用程式
現在,你可以測試你的應用程式:
- 執行你的應用程式,你會看到登入按鈕。
- 點擊登入按鈕,SDK 會初始化登入流程並將你重定向到 Logto 登入頁面。
- 登入後,你將被重定向回應用程式並看到登出按鈕。
- 點擊登出按鈕以清除權杖存儲並登出。
獲取使用者資訊
顯示使用者資訊
要顯示使用者資訊,你可以使用 logtoClient.idTokenClaims
getter。例如,在 Flutter 應用程式中:
class _MyHomePageState extends State<MyHomePage> {
// ...
Widget build(BuildContext context) {
// ...
Widget getUserInfoButton = TextButton(
onPressed: () async {
final userClaims = await logtoClient.idTokenClaims;
print(userInfo);
},
child: const Text('Get user info'),
);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
isAuthenticated == true ? signOutButton : signInButton,
isAuthenticated == true ? getUserInfoButton : const SizedBox.shrink(),
],
),
),
);
}
}
請求額外的宣告 (Claims)
你可能會發現從 client.idTokenClaims
返回的物件中缺少一些使用者資訊。這是因為 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
。
要請求額外的權限範圍 (Scopes),可以將權限範圍傳遞給 LogtoConfig
物件。例如:
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
scopes: ["email", "phone"],
);
我們也在 SDK 套件中提供了一個內建的 LogtoUserScope
列舉,幫助你使用預定義的權限範圍 (Scopes)。
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
scopes: [LogtoUserScope.email.value, LogtoUserScope.phone.value],
);
需要網路請求的宣告 (Claims)
為了防止 ID 權杖 (ID token) 膨脹,某些宣告 (Claims) 需要透過網路請求來獲取。例如,即使在權限範圍 (Scopes) 中請求了 custom_data
宣告,它也不會包含在使用者物件中。要存取這些宣告,你可以使用 logtoClient.getUserInfo()
方法:
class _MyHomePageState extends State<MyHomePage> {
// ...
Widget build(BuildContext context) {
// ...
Widget getUserInfoButton = TextButton(
onPressed: () async {
final userInfo = await logtoClient.getUserInfo();
print(userInfo);
},
child: const Text('取得使用者資訊'),
);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('我的示範應用程式'),
isAuthenticated == true ? signOutButton : signInButton,
isAuthenticated == true ? getUserInfoButton : const SizedBox.shrink(),
],
),
),
);
}
}
權限範圍 (Scopes) 和宣告 (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 時新增它們:
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
// 新增你的 API 資源
resources: ["https://shopping.your-app.com/api", "https://store.your-app.com/api"],
);
每個 API 資源都有其自身的權限(權限範圍)。
例如,https://shopping.your-app.com/api
資源具有 shopping:read
和 shopping:write
權限,而 https://store.your-app.com/api
資源具有 store:read
和 store:write
權限。
要請求這些權限,你可以在應用程式中配置 Logto 時新增它們:
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
resources: ["https://shopping.your-app.com/api", "https://store.your-app.com/api"],
// 新增你的 API 資源的權限範圍 (scopes)
scopes: ["shopping:read", "shopping:write", "store:read", "store:write"]
);
你可能會注意到權限範圍是獨立於 API 資源定義的。這是因為 OAuth 2.0 的資源標示符 (Resource Indicators) 指定請求的最終權限範圍將是所有目標服務中所有權限範圍的笛卡兒積。
因此,在上述情況中,權限範圍可以從 Logto 的定義中簡化,兩個 API 資源都可以擁有 read
和 write
權限範圍而不需要前綴。然後,在 Logto 配置中:
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
resources: ["https://shopping.your-app.com/api", "https://store.your-app.com/api"],
// 所有資源共用的權限範圍 (Scopes)
scopes: ["read", "write"]
);
對於每個 API 資源,它將請求 read
和 write
權限範圍。
請求未在 API 資源中定義的權限範圍是可以的。例如,即使 API 資源中沒有可用的 email
權限範圍,你也可以請求 email
權限範圍。不可用的權限範圍將被安全地忽略。
成功登入後,Logto 將根據使用者的角色向 API 資源發出適當的權限範圍。
取得 API 資源的存取權杖
要獲取特定 API 資源的存取權杖 (Access token),你可以使用 getAccessToken
方法:
Future<AccessToken?> getAccessToken(String resource) async {
var token = await logtoClient.getAccessToken(resource: resource);
return token;
}
此方法將返回一個 JWT 存取權杖 (Access token),當使用者擁有相關權限時,可以用來存取 API 資源。如果當前快取的存取權杖 (Access token) 已過期,此方法將自動嘗試使用重新整理權杖 (Refresh token) 獲取新的存取權杖 (Access token)。
為組織取得存取權杖
就像 API 資源一樣,你也可以為組織請求存取權杖。當你需要存取使用組織權限範圍而非 API 資源權限範圍定義的資源時,這會很有用。
如果你對組織 (Organization) 不熟悉,請閱讀 🏢 組織(多租戶,Multi-tenancy) 以開始了解。
在配置 Logto client 時,你需要新增 LogtoUserScope.Organizations
權限範圍 (scope):
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
scopes: [LogtoUserScopes.organizations.value]
);
使用者登入後,你可以為使用者獲取組織權杖 (organization token):
// 使用者的有效組織 ID 可以在 ID 權杖 (ID token) 宣告 (claim) `organizations` 中找到。
Future<AccessToken?> getOrganizationAccessToken(String organizationId) async {
var token = await logtoClient.getOrganizationToken(organizationId);
return token;
}
遷移指南
如果你從 Logto Dart SDK 的舊版本遷移,版本 < 3.0.0:
-
更新你的
pubspec.yaml
文件以使用最新版本的 Logto Dart SDK。pubspec.yamldependencies:
logto_dart_sdk: ^3.0.0 -
更新 Android manifest 文件,將舊的
flutter_web_auth
回調活動替換為新的flutter_web_auth_2
。FlutterWebAuth
->FlutterWebAuth2
flutter_web_auth
->flutter_web_auth_2
-
將
redirectUri
傳遞給signOut
方法。現在在調用
signOut
方法時需要redirectUri
。對於 iOS 平台,此參數無用,但對於需要額外end_session
請求來清理登入會話的 Android 和 Web 平台,此參數將作為end_session
請求中的post_logout_redirect_uri
參數使用。await logtoClient.signOut(redirectUri);
疑難排解
Android 疑難排解
-
你需要更新你的 AndroidManifest.xml 以包含
com.linusu.flutter_web_auth_2.CallbackActivity
活動,如下所示:android/app/src/main/AndroidManifest.xml<manifest>
<application>
<!-- 添加 com.linusu.flutter_web_auth_2.CallbackActivity 活動 -->
<activity
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
android:exported="true">
<intent-filter android:label="flutter_web_auth_2">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="YOUR_CALLBACK_URL_SCHEME_HERE" />
</intent-filter>
</activity>
</application>
</manifest> -
如果你的目標是 S+(SDK 版本 31 及以上),你需要為
android:exported
提供明確的值。如果你遵循了早期的安裝說明,這一點可能未包含。確保在你的AndroidManifest.xml
文件中的com.linusu.flutter_web_auth.CallbackActivity
活動中添加android:exported="true"
。 -
成功登入後瀏覽器未關閉:
為確保
flutter_web_auth_2
正常工作,你需要從AndroidManifest.xml
文件中移除任何android:taskAffinity
條目。將android:launchMode="singleTop"
設置為你的AndroidManifest.xml
文件中的主活動。android/app/src/main/AndroidManifest.xml<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
// ...
/>