用Auth0进行Flutter认证和授权,第一部分。将认证添加到应用程序中

582 阅读25分钟

欢迎!在这个由四部分组成的教程中,你将使用一个Flutter应用程序并通过Auth0来增强它。

在这个由四部分组成的教程中,你将使用一个Flutter应用程序并通过Auth0来增强它。您将从添加基本的用户名/密码认证开始,然后是社交登录,最后是启用使用授权的实时支持聊天。

在本教程结束时,你将建立一个相当复杂的Flutter应用程序,你可以将其作为你自己创作的基础,而且你也将涵盖Auth0的许多功能。

在我们开始之前,让我们把术语搞清楚......

认证和授权是应用程序中的两个关键安全组件,无论是移动应用程序、Web应用程序,还是机器对机器的连接。许多人对这些术语感到困惑,所以这里有一些简单的定义。

  • 认证处理**"你是谁?"**的问题。你将在本节中借助一个名为OpenID Connect(简称 "OIDC")的认证协议来实现它。
  • 授权回答**"你被允许做什么?"**的问题。你将在后面的章节中借助于一个名为OAuth 2.0的授权协议来实现它,简称 "OAuth2"。

在 Flutter 应用程序中添加验证功能

在本节中,您将学习如何用Auth0保护Flutter应用程序。您将使用一个可投入生产的 Flutter 应用程序,并为其添加登录屏幕和注销功能,而且您只需花费一小部分精力就可以实现登录和注销

如果你了解Flutter的基础知识,你就能更顺利地学习本教程,但这并不是硬性要求。如果您有使用任何现代网络框架的经验,您可能就能理解代码,并在学习过程中学习Flutter和Dart(Flutter的编程语言)。

您将学习和构建的内容

虽然你可以创建一个新的Flutter项目并实现你在本教程中所学到的一切,但将认证添加到现有的可生产的应用程序中是很常见的。我将提供一个生产就绪的应用程序。 MJ咖啡,你将通过添加认证来确保其安全性。

在后面的章节中,你将通过一个社会身份提供者,如谷歌或苹果,启用认证。然后你将通过添加角色和权限来进行授权,根据每个用户的权限和角色来限制应用程序的功能。

我将在这个视频概述中进一步解释本教程将涵盖的内容

我将提供应用程序的 "初始 "和 "最终 "版本的源代码。我强烈建议你使用 "入门 "版本,并按照教程一步一步地操作,以便更好地理解该应用程序和你对它的添加。

此外,我还录制了支持本教程的视频。你可以在我的Youtube频道播放列表中找到它们。

如果你想在专注于构建和执行步骤的同时略过这些内容,请寻找🛠表情符号。

设置初始应用程序

前提条件

在开始之前,你需要在你的机器上安装以下东西。

  • Flutter SDK2.0版或更高版本。我使用2.2版本来构建我的应用程序。
  • 如果你想为iOS构建应用程序,你将需要以下条件。
    • Xcode 11或更高版本。
    • **Ruby 2.6.0或更高版本。**这是下一个iOS要求所需要的,也就是...
    • CocoaPods1.10.0或更高版本。
  • null安全的基本理解。如果你使用过Kotlin或Swift的可选类型,你应该没问题;如果没有,请阅读Flutter中的空值安全.
  • **集成开发环境或你选择的编辑器。**我推荐。
    • Android Studio,或
    • IntelliJ,或
    • Visual Studio Code(我将在这个系列中使用它)。
  • 用于你的IDE的Dart和Flutter插件
  • 一杯茶或咖啡。

获取项目,配置它,并运行它

🛠 打开MJ咖啡应用的资源库从主分支下载源代码。这包含了一个功能齐全的应用程序,你可以随时添加Auth0认证/授权和聊天。

🛠 如果你想为iOS构建应用程序,你需要为构建过程指定你自己的开发团队。用Xcode打开 /ios/Runner.xcworkspace/文件,选择Runner项目,然后选择Runner目标,打开Signing & Capabilities标签,并在Team下拉菜单中选择你的团队。

Screenshot of Xcode. The reader is instructed to select the “Runner” project and then the “Runner” target, then select “Signing and Capabilities”, and finally select their development team.

🛠 通过运行该应用程序确认其工作。打开一个命令行界面,导航到项目的根目录,然后输入flutter run

Flutter将编译该项目,并在连接到你的电脑的任何移动设备或在其上运行的任何移动设备模拟器上运行它。如果它找不到任何一个,它将在一个浏览器窗口中运行一个移动设备仿真器。

你会看到该应用程序的主屏幕。

The MJ Coffee App’s home screen

快速浏览一下该应用程序

🛠 点击登录|注册按钮。现在,没有登录功能,所以应用程序会立即带你到菜单屏幕。

The MJ Coffee App’s “Menu” screen

🛠 点击位于屏幕底部中心的支持按钮。它将带你到最终实现该支持聊天功能的屏幕。

The MJ Coffee App’s “Support” screen, which is currently blank

🛠 现在点击位于屏幕右下方的个人资料按钮。它将带你到个人资料屏幕,最终会显示关于登录用户的一些信息。

The MJ Coffee App’s “Profile” screen, which currently shows a coffee illustration

🛠 最后,点击注销按钮,这将使你回到主屏幕。

现在,你已经参观了这个应用程序,是时候开始实现新的功能了

实现登录

安装Flutter的依赖项

第一步是导入所需的库。你将通过指定三个新的依赖项来实现这一目标。

你将通过在项目的 /pubspec.yaml文件(位于根目录下)中添加条目,其中指定了依赖关系。

🛠 添加以下几行到 /pubspec.yaml's dependencies:部分,就在以json_annotation 开始的那一行之后。

  http: ^0.13.3
  flutter_appauth: ^1.1.0
  flutter_secure_storage: ^4.2.0

dependencies:部分最终应该是这样的。

dependencies:
  flutter:
    sdk: flutter
  font_awesome_flutter: ^9.1.0
  flutter_svg: ^0.22.0
  google_fonts: ^2.1.0
  json_annotation: ^4.0.1
  http: ^0.13.3
  flutter_appauth: ^1.1.0
  flutter_secure_storage: ^4.2.0

🛠 保存该文件,然后通过以下两种方式安装这些依赖。

  • 在你的项目根部的命令行上运行flutter pub get 命令,或
  • 在你的编辑器或IDE中运行Pub get

配置回调URL

回调URL是授权服务器(如Auth0)用来与你的应用程序通信的一种机制。它指定了一个在用户被认证后应返回的位置。

由于未经授权的各方可以操纵回调URL,Auth0只识别允许回调URL列表中的URL。这些URL存储在Auth0仪表板的应用程序设置页面中。

对于Web应用程序,回调URL是一个有效的HTTPS URL。对于本地应用程序,即您的Flutter实现,您需要根据您的应用程序的唯一名称(该名称是Android中的应用程序ID和iOS中的捆绑名称)创建一个 "伪URL"。这些格式类似于URL。

你将指定这个应用程序的名字是 mj.coffee.app,这意味着这个应用程序的回调URL将是 mj.coffee.app://login-callback.

flutter_appauth 将在该回调URL上用一个意图过滤器注册你的应用程序。如果没有匹配,应用程序将不会收到结果。

配置Android的回调URL

🛠要配置应用程序的Android版本,请打开 /android/app/build.gradle文件。更新文件中的defaultConfig 部分,添加一个新的项目:manifestPlaceHolders 及其值。 ['appAuthRedirectScheme': 'mj.coffee.app'].appAuthRedirectScheme 的值必须是小写字母。

🛠 你应该把minSdkVersion 的值至少设为 18,因为这是对flutter_secure_storage 包的一个要求。对于MJ咖啡应用,我把minSdkVersion 改为 21.

结果应该是这样的。

// /android/app/build.gradle

    defaultConfig {
        applicationId "mj.coffee.app"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        multiDexEnabled true
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        manifestPlaceholders = [
            'appAuthRedirectScheme': 'mj.coffee.app'
        ]
    }

配置iOS的回调URL

为了配置iOS版本的应用程序,你需要做的唯一改变是添加一个回调方案。

🛠 要做到这一点,请打开 /ios/Runner/Info.plist文件。在 <dict>标签,添加一个新的键,CFBundleURLTypes ,这样,标签的开头就会像这样: <dict>标签的开头看起来像这样。

<!-- /ios/Runner/Info.plist -->

...
<dict>
   <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>mj.coffee.app</string>
            </array>
        </dict>
    </array>
...

🛠 运行Android和iOS版本,并通过使用以下命令确保应用程序在所有设备或模拟器/仿真器上运行,没有错误。

flutter run -d all

配置Auth0

下一步是在Auth0仪表板上将MJ Coffee注册为一个应用程序。

**你需要一个Auth0账户来完成这个步骤。**如果你还没有,你可以注册一个免费账户。免费层对于许多小型应用程序来说是足够慷慨的。

🛠 登录到你的Auth0账户,按照以下步骤注册应用程序。

  • 🛠 转到你的仪表板的应用程序部分。

The main page of the Auth0 dashboard. The reader is directed to click “Applications”.

The main page of the Auth0 dashboard. The reader is directed to click the “Applications” menu item in the “Applications” menu.

  • 🛠 单击 "创建应用程序"按钮。

The main page of the Auth0 dashboard’s “Applications” page. The reader is directed to click the “Create Application” button.

  • 🛠 为你的应用程序输入一个名称(例如,"MJ Coffee Flutter Application"),并选择本地应用程序类型。

The “Create Application” dialog. The reader is directed to enter a name for the application, select the “Native” application type, and click the “Create” button.

  • 🛠 你会看到你新注册的应用程序的快速启动页面。转到 "连接"页面...

The “Quick Start” tab for the “MJ Coffee” application in the Auth0 dashboard. The user is directed to click the “Connections” tab.

...并确保选择用户名-密码-认证(在该页面的数据库部分)。你可以,而且你以后也会为这个应用程序添加一个社交连接。

The “Connections” tab for the “MJ Coffee” application in the Auth0 dashboard. The user is directed to click the “Connections” tab.

  • 🛠 然后转到设置页面。你可以找到所有信息,包括客户ID、客户秘密、域名(租户)等。

The “Settings” tab for the “MJ Coffee” application in the Auth0 dashboard. The user is directed to copy the values in the “Domain” and “Client ID” fields.

  • 🛠 你需要在应用程序URI下的允许回调URL中添加一个应用程序的回调URL。使用的值是 mj.coffee.app://login-callback:

The “Quick Start” tab for the “MJ Coffee” application in the Auth0 dashboard. The user is directed to add the callback URL for the app to the “Allowed Callback URLs” list.

  • 🛠 滚动到页面底部,点击保存更改按钮。

The “Save Changes” button. The user is directed to click it.

向应用程序提供域名和客户ID

你需要使用你在Flutter应用程序中从设置页面复制的域名和客户ID。你可以在应用程序的代码中把这些值存储在常量变量中,或者你可以在运行应用程序时把这些值作为 --dart-define参数传递给应用程序。

与其在你的代码中存储这些敏感信息(这是一个很大的安全风险),我建议你在运行应用程序时将这些值作为 --dart-define参数。

🛠 要在终端或PowerShell中进行这项工作,请使用此命令。

flutter run -d all --dart-define=AUTH0_DOMAIN={YOUR DOMAIN} --dart-define=AUTH0_CLIENT_ID={YOUR CLIENT ID}

你可以选择让你选择的编辑器提供这些值。例如,你可以让Visual Studio Code将这些额外的 --dart-define值,将它们添加到你的启动配置文件的args 字段中(/.vscode/launch.json):

"configurations": [
  {
    "name": "Flutter",
    "request": "launch",
    "flutterMode": "debug",
    "type": "dart",
    "args": [
      "--dart-define",
      "AUTH0_DOMAIN={YOUR DOMAIN}",
      "--dart-define",
      "AUTH0_CLIENT_ID={YOUR CLIENT ID}"
    ]
  }
]

🛠 应用程序应该捕获你传递给它的值。要做到这一点,需要在文件的 constants.dart目录中的 /lib/helpers/目录下的文件中定义这些常量来做到这一点--把这些常量加在 import语句之后。

// /lib/helpers/constants.dart

const AUTH0_DOMAIN = String.fromEnvironment('AUTH0_DOMAIN');
const AUTH0_CLIENT_ID = String.fromEnvironment('AUTH0_CLIENT_ID');
const AUTH0_ISSUER = 'https://$AUTH0_DOMAIN';
const BUNDLE_IDENTIFIER = 'mj.coffee.app';
const AUTH0_REDIRECT_URI = '$BUNDLE_IDENTIFIER://login-callback';

注意,你只需要域名和客户端ID,因为使用PKCE的授权代码流不需要客户端秘密。

该代码还为你的租户定义了一个顶级域名,它被称为发行者。

如前所述,你需要根据你的捆绑标识符创建你的重定向URI,这个标识符是你之前添加到 "允许的回调URL "列表中的。然而,最好牢记。

  • 捆绑标识符必须与Android上的appAuthRedirectScheme ,并且
  • 重定向URL的方案部分必须与iOS上的CFBundleURLSchemes 匹配...

...而且这两个值都必须是小写的。

将Auth0与Flutter集成

由于Auth0是一个标准的OAuth 2.0授权服务器,你可以利用任何标准的OpenID Connect SDK来对Auth0进行认证。其中一个是flutter_appauth ,这是一个围绕AppAuth SDK的包装,用于本地应用程序。你将需要把它集成到你的应用程序中。

🛠 打开这个 /lib/services/auth_service.dart文件,并更新它以导入必要的库,以及实例化FlutterAppAuthFlutterSecureStorage

// /lib/services/auth_service.dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/services.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mjcoffee/helpers/constants.dart';
import 'package:mjcoffee/models/auth0_id_token.dart';
import 'package:mjcoffee/models/auth0_user.dart';

class AuthService {

  static final AuthService instance = AuthService._internal();
  factory AuthService() => instance;
  AuthService._internal();

  final FlutterAppAuth appAuth = FlutterAppAuth();
  final FlutterSecureStorage secureStorage = const FlutterSecureStorage();

}

OpenID Connect有一个协议,即OpenID Connect Discovery,它提供了一种标准的方式来发现JSON文档中的授权服务器端点。

在Auth0中,你可以找到发现文件,在 /.well-known/openid-configuration你的租户地址的端点。对于MJ咖啡,这个端点是 https://YOUR-AUTH0-TENANT-NAME.auth0.com/.well-known/openid-configuration.

如果你看我的视频,你会看到一个发现URL的例子。

AppAuth支持三种方法来配置端点。方便的是,你只需将顶级域名(即发行商)作为参数传递给AppAuth方法。然后,AppAuth在内部从端点获取发现文件,并确定将其发送到何处。 openid-configuration然后在内部从端点获取发现文件,并找出发送后续请求的地方。

🛠 让我们在我们的AuthService 中创建一个登录方法,以构建AuthorizationTokenRequest 。将以下内容添加到 /lib/services/auth_service.dart:

// /lib/services/auth_service.dart

  login() async {
      final authorizationTokenRequest = AuthorizationTokenRequest(
        AUTH0_CLIENT_ID, AUTH0_REDIRECT_URI,
        issuer: AUTH0_ISSUER,
        scopes: ['openid', 'profile', 'offline_access', 'email'],
      );
      final AuthorizationTokenResponse? result =
          await appAuth.authorizeAndExchangeCode(
        authorizationTokenRequest,
      );
      print(result);
  }

为了构造请求,你可以创建AuthorizationTokenRequest 对象,绕过强制性的clientIDredirectUrl 参数,使用的值为 AUTH0_CLIENT_IDAUTH0_REDIRECT_URI的值,并将 AUTH0_ISSUER作为issuer 的值,以实现发现。

如果你定义了scopes ,那就最好了,这样当用户允许时,你就可以代表他们执行操作。下面是我们在上面的代码中要求的作用域。

  • openid:执行OpenID连接签到。
  • profile:检索用户的资料。
  • offline_access:检索刷新令牌,以便从应用程序offline_access
  • email:检索用户的电子邮件。

你将在本教程的后面添加更多的作用域。

一旦请求被构建,调用 appAuth.authorizeAndExchangeCode()开始一个签到事务。认证过程将开始,完成后,用户将带着AuthorizationTokenResponse ,如下图所示,其中包含访问令牌、ID令牌和刷新令牌,返回到应用程序。

AuthorizationTokenResponse(
    String? accessToken,
    String? refreshToken,
    DateTime? accessTokenExpirationDateTime,
    String? idToken,
    String? tokenType,
    this.authorizationAdditionalParameters,
    Map<String, dynamic>? tokenAdditionalParameters,
  )

访问令牌、刷新令牌和ID令牌

你可以使用访问令牌来访问API。客户端不能解码这个令牌,这很正常,因为它只对API的授权服务器有意义。

作为一种安全措施,访问令牌通常有很短的生存时间。有不同的方法可以让它活得更久。一种方法是使用刷新令牌,它可以重新授权你的用户。如果有刷新令牌,应用程序可以使用它来默默地获得一个新的访问令牌。为此,应用程序将存储刷新令牌,出于安全原因,它将安全地存储它们。

🛠 我建议为你的刷新令牌定义一个常量密钥。将此添加到你的 constants.dart文件中。

// /lib/helpers/constants.dart

const REFRESH_TOKEN_KEY = 'refresh_token';

虽然访问令牌的内容对客户端来说是不透明的,但AppAuth SDK会验证ID令牌,因为它是OpenID Connect客户端的一部分责任。应用程序应解码ID Token的主体,以接收其JSON有效载荷。

🛠 为了获得ID令牌的有效载荷,我们需要创建一个模型。我们将其称为Auth0IdToken 。创建一个新文件,名为 auth0_id_token.dart的新文件,在 /lib/models目录下的一个新文件,模型就放在那里。

// /lib/models/auth0_id_token.dart

import 'package:json_annotation/json_annotation.dart';
part 'auth0_id_token.g.dart';

@JsonSerializable()
class Auth0IdToken {
  Auth0IdToken({
    required this.nickname,
    required this.name,
    required this.email,
    required this.picture,
    required this.updatedAt,
    required this.iss,
    required this.sub,
    required this.aud,
    required this.iat,
    required this.exp,
    this.authTime,
  });

  final String nickname;
  final String name;
  final String picture;

  @JsonKey(name: 'updated_at')
  final String updatedAt;

  final String iss;

  // In OIDC, "sub" means "subject identifier",
  // which for our purposes is the user ID.
  // This getter makes it easier to understand.
  String get userId => sub;
  final String sub;

  final String aud;
  final String email;
  final int iat;
  final int exp;

  @JsonKey(name: 'auth_time')
  final int? authTime; // this might be null for the first time login

  factory Auth0IdToken.fromJson(Map<String, dynamic> json) =>
      _$Auth0IdTokenFromJson(json);

  Map<String, dynamic> toJson() => _$Auth0IdTokenToJson(this);
}

ID令牌是由索赔组成的,索赔是名称/价值对,包含关于用户的信息或关于Open ID Connect服务的元信息。Auth0IdToken 模型包含了包含令牌索赔的字段,它们是。

  • iss:响应的发行者的标识符。它的值是一个URL。
  • sub:主题的标识符。在我们的应用程序中,它是用户的ID。由于sub 在Open ID Connect之外不是一个经常使用的术语,我们创建了一个名为userId 的getter,简单地返回sub的值。
  • aud::受众的标识符--也就是说,ID令牌是为谁准备的。
  • iat:构成令牌的JWT的发布时间(iat 是 "发布 时间"的简称)。
  • exp:令牌的过期时间。过了这个时间,该令牌就不能使用了。

其他字段 -nickname,name,email,picture, 和updatedAt 是用于包含用户具体信息的索赔。

Auth0IdToken 类需要一些方法来把来自认证服务器的数据转换成Auth0IdToken 对象和Auth0IdToken 对象的 JSON。你可以手动编写这些方法,但是用生成这些方法来代替更简单、更容易出错的方法。

你可能已经注意到文件开头的这两行。

import 'package:json_annotation/json_annotation.dart';
part 'auth0_id_token.g.dart';
  • Ǟ import行带来了json_annotation 库,你将用它来生成代码来序列化和反序列化一个对象。代码中的 @JsonSerializable()代码中的注解指定这些是要被序列化和反序列化的Auth0IdToken 对象。
  • part 行指定该文件的内容 auth0_id_token.g.dart属于这个文件。文件名的后缀 g.dart文件名的扩展名表明它是一个生成的dart文件。

🛠 运行下面的命令,为Auth0IdToken ,生成JSON转换方法。

flutter pub run build_runner build --delete-conflicting-outputs

🛠 一旦你生成了JSON转换方法,你就可以在 类中实现该方法。 parseIdToken()方法在AuthService 类中通过添加以下内容来实现。

// /lib/services/auth_service.dart

  Auth0IdToken parseIdToken(String idToken) {
    final parts = idToken.split(r'.');
    assert(parts.length == 3);

    final Map<String, dynamic> json = jsonDecode(
      utf8.decode(
        base64Url.decode(
          base64Url.normalize(parts[1]),
        ),
      ),
    );

    return Auth0IdToken.fromJson(json);
  }

现在你有了ID Token,你可以从OpenID Connect端点获得用户的信息,了解用户的详细信息,这就是 https://[AUTH0_DOMAIN]/userinfo.

让我们创建另一个模型,Auth0User ,这样我们就可以对来自userinfo 端点的数据进行反序列化和序列化。

🛠 创建一个文件 auth0_user.dart/lib/models/目录中创建一个文件,内容如下。

// /lib/models/auth0_user.dart

import 'package:json_annotation/json_annotation.dart';
part 'auth0_user.g.dart';

@JsonSerializable()
class Auth0User {
  Auth0User({
    required this.nickname,
    required this.name,
    required this.email,
    required this.picture,
    required this.updatedAt,
    required this.sub,
  });
  final String nickname;
  final String name;
  final String picture;

  @JsonKey(name: 'updated_at')
  final String updatedAt;

 // userID getter to understand it easier
  String get id => sub;
  final String sub;

  final String email;

  factory Auth0User.fromJson(Map<String, dynamic> json) =>
      _$Auth0UserFromJson(json);

  Map<String, dynamic> toJson() => _$Auth0UserToJson(this);
}

🛠 和Auth0IdToken 一样,Auth0User 使用json_annotation 库来生成代码来序列化和反序列化其实例。运行下面的命令来生成该代码。

flutter pub run build_runner build --delete-conflicting-outputs

🛠 这就完成了Auth0用户的模型,所以让我们为Auth0类创建一个 getUserDetails()方法,如下:AuthService 类。

// /lib/services/auth_service.dart

 Future<Auth0User> getUserDetails(String accessToken) async {
    final url = Uri.https(
      AUTH0_DOMAIN,
      '/userinfo',
    );

    final response = await http.get(
      url,
      headers: {'Authorization': 'Bearer $accessToken'},
    );

    print('getUserDetails ${response.body}');

    if (response.statusCode == 200) {
      return Auth0User.fromJson(jsonDecode(response.body));
    } else {
      throw Exception('Failed to get user details');
    }
  }

🛠 getUserDetails()使用Dart的 http库,所以在文件顶部添加以下 import语句在文件的顶部。

// /lib/services/auth_service.dart

import 'package:http/http.dart' as http;

🛠 由于你需要在整个应用程序中重复使用idToken,profile, 和accessToken ,最好将它们的值存储为AuthService 的成员,以便于访问它们。将这些实例变量添加到AuthService

// /lib/services/auth_service.dart

  Auth0User? profile;
  Auth0IdToken? idToken;
  String? auth0AccessToken;

🛠 你可以创建一个简单的方法。 _setLocalVariables()来存储这些本地值。将以下内容添加到AuthService

// /lib/services/auth_service.dart

  Future<String> _setLocalVariables(result) async {
    final bool isValidResult =
        result != null && result.accessToken != null && result.idToken != null;

    if (isValidResult) {
      auth0AccessToken = result.accessToken;
      idToken = parseIdToken(result.idToken!);
      profile = await getUserDetails(result.accessToken!);

      if (result.refreshToken != null) {
        await secureStorage.write(
          key: REFRESH_TOKEN_KEY,
          value: result.refreshToken,
        );
      }

      return 'Success';
    } else {
      return 'Something is Wrong!';
    }
  }

如果访问令牌和ID令牌可用,它就存储它们的值。如果刷新令牌也是可用的,它将其值写入安全存储,并且该值只能用刷新令牌的密钥检索。

🛠 有了你所做的修改,你现在可以更新AuthService's login()方法来返回成功登录的响应。更新该方法,使其看起来像这样。

// /lib/services/auth_service.dart

Future<String> login() async {
    try {
      final authorizationTokenRequest = AuthorizationTokenRequest(
        AUTH0_CLIENT_ID,
        AUTH0_REDIRECT_URI,
        issuer: AUTH0_ISSUER,
        scopes: ['openid', 'profile', 'offline_access', 'email'],
      );

      final AuthorizationTokenResponse? result =
          await appAuth.authorizeAndExchangeCode(
        authorizationTokenRequest,
      );

      return await _setLocalVariables(result);
    } on PlatformException {
      return 'User has cancelled or no internet!';
    } catch (e) {
      return 'Unkown Error!';
    }
  }

你可以捕捉任何异常,并根据它们的类型返回一个特定的响应,以更好地处理错误。

处理应用程序的初始状态

唯一缺少的是处理应用程序启动时的认证状态。你可能希望能够默默地登录,并在有刷新令牌的情况下检索一个新的访问令牌。

🛠 让我们添加一个新方法。 init()来处理应用程序的初始状态。通过在AuthService 中添加以下内容来实现这个方法。

// /lib/services/auth_service.dart

  Future<bool> init() async {
    final storedRefreshToken = await secureStorage.read(key: REFRESH_TOKEN_KEY);

    if (storedRefreshToken == null) {
      return false;
    }

    try {
      final TokenResponse? result = await appAuth.token(
        TokenRequest(
          AUTH0_CLIENT_ID,
          AUTH0_REDIRECT_URI,
          issuer: AUTH0_ISSUER,
          refreshToken: storedRefreshToken,
        ),
      );
      final String setResult = await _setLocalVariables(result);
      return setResult == 'Success';
    } catch (e, s) {
      print('error on Refresh Token: $e - stack: $s');
      // logOut() possibly
      return false;
    }
  }

init()检查安全存储中是否有刷新令牌,如果没有则立即返回 false如果没有则立即返回。然而,如果它找到一个刷新令牌。 init()通过一个TokenRequest 对象将检索到的请求令牌传递给 appAuth.token()以便自动获得新的访问、ID和刷新令牌,而不需要用户手动登录。

在主屏幕上启用登录功能

现在你已经有了登录和初始设置的基础方法,现在是时候为应用程序的屏幕实现类似的方法了,其代码在 /lib/screens/目录中。

🛠 应用程序的主屏幕是在HomeScreen 类中实现的,位于 /lib/screens/home.dart.打开该文件,将这一行添加到其他 import语句。

// /lib/screens/home.dart

import 'package:mjcoffee/services/auth_service.dart';

现在滚过HomeScreen 类到_HomeScreenState 类。你需要对这个类做一些修改。

🛠 第一组改动是对_HomeScreenState 开始的实例变量的改动。将它们改为以下内容。

// /lib/screens/home.dart

    bool isProgressing = false;
    bool isLoggedIn = false;
    String errorMessage = '';
    String? name;

🛠该 initState()方法就在这些变量下面。现在,它所做的唯一事情就是调用它在超类中的对应部分。将implement init action 注释替换为调用 initAction().这个方法看起来应该是这样的。

// /lib/screens/home.dart

  @override
  void initState() {
    initAction();
    super.initState();
  }

你将会实现 initAction()很快就会实现。

最后,看一下 build()方法,它定义了主屏幕的用户界面。滚动浏览这个方法,直到你找到这个 Row()函数调用。

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    if (isProgressing)
      CircularProgressIndicator()
    else if (!isLoggedIn)
      CommonButton(
        onPressed: () {
            CoffeeRouter.instance.pushReplacement(MenuScreen.route());
            /// ----------------------
            /// Implement login action
            /// ----------------------
        },
        text: 'Login | Register',
      )
    else
      Text('Welcome $name'),
  ], // <Widget>[]
),

🛠 替换Implement login section 的注释,以便使 Row()``build 中的函数调用看起来像这样。

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    if (isProgressing)
      CircularProgressIndicator()
    else if (!isLoggedIn)
      CommonButton(
        onPressed: loginAction,
        text: 'Login | Register',
      )
    else
      Text('Welcome $name'),
  ], // <Widget>[]
),

🛠 现在将这些方法添加到_HomeScreenState ,在 build()方法。

setSuccessAuthState() {
  setState(() {
    isProgressing = false;
    isLoggedIn = true;
    name = AuthService.instance.idToken?.name;
  });

  CoffeeRouter.instance.push(MenuScreen.route());
}

setLoadingState() {
  setState(() {
    isProgressing = true;
    errorMessage = '';
  });
}

Future<void> loginAction() async {
  setLoadingState();
  final message = await AuthService.instance.login();
  if (message == 'Success') {
    setSuccessAuthState();
  } else {
    setState(() {
      isProgressing = false;
      errorMessage = message;
    });
  }
}

initAction() async {
  setLoadingState();
  final bool isAuth = await AuthService.instance.init();
  if (isAuth) {
    setSuccessAuthState();
  } else {
    setState(() {
      isProgressing = false;
    });
  }
}

关于这些方法的一些说明。

  • initAction()当主屏幕启动时调用,并处理应用程序有一个刷新令牌的情况。
  • Row()中的函数调用。 build()方法中的函数调用决定了用户根据他们的登录状态看到什么。当用户登录后,屏幕会显示一个包含用户名字的欢迎信息。当用户没有登录时,如果登录正在进行,它将显示一个进度指示器,否则将显示 "登录|注册 "按钮。
  • 按下 "登录|注册 "按钮会导致 loginAction()方法被调用。
  • 如果登录正在进行,会出现一个加载指示器。
  • 许多方法都会调用 setSuccessAuthState(),它将主屏幕的实例变量设置为适当的值,并将用户重定向到适当的屏幕。如果某些操作失败,你可以很容易地在屏幕上显示一个错误信息。

登录

🛠 如果你已经走到了这一步,你已经做得很好了,现在是时候看看你到目前为止取得了什么成果。确保你的模拟器或设备处于激活状态,并停止这个应用程序的任何早期版本。一旦你完成了这些,使用这个命令运行该应用程序。

flutter run -d all --dart-define=AUTH0_DOMAIN=[YOUR DOMAIN] --dart-define=AUTH0_CLIENT_ID=[YOUR CLIENT ID]

一旦该应用程序被加载,点击 "登录|注册 "按钮。

在iOS上,当你第一次运行该应用程序时,你会看到这样的提示。

Allowed callback URLs

这个提示是iOS'的一个结果。 ASWebAuthenticationSession,用户通过网络服务认证的会话。iOS正在通知用户,应用程序打算使用Auth0来登录用户。

如果你点击 "继续 "并且一切顺利,你会看到Auth0通用登录页面,如下图所示(左边是Android版本,右边是iOS版本)。

Allowed callback URLs

请注意,你可以在Auth0仪表板上对这个页面进行样式设计,甚至可以选择其他模板。观看这个视频,了解更多关于Auth0中登录页面的主题设计

一旦你登录,你将被重定向到应用程序,在那里你会被问候名字。然后你将被重定向到菜单屏幕,正如_HomeScreenState's最后一行所指定的那样 setSuccessAuthState()方法的最后一行。

CoffeeRouter.instance.push(MenuScreen.route());

如果你使用应用程序注册了一个新账户,你可能会收到一封来自Auth0的应用程序确认邮件。

🛠 要确认刷新令牌是否有效,请终止应用程序,并再次运行。该应用程序将从安全商店检索刷新令牌,获得新的访问令牌和ID令牌,然后直接带你到菜单屏幕,绕过登录过程,不询问你的凭证。

简单注销

会话的层级

每次登录都需要注销!这比它看起来要复杂,因为通常有三个会话层需要考虑。

  • 应用会话层。这是应用程序,在这种情况下,它是MJ咖啡应用程序。
  • Auth0会话层。Auth0为每个登录的用户维护一个会话,并将他们的信息存储在cookie或其他方式中。
  • 身份提供者会话层。这是另一个提供身份服务的服务,如Facebook或谷歌。

在用户注销后,你可以将用户重定向到一个特定的URL。你需要在你的租户或应用程序设置中注册重定向URL。

OIDC认证请求的一个参数叫做prompt ,它指定了应如何提示用户重新认证和同意。它也使清除会话变得容易。

prompt 取一个列表,可以包含这些值的任何组合。

  • none:不要显示任何认证或同意的用户界面页面。
  • login:忽略任何现有的会话,要求用户登录。
  • consent:在向应用程序返回信息之前,征求用户的同意。
  • select_account:显示一个提示,要求用户选择一个用户账户。在用户有多个账户的情况下很有用。

🛠幸运的是,AppAuth SDK中支持prompt 。在AuthService 类中(位于 /lib/services/auth_service.dart)中找到了 login()方法,在那里你已经构建了AuthorizationTokenRequest 。改变你对AuthorizationTokenRequest 构造函数的调用,使其包括login 作为prompt 的值。

// /lib/services/auth_service.dart

final authorizationTokenRequest = AuthorizationTokenRequest(
  AUTH0_CLIENT_ID,
  AUTH0_REDIRECT_URI,
  issuer: AUTH0_ISSUER,
  scopes: ['openid', 'profile', 'offline_access', 'email'],
  promptValues: ['login'],
);

删除刷新令牌

由于刷新令牌,用户应该能够切换到另一个应用程序,甚至关闭它,然后返回到MJ咖啡,而不需要重新认证,因为他们仍然在登录。注销意味着用户暂时不再使用该应用程序。下次有人使用该应用程序时,他们应该被要求登录。这可以通过删除刷新令牌来实现。

🛠 要删除刷新令牌,我们需要从安全存储中删除刷新令牌密钥。添加这个 logout()方法到AuthService ,就在 login()方法之后。

// /lib/services/auth_service.dart

Future<void> logout() async {
  await secureStorage.delete(key: REFRESH_TOKEN_KEY);
}

下次用户运行该应用时,他们将被送到主屏幕和其登录按钮,因为该应用不再有刷新令牌,因此没有办法自动认证。

虽然这种方法对MJ Coffee应用来说已经足够了,但我想说的是,你也可以手动调用注销端点并传递必要的参数,如下图所示。

// Example:

Future<bool> logout() async {
  await secureStorage.delete(key: REFRESH_TOKEN_KEY);

  final url = Uri.https(
      AUTH0_DOMAIN,
      '/v2/logout',
      {
        'client_id': AUTH0_CLIENT_ID,
        'federated': '',
        //'returnTo': 'YOUR_RETURN_LOGOUT_URL'
      },
    );

    final response = await http.get(
      url,
      headers: {'Authorization': 'Bearer $auth0AccessToken'},
    );

    print(
      'logout: ${response.request} ${response.statusCode} ${response.body}',
    );

    return response.statusCode == 200;
}

欲了解更多信息,你可以阅读Auth0关于注销的文档。

🛠 让我们启用 "注销 "按钮。它在配置文件屏幕上,是由ProfileScreen 类实现的(位于 /lib/screens/profile.dart).在 build()方法中,找到 "Logout "按钮和它的onPressed 参数。替换 "执行注销 "的注释,使调用的 Padding()函数看起来像这样。

// /lib/screens/profile.dart

Padding(
  padding: const EdgeInsets.symmetric(horizontal: 30),
  child: CommonButton(
    onPressed: () async {
      await AuthService.instance.logout();
      CoffeeRouter.instance.pushReplacement(HomeScreen.route());
    },
    text: 'Logout',
  ),
);

当用户按下 "Logout "时,AuthService 实例的 logout()被调用,用户被重定向到主屏幕。

🛠由于你要使用AuthService's logout()方法,你必须导入它的文件。将以下内容添加到 import语句的顶部添加以下内容 /lib/screens/profile.dart:

import 'package:mjcoffee/services/auth_service.dart';

🛠 重新启动你的应用程序,进入个人资料界面并注销。你会被送回主屏幕。你将不得不登录以再次使用该应用程序。

结语

祝贺你!你刚刚整合了Auth0-pullivan。你刚刚将Auth0驱动的登录和注销整合到MJ Coffee应用程序中。

在接下来的章节中,你将继续为该应用程序添加认证功能。你将了解更多关于刷新令牌轮换、管理出现在登录框中的品牌、角色以及通过苹果和谷歌账户添加社交登录。