跳到主要内容

为你的 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 包管理器直接安装 logto_dart_sdk package。 在你的项目根目录下运行以下命令:

flutter pub add logto_dart_sdk

或者将以下内容添加到你的 pubspec.yaml 文件中:

dependencies:
logto_dart_sdk: ^3.0.0

然后运行:

flutter pub get

设置

SDK 版本兼容性

Logto SDK 版本Dart SDK 版本Dart 3.0 兼容性
< 2.0.0>= 2.17.6 < 3.0.0false
>= 2.0.0 < 3.0.0>= 3.0.0true
>= 3.0.0>= 3.6.0true

flutter_secure_storage 设置

在底层,这个 SDK 使用 flutter_secure_storage 来实现跨平台的持久安全令牌存储。

  • iOS 使用 Keychain
  • Android 使用 AES 加密。

配置 Android 版本

在项目的 android/app/build.gradle 文件中,将 android:minSdkVersion 设置为 >= 18

build.gradle
  android {
...

defaultConfig {
...
minSdkVersion 18
...
}
}

禁用 Android 上的自动备份

默认情况下,Android 会在 Google Drive 上备份数据。这可能导致异常 java.security.InvalidKeyException:Failed 解包密钥。为避免这种情况,

  1. 要禁用自动备份,请转到应用程序的清单文件,并将 android:allowBackupandroid:fullBackupContent 属性设置为 false

    AndroidManifest.xml
    <manifest ... >
    ...
    <application
    android:allowBackup="false"
    android:fullBackupContent="false"
    ...
    >
    ...
    </application>
    </manifest>

  2. FlutterSecureStorage 中排除 sharedprefs

    如果你需要为应用程序保留 android:fullBackupContent 而不是禁用它,可以从备份中排除 sharedprefs 目录。 请参阅 Android 文档 了解更多详细信息。

    在你的 AndroidManifest.xml 文件中,向 <application> 元素添加 android:fullBackupContent 属性,如以下示例所示。此属性指向一个包含备份规则的 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,你需要在 AndroidManifest.xml 文件中注册你的登录 redirectUri。

    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>
  • Web 浏览器:创建一个端点来处理回调 URL

    如果你使用的是 Web 平台,你需要创建一个端点来处理回调 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 实例。

lib/main.dart
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 客户端
);
render();
}


void initState() {
super.initState();
_init();
}

// ...
}

实现登录

在我们深入细节之前,这里是终端用户体验的快速概述。登录过程可以简化如下:

  1. 你的应用调用登录方法。
  2. 用户被重定向到 Logto 登录页面。对于原生应用,将打开系统浏览器。
  3. 用户登录并被重定向回你的应用(配置为重定向 URI)。

关于基于重定向的登录

  1. 此认证 (Authentication) 过程遵循 OpenID Connect (OIDC) 协议,Logto 强制执行严格的安全措施以保护用户登录。
  2. 如果你有多个应用程序,可以使用相同的身份提供商 (IdP)(日志 (Logto))。一旦用户登录到一个应用程序,当用户访问另一个应用程序时,Logto 将自动完成登录过程。

要了解有关基于重定向的登录的原理和好处的更多信息,请参阅 Logto 登录体验解释


在开始之前,你需要在管理控制台为你的应用程序添加一个重定向 URI。

让我们切换到 Logto Console 的应用详情页面。添加一个重定向 URI io.logto://callback 并点击“保存更改”。

Logto Console 中的重定向 URI
  • 对于 iOS,重定向 URI 方案并不重要,因为 ASWebAuthenticationSession 类会监听重定向 URI,无论它是否注册。
  • 对于 Android,重定向 URI 方案必须在 AndroidManifest.xml 文件中注册。

配置好重定向 URI 后,我们在页面上添加一个登录按钮,该按钮将调用 logtoClient.signIn API 来启动 Logto 登录流程:

lib/main.dart
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,然后点击“保存更改”。

Logto Console 中的注销后重定向 URI

注销后重定向 URI 是一个 OAuth 2.0 概念,意味着在注销后应该重定向的位置。

现在让我们在主页上添加一个登出按钮,以便用户可以从你的应用程序中登出。

lib/main.dart
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。该方法返回一个布尔值,如果用户已认证 (Authentication),则返回 true,否则返回 false

在示例中,我们根据认证 (Authentication) 状态有条件地渲染登录和登出按钮。现在让我们更新 Widget 中的 render 方法以处理状态变化:

lib/main.dart
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,
],
),
),
);
}
}

检查点:测试你的应用程序

现在,你可以测试你的应用程序:

  1. 运行你的应用程序,你将看到登录按钮。
  2. 点击登录按钮,SDK 将初始化登录过程并将你重定向到 Logto 登录页面。
  3. 登录后,你将被重定向回你的应用程序,并看到登出按钮。
  4. 点击登出按钮以清除令牌存储并登出。

获取用户信息

显示用户信息

要显示用户的信息,你可以使用 logtoClient.idTokenClaims 获取器。例如,在 Flutter 应用中:

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
// ...


Widget build(BuildContext context) {
// ...

Widget getUserInfoButton = TextButton(
onPressed: () async {
final userClaims = await logtoClient.idTokenClaims;
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(),
],
),
),
);
}
}

请求额外的声明 (Claims)

你可能会发现从 client.idTokenClaims 返回的对象中缺少一些用户信息。这是因为 OAuth 2.0 和 OpenID Connect (OIDC) 的设计遵循最小权限原则 (PoLP),而 Logto 是基于这些标准构建的。

默认情况下,返回的声明(Claim)是有限的。如果你需要更多信息,可以请求额外的权限(Scope)以访问更多的声明(Claim)。

信息:

“声明(Claim)”是关于主体的断言;“权限(Scope)”是一组声明。在当前情况下,声明是关于用户的一条信息。

以下是权限(Scope)与声明(Claim)关系的非规范性示例:

提示:

“sub” 声明(Claim)表示“主体(Subject)”,即用户的唯一标识符(例如用户 ID)。

Logto SDK 将始终请求三个权限(Scope):openidprofileoffline_access

要请求额外的权限 (Scopes),你可以将权限传递给 LogtoConfig 对象。例如:

lib/main.dart
// 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],
);

需要网络请求的声明

为了防止 ID 令牌 (ID token) 过大,一些声明需要通过网络请求来获取。例如,即使在权限中请求了 custom_data 声明,它也不会包含在用户对象中。要访问这些声明,你可以使用 logtoClient.getUserInfo() 方法

lib/main.dart
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(),
],
),
),
);
}
}
该方法将通过请求 userinfo 端点来获取用户信息。要了解更多可用的权限和声明,请参阅 权限和声明部分。

权限 (Scopes) 和声明 (Claims)

Logto 使用 OIDC 权限和声明约定 来定义从 ID 令牌和 OIDC 用户信息端点检索用户信息的权限和声明。“权限”和“声明”都是 OAuth 2.0 和 OpenID Connect (OIDC) 规范中的术语。

以下是支持的权限 (Scopes) 列表及相应的声明 (Claims):

openid

声明名称类型描述需要用户信息吗?
substring用户的唯一标识符

profile

声明名称类型描述需要用户信息吗?
namestring用户的全名
usernamestring用户名
picturestring终端用户的个人资料图片的 URL。此 URL 必须指向一个图像文件(例如 PNG、JPEG 或 GIF 图像文件),而不是包含图像的网页。请注意,此 URL 应特别引用适合在描述终端用户时显示的终端用户的个人资料照片,而不是终端用户拍摄的任意照片。
created_atnumber终端用户创建的时间。时间表示为自 Unix 纪元(1970-01-01T00:00:00Z)以来的毫秒数。
updated_atnumber终端用户信息最后更新的时间。时间表示为自 Unix 纪元(1970-01-01T00:00:00Z)以来的毫秒数。

其他 标准声明 包括 family_namegiven_namemiddle_namenicknamepreferred_usernameprofilewebsitegenderbirthdatezoneinfolocale 也将包含在 profile 权限中,无需请求用户信息端点。与上述声明的区别在于,这些声明只有在其值不为空时才会返回,而上述声明在值为空时将返回 null

备注:

与标准声明不同,created_atupdated_at 声明使用毫秒而不是秒。

email

声明名称类型描述需要用户信息吗?
emailstring用户的电子邮件地址
email_verifiedboolean电子邮件地址是否已验证

phone

声明名称类型描述需要用户信息吗?
phone_numberstring用户的电话号码
phone_number_verifiedboolean电话号码是否已验证

address

有关地址声明的详细信息,请参阅 OpenID Connect Core 1.0

custom_data

声明名称类型描述需要用户信息吗?
custom_dataobject用户的自定义数据

identities

声明名称类型描述需要用户信息吗?
identitiesobject用户的关联身份
sso_identitiesarray用户的关联 SSO 身份

roles

声明名称类型描述需要用户信息吗?
rolesstring[]用户的角色

urn:logto:scope:organizations

声明名称类型描述需要用户信息吗?
organizationsstring[]用户所属的组织 ID
organization_dataobject[]用户所属的组织数据

urn:logto:scope:organization_roles

声明名称类型描述需要用户信息吗?
organization_rolesstring[]用户所属的组织角色,格式为 <organization_id>:<role_name>

考虑到性能和数据大小,如果“需要用户信息吗?”为“是”,则表示声明不会显示在 ID 令牌中,而会在 用户信息端点 响应中返回。

API 资源和组织 (Organizations)

我们建议首先阅读 🔐 基于角色的访问控制 (RBAC),以了解 Logto RBAC 的基本概念以及如何正确设置 API 资源。

配置 Logto 客户端

一旦你设置了 API 资源,就可以在应用中配置 Logto 时添加它们:

lib/main.dart
// 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:readshopping:write 权限,而 https://store.your-app.com/api 资源具有 store:readstore:write 权限。

要请求这些权限,你可以在应用中配置 Logto 时添加它们:

lib/main.dart
// 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: ["shopping:read", "shopping:write", "store:read", "store:write"]
);

你可能会注意到权限是与 API 资源分开定义的。这是因为 OAuth 2.0 的资源指示器 指定请求的最终权限将是所有目标服务中所有权限的笛卡尔积。

因此,在上述情况下,权限可以从 Logto 中的定义简化,两个 API 资源都可以拥有 read write 权限,而无需前缀。然后,在 Logto 配置中:

lib/main.dart
// 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 资源,它将请求 readwrite 权限。

备注:

请求 API 资源中未定义的权限是可以的。例如,即使 API 资源没有可用的 email 权限,你也可以请求 email 权限。不可用的权限将被安全地忽略。

成功登录后,Logto 将根据用户的角色向 API 资源发布适当的权限。

获取 API 资源的访问令牌 (Access token)

要获取特定 API 资源的访问令牌 (access token),你可以使用 getAccessToken 方法:

lib/main.dart
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 资源一样,你也可以请求组织的访问令牌。这在你需要访问使用组织权限 (Scope) 而不是 API 资源权限 (Scope) 定义的资源时非常有用。

如果你对组织不熟悉,请阅读 🏢 组织(多租户) 以开始了解。

在配置 Logto 客户端时,你需要添加 LogtoUserScope.Organizations 权限:

lib/main.dart
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
scopes: [LogtoUserScopes.organizations.value]
);

用户登录后,你可以获取用户的组织令牌:

// 用户的有效组织 ID 可以在 ID 令牌声明 `organizations` 中找到。
Future<AccessToken?> getOrganizationAccessToken(String organizationId) async {
var token = await logtoClient.getOrganizationToken(organizationId);

return token;
}

迁移指南

如果你是从之前版本的 Logto Dart SDK 迁移,版本 < 3.0.0:

  1. 更新你的 pubspec.yaml 文件以使用最新版本的 Logto Dart SDK。

    pubspec.yaml
    dependencies:
    logto_dart_sdk: ^3.0.0
  2. 更新 Android manifest 文件,将旧的 flutter_web_auth 回调活动替换为新的 flutter_web_auth_2

    • FlutterWebAuth -> FlutterWebAuth2
    • flutter_web_auth -> flutter_web_auth_2
  3. 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 条目。在 AndroidManifest.xml 文件中的主活动中设置 android:launchMode="singleTop"

    android/app/src/main/AndroidManifest.xml
    <activity
    android:name=".MainActivity"
    android:launchMode="singleTop"
    android:theme="@style/LaunchTheme"
    // ...
    />

延伸阅读

终端用户流程:认证流程、账户流程和组织流程 配置连接器 保护你的 API