使用Flutter和Firebase认证的苹果登录

1,291 阅读7分钟

在本教程中,我们将看到如何从头开始向我们的Flutter和Firebase应用程序添加苹果登录

我们将使用pub.dev上的the_apple_sign_inFlutter插件。

这个插件使得在iOS上支持Apple Sign In成为可能,并在Firebase上将其作为一种认证方法

然而,请注意,这个插件只支持iOS,不支持Android,而且你只能在运行iOS 13及以上的设备上使用。

如果你想在iOS和Android上都启用苹果登录,你应该使用sign_in_with_apple代替。这将在这两个平台上工作,但整合需要一些服务器端的代码

一些背景

Apple Sign In是一种新的认证方法,在iOS 13及以上版本中可用。

它非常方便,因为你的iOS用户已经有一个Apple ID,可以用它来登录你的应用程序。

因此,就像你在Android上提供Google Sign In一样,在iOS上提供Apple Sign In是有意义的。

前提条件

  • 安装了Xcode 11
  • 一个苹果开发者账户
  • 一个iOS 13.x设备或模拟器,用一个Apple ID登录。

项目设置

在创建一个新的Flutter项目后,将以下依赖关系添加到您的pubspec.yaml 文件。

dependencies:
  the_apple_sign_in: ^1.1.1
  cupertino_icons: ^1.0.3
  firebase_auth: ^3.1.1
  firebase_core: ^1.6.0
  provider: ^6.0.0

注意:我们将使用Provider进行依赖性注入,但如果你愿意,也可以使用其他东西。

接下来,我们需要将Firebase添加到我们的Flutter应用中。按照这个指南来做。

在我们完成所需的步骤后,GoogleService-Info.plist 文件应该被添加到我们的Xcode项目中。

而在Xcode 11中,选择签名和能力标签,并添加 "Sign In With Apple "作为一个新的能力。

添加Sign In With Apple功能

一旦完成这些,确保在代码签名部分选择一个团队。

设置代码签名选项

这将在苹果开发者门户的 "证书、标识符和配置文件 "部分生成和配置一个应用程序ID。如果你不这样做,签到就无法进行。

作为最后一步,我们需要在Firebase中启用苹果登录。这可以在Authentication ->Sign-in method 下完成。

这样就完成了Apple Sign In的设置,我们就可以深入研究代码了。

检查Apple Sign In是否可用

在我们添加UI代码之前,让我们写一个简单的类来检查苹果登录是否可用*(记住,我们只支持iOS 13+*)。

import 'package:apple_sign_in/apple_sign_in.dart';

class AppleSignInAvailable {
  AppleSignInAvailable(this.isAvailable);
  final bool isAvailable;

  static Future<AppleSignInAvailable> check() async {
    return AppleSignInAvailable(await AppleSignIn.isAvailable());
  }
}

然后,在我们的main.dart 文件中,让我们修改入口点。

void main() async {
  // Fix for: Unhandled Exception: ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  final appleSignInAvailable = await AppleSignInAvailable.check();
  runApp(Provider<AppleSignInAvailable>.value(
    value: appleSignInAvailable,
    child: MyApp(),
  ));
}

第一行防止在绑定初始化之前,如果我们试图在平台通道上发送消息,就会出现异常。这是有必要的,因为我们在运行应用程序之前调用Firebase.initializeApp()

然后,我们通过使用我们刚刚创建的类来检查苹果登录是否可用。

然后我们使用Provider来使这个值对我们应用程序中的所有部件可用。

注意:这个检查是提前完成的,这样appleSignInAvailable ,整个widget树都可以同步使用。这样我们就不需要一个FutureBuilder 来在我们的widget中执行这个检查。

添加UI代码

取代默认的计数器应用程序,我们想显示一个带有按钮的登录页面。

import 'package:apple_sign_in/apple_sign_in.dart';
import 'package:apple_sign_in_firebase_flutter/apple_sign_in_available.dart';
import 'package:apple_sign_in_firebase_flutter/auth_service.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class SignInPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final appleSignInAvailable =
        Provider.of<AppleSignInAvailable>(context, listen: false);
    return Scaffold(
      appBar: AppBar(
        title: Text('Sign In'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(6.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [            
            if (appleSignInAvailable.isAvailable)
              AppleSignInButton(
                style: ButtonStyle.black, // style as needed
                type: ButtonType.signIn, // style as needed
                onPressed: () {},
              ),
          ],
        ),
      ),
    );
  }
}

注意:我们使用collection-if来显示AppleSignInButton ,如果苹果登录是可用的。请看这个视频,了解Dart中的UI-as-code操作。

回到我们的main.dart 文件,我们可以更新我们的根部件来使用SignInPage

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Apple Sign In with Firebase',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.indigo,
      ),
      home: SignInPage(),
    );
  }
}

在这个阶段,我们可以在iOS 13模拟器上运行该应用,得到以下结果。

苹果登录按钮

添加认证代码

这里是我们将用于与苹果公司签到的完整认证服务(下面解释)。

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/services.dart';
import 'package:the_apple_sign_in/the_apple_sign_in.dart';

class AuthService {
  final _firebaseAuth = FirebaseAuth.instance;

  Future<User> signInWithApple({List<Scope> scopes = const []}) async {
    // 1. perform the sign-in request
    final result = await TheAppleSignIn.performRequests(
        [AppleIdRequest(requestedScopes: scopes)]);
    // 2. check the result
    switch (result.status) {
      case AuthorizationStatus.authorized:
        final appleIdCredential = result.credential!;
        final oAuthProvider = OAuthProvider('apple.com');
        final credential = oAuthProvider.credential(
          idToken: String.fromCharCodes(appleIdCredential.identityToken!),
          accessToken:
              String.fromCharCodes(appleIdCredential.authorizationCode!),
        );
        final userCredential =
            await _firebaseAuth.signInWithCredential(credential);
        final firebaseUser = userCredential.user!;
        if (scopes.contains(Scope.fullName)) {
          final fullName = appleIdCredential.fullName;
          if (fullName != null &&
              fullName.givenName != null &&
              fullName.familyName != null) {
            final displayName = '${fullName.givenName} ${fullName.familyName}';
            await firebaseUser.updateDisplayName(displayName);
          }
        }
        return firebaseUser;
      case AuthorizationStatus.error:
        throw PlatformException(
          code: 'ERROR_AUTHORIZATION_DENIED',
          message: result.error.toString(),
        );

      case AuthorizationStatus.cancelled:
        throw PlatformException(
          code: 'ERROR_ABORTED_BY_USER',
          message: 'Sign in aborted by user',
        );
      default:
        throw UnimplementedError();
    }
  }
}

首先,我们向我们的方法传递一个List<Scope> 参数。范围是可以从用户那里请求的联系信息的种类(emailfullName)。

然后,我们对AppleSignIn.performRequestsawait 进行调用以获得结果。

最后,我们用switch 语句来解析结果。三个可能的情况是:authorized,errorcancelled

已授权

如果请求被授权,我们用收到的identityTokenauthorizationCode 创建一个OAuthProvider 证书。

然后我们将其传递给_firebaseAuth.signInWithCredential() ,并得到一个UserCredential ,我们可以用它来提取FirebaseUser

而如果我们要求提供全名,我们可以用Apple ID凭证中的fullName ,更新FirebaseUser 对象的资料信息。

注意:根据这个问题,苹果公司的签到API在初次登录后总是返回null ,以获取fullName 数据。之后,返回的唯一信息是uid 。因此,上面的代码在更新Firebase显示名称之前有一些防御性的null 检查。

错误或取消

如果认证失败或被用户取消,我们会抛出一个PlatformException ,可以由调用网站处理。

使用认证代码

现在我们的认证服务已经准备好了,我们可以像这样通过Provider 把它添加到我们的应用程序中。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<AuthService>(
      create: (_) => AuthService(),
      child: MaterialApp(
        title: 'Apple Sign In with Firebase',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.indigo,
        ),
        home: SignInPage(),
      ),
    );
  }
}

然后,在我们的SignInPage ,我们可以添加一个方法来签到和处理任何错误。

Future<void> _signInWithApple(BuildContext context) async {
  try {
    final authService = Provider.of<AuthService>(context, listen: false);
    final user = await authService.signInWithApple(
        scopes: [Scope.email, Scope.fullName]);
    print('uid: ${user.uid}');
  } catch (e) {
    // TODO: Show alert here
    print(e);
  }
}

最后,我们记得要在AppleSignInButton 的回调中调用这个方法。

AppleSignInButton(
  style: ButtonStyle.black,
  type: ButtonType.signIn,
  onPressed: () => _signInWithApple(context),
)

测试事情

我们的实现已经完成,我们可以运行这个应用程序。

如果我们按下登录按钮,而我们的模拟器或设备上没有配置Apple ID,我们将得到以下结果。

苹果登录设置提示

用我们的苹果ID登录后,我们可以再次尝试,我们会得到这样的结果。

继续之后,我们会被提示输入我们的苹果ID的密码(或者使用触摸ID/面容ID,如果设备上启用的话)。如果我们要求全名和电子邮件访问,用户将有机会编辑名字,并选择分享或隐藏电子邮件。

在确认了这一点之后,签到就完成了,应用程序就通过了Firebase的认证。

注意:如果认证后签到界面没有被驳回,很可能是因为你忘记在Xcode的代码签名选项中设置团队。

下一个合乎逻辑的步骤是脱离SignInPage ,转而显示主页。这可以通过在SignInPage 上面添加一个小部件来实现,根据onAuthStateChaged 流的FirebaseAuth 来决定显示哪一个页面。如何实现这个目标,请看这个教程

恭喜你,你现在已经在你的Flutter应用程序中启用了苹果登录功能!你的iOS用户非常感激。你的iOS用户很感激。🙏

本教程的完整源代码在GitHub上

编码愉快!