iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Mobile Development

Flutter :30天打造念佛App,跨平台應用從Mobile到VR,讓極樂世界在眼前實現!系列 第 23

[ Day 23 ] Flutter 第三方登入 實戰應用篇—穿越到Coding世界的勇者啊,你知道裝備可以放哪嗎(3) #Apple登入 #Google登入

  • 分享至 

  • xImage
  •  

2025 iThome鐵人賽
「 Flutter :30天打造念佛App,跨平台從Mobile到VR,讓極樂世界在眼前實現 ! 」
Day 23
「 Flutter 第三方登入 實戰應用篇—穿越到Coding世界的勇者啊,你知道裝備可以放哪裡嗎(3)


前言

前兩天我們已經完成了本機與雲端的資料儲存,
讓用戶的資料能被完善保存。
雲端資料庫的存取是需要驗證身份的,用戶只能存取自身相關資料,
所以我們今天要來實作「 Sign In With Apple & Google 」,
讓用戶可以很快速地透過第三方登入建立存取資料的身份

Day23 文章目錄:
一、第三方登入
二、Apple 登入
三、Google 登入


一、第三方登入

1. 簡介

第三方登入是指透過外部身分提供者完成身分驗證與授權
例如:Google、Apple、LINE、Facebook 等。

  • 技術上大多採用 OAuth 2.0 + OpenID Connect(OIDC):
    • OAuth 2.0(授權):讓使用者將「存取某個資源的許可」授權給 App,
      流程的主要產物是 Access Token(用於呼叫API)。
    • OpenID Connect(身分):建立在 OAuth 2.0 之上的身分協定,
      會簽發ID Token(通常為 JWT),讓 App 能確認「是誰在使用」。
  • App 主要拿到:
    • ID Token:用於識別身分 (可驗簽,適合同步到自家後端)。
    • Access Token:用於呼叫供應商的 API(例如取得使用者 Profile)。

2. 常見實作方式

實作方式 做法 / 套件 優點 缺點 工時
Firebase Auth 聚合 firebase_auth + google_sign_in、sign_in_with_apple 跨 iOS/Android 一致;帳號連結、省驗證;狀態管理簡單;安全性由 Firebase 托管 LINE 不在內建清單;綁 Firebase 生態 最快
各家原生 SDK google_sign_in、sign_in_with_apple、LINE 官方 SDK 不綁 Firebase;可完全自訂;能直連既有後端 各家 SDK 細節多、平台差異大;Token 驗簽與管理需自理 中等
通用 OAuth + PKCE flutter_web_auth_2 + http + crypto 適合沒有優質套件的供應商;高可控性 需熟悉OAuth;回跳/換Token/錯誤處理需自己實作 偏高

3. 相關名詞

名詞 定義 / 內容 來源 主要用途 出現階段 範例/備註
Token 一段字串(多半是 JWT)代表某種權限/身分 身分供應商(Google/Apple)或 Firebase ID Token:辨識使用者身分;Access Token:呼叫供應商 API;Refresh Token:換新 access token 執行階段(登入後/呼叫 API 時) Google id_token/access_token;Firebase ID token(Firestore/Functions 依此授權)
憑證(Credential, 登入憑據) 交給 FirebaseAuth 的「登入資料包」,把第三方 token 封裝起來 由 App 依第三方 token 建立 讓 FirebaseAuth.singInWithCredential(...)登入/建立 Firebase 使用者 執行階段(登入時) Google:GoogleAuthProvider.credential(idToken, accessToken);Apple:OAuthProvider('apple.com').credential(idToken, rawNonce)
憑證(Certificate, 程式簽署憑證) X.509 憑證(含公私鑰),用來簽 App Apple Developer 讓 App 可安裝到實機、可上架 建置/發佈階段 Apple Development / Apple Distribution certificate
描述檔(Provisioning Profile) 「通行證 + 規則」:綁 App ID、允許的 Certificate(Dev/Dist)、(開發/Ad Hoc)裝置清單、Entitlements Apple Developer 決定此 App 用哪些權限/哪張證書簽、能裝到哪些裝置 建置/發佈階段 iOS App Development / Ad Hoc / App Store(Distribution) Profile;自動簽章用 Xcode Managed Profile
簽章(Code Signing) 用 Certificate + Provisioning Profile 對 App 二進位做數位簽署 本機 Xcode(開發者個人私鑰) 產生可安裝/可上架的 App;把 Entitlements(如 Sign in with Apple)寫進去 建置/發佈階段 Xcode 顯示:Signing Certificate ...、Provisioning Profile ...

二、Apple 登入

1. Apple Developer ( 帳號開啟 Sign in with Apple )

提醒:
每個 flavor 有自己的 Bundle ID 與對應的 Target / Scheme / Build Configuration。
所以都需要分別設定以下流程。

2. 設定 Xcode

3. 設定 Firebase Authentication

4. 安裝套件 pubspec.yaml

dependencies:
  firebase_core: ^latest
  firebase_auth: ^latest
  sign_in_with_apple: ^5.0.0

5. 登入firebase_auth_repository.dart

Apple 通常只會在首次授權回傳用戶的 姓名 / Email (通常只給一次),
所以要自行存到 Firestore。

import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:firebase_auth/firebase_auth.dart' as fb;

class FirebaseAuthRepository {
  final fb.FirebaseAuth _auth = fb.FirebaseAuth.instance;

  Future<fb.UserCredential> signInWithApple() async {
    //產生 nonce
    final rawNonce = _randomNonce();
    final hashedNonce = _sha256ofString(rawNonce);

    //Apple 授權
    final apple = await SignInWithApple.getAppleIDCredential(
      scopes: [AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName],
      nonce: hashedNonce,
    );

    //交給 Firebase
    final oauthCred = fb.OAuthProvider('apple.com').credential(
      idToken: apple.identityToken,
      rawNonce: rawNonce,
    );
    final cred = await _auth.signInWithCredential(oauthCred);

    // 拿到名字存起來(Apple 只會給一次)
    final displayName = [
      apple.givenName ?? '',
      apple.familyName ?? '',
    ].where((s) => s.isNotEmpty).join(' ').trim();

    if (displayName.isNotEmpty && cred.user != null && cred.user!.displayName == null) {
      await cred.user!.updateDisplayName(displayName);
    }
    return cred;
  }

  String _randomNonce([int length = 32]) {
    const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
    final rnd = Random.secure();
    return List.generate(length, (_) => chars[rnd.nextInt(chars.length)]).join();
  }

  String _sha256ofString(String input) {
    final bytes = sha256.convert(utf8.encode(input)).bytes;
    return base64Url.encode(bytes).replaceAll('=', '');
  }
}

三、Google 登入

1. 安裝套件 pubspec.yaml

dependencies:
  firebase_core: ^latest  
  google_sign_in: ^latest   #跳授權頁、取得 google token
  firebase_auth: ^latest    #用 google 憑證 建立/取得 對應的firebase使用者

2. 平台設定

提醒:
每個 flavor 都要在 firebase 各自建立 App,
每個 flavor 都有各自的 bundle ID / applicationId,
所以都需要各自設定流程。

Android :
(1) Firebase 添加Android App應用 (詳見 Day22文章)
(2) 下載 google-services.json 加入Android Studio專案 (詳見 Day22文章)
(3) 設定 android/build.gradle、android/app/build.gradle (詳見 Day22文章)

(4) 取得 SHA-1 / SHA-256

SHA-1、SHA-256 都是雜湊演算法。
在 Android 簽章情境中,它們用來對簽署憑證做雜湊,得到一段指紋。
Google/Firebase 會將 包名 (package name / applicationId) 與簽署憑證指紋綁一起,
只有這把金鑰簽過的 App 才能使用對應的 OAuth 用戶端(否則 Google Sign-In 會失敗)。
指紋只是憑證的縮影;真正要保密的是 keystore 檔與密碼。

名稱 情境 作用 在 Firebase 需要登錄的指紋
Debug signing key 本機開發 簽 debug 變體 SHA-1 / SHA-256
Upload/Release signing key 本機開發 簽要上傳的包(若不上架就是最終包) SHA-1 / SHA-256
App signing key(Play) Google Play Play 伺服器重新簽最終包 SHA-1 / SHA-256(從 Play Console 抓)

A.Debug signing key

(a) 列出所有 build variant 簽章資訊

cd android //移動到android目錄
//macOS
./gradlew :app:signingReport //跑 app 模組列出每個 build variant 的簽章資訊

(b) 取得SHA1、SHA-256(build type : debug)

B.Release signing key

(a) 建立 release keystore

//先確認位置:android目錄,執行以下(macOS)

keytool -genkeypair -v \
  -keystore app/release.keystore \
  -alias upload \
  -keyalg RSA -keysize 2048 -validity 10000

  • 填入資訊
輸入金鑰儲存庫密碼:  
重新輸入新密碼: 
Enter the distinguished name. Provide a single dot (.) to leave a sub-component empty or press ENTER to use the default value in braces.
您的名字與姓氏為何?
  [Unknown]:  
您的組織單位名稱為何?
  [Unknown]:  
您的組織名稱為何?
  [Unknown]:  
您所在的城市或地區名稱為何?
  [Unknown]:  
您所在的州及省份名稱為何?
  [Unknown]:  
此單位的兩個字母國別代碼為何?
  [Unknown]:  
  • ~/.gradle/gradle.properties 放置密碼(使用者本機,最小揭露安全)
  • 終端機執行
mkdir -p ~/.gradle.                  //建資料夾
nano ~/.gradle/gradle.properties     //終端機nano編輯
  • nano編輯
//設定儲存路徑和密碼
MY_KEYSTORE=/Users/*****/Desktop/Amitabha/amitabha/android/app/release.keystore
MY_KEY_ALIAS=upload
MY_KEYSTORE_PASSWORD=<你的密碼>
MY_KEY_PASSWORD=<你的密碼>
  • 終端機執行
//nano編輯
1. 編輯完進行儲存: Ctrl + O
2. 確認檔名: Enter 
3. 退出: Ctrl + X 
  • 檢查,keystore 有被gitignore忽略:
git check-ignore -v android/app/release.keystore

//有輸出代表已忽略,例如:
android/.gitignore:13:**/*.keystore     android/app/release.keystore

(b) 添加singingConfigs、簽章改成release ( android/app/build.gradle.kts )

android {
    // 略...其他原本的設定(namespace/compileSdk/defaultConfig...)
		
    signingConfigs {
        create("release") {
            val ks = (project.findProperty("MY_KEYSTORE") ?: System.getenv("MY_KEYSTORE")) as String
            storeFile = file(ks)
            storePassword = (project.findProperty("MY_KEYSTORE_PASSWORD")
                ?: System.getenv("MY_KEYSTORE_PASSWORD")) as String
            keyAlias = (project.findProperty("MY_KEY_ALIAS")
                ?: System.getenv("MY_KEY_ALIAS")) as String
            keyPassword = (project.findProperty("MY_KEY_PASSWORD")
                ?: System.getenv("MY_KEY_PASSWORD")) as String
        }
    }

    buildTypes {
        release {
            // 將簽章從debug改成release
            signingConfig = signingConfigs.getByName("release")
        }
    }
}

(c) 取得SHA1、SHA-256(build type : release)

//Android目錄 
./gradlew :app:signingReport

C. App signing key

上架到 Google Play 會啟用 Play 應用程式簽署:
記得到 Play Console 的 應用程式完整性 頁面,複製App signing key的SHA-1/256,
添加到 Firebase / Android OAuth 用戶端。

(5) 添加 SHA-1 / SHA-256 到 Firebase

iOS :
(1) Firebase 添加iOS App應用 (詳見 Day22文章)
(2) 下載 GoogleService-Info.plist 加入 Xcode 專案 (詳見 Day22文章)

(3) URL Types 加上 Reversed Client ID( GoogleService-Info.plist )

確認 firebase Auth 啟用 Google、Apple 登入

3. 初始化 main.dart

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const MyApp());
}

4. 登入 / 登出 firebase_auth_repository.dart

import 'package:firebase_auth/firebase_auth.dart' as fb;
import 'package:google_sign_in/google_sign_in.dart' as gsi;
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:flutter/services.dart' show PlatformException;
import '../domain/auth_error.dart';

class FirebaseAuthRepository {
  final fb.FirebaseAuth _auth = fb.FirebaseAuth.instance;
  final _google = GoogleSignIn(scopes: ['email', 'profile']);

  // ===== Google =====
  @override
  Future<fb.UserCredential> signInWithGoogle() async {
    try {
      //先嘗試輕量登入
      gsi.GoogleSignInAccount? account;
      final f = _gsi.attemptLightweightAuthentication();
      if (f != null) {
        account = await f; 
      }

      //互動式登入
      account ??= await _gsi.authenticate(scopeHint: const ['email', 'profile']);

      //拿 idToken
      final idToken = account.authentication.idToken;

      //給 Firebase
      final cred = fb.GoogleAuthProvider.credential(
        idToken: idToken,
        accessToken: null, 
      );
      return await _auth.signInWithCredential(cred);
    } on gsi.GoogleSignInException catch (e) {
      // 列舉型別錯誤碼
      if (e.code == gsi.GoogleSignInExceptionCode.canceled) {
        throw const AuthException(AuthError.cancelled);
      }
      throw const AuthException(AuthError.failed);
    } on fb.FirebaseAuthException catch (e) {
      if (e.code == 'network-request-failed') {
        throw const AuthException(AuthError.network);
      }
      throw const AuthException(AuthError.failed);
    } on PlatformException catch (e) {
      final code = e.code.toLowerCase();
      if (code.contains('network')) throw const AuthException(AuthError.network);
      if (code.contains('canceled') || code.contains('cancelled')) {
        throw const AuthException(AuthError.cancelled);
      }
      throw const AuthException(AuthError.failed);
    }
  }

  // ===== Apple登入略,同前段 =====
  
  
  //登出
  Future<void> signOut() async {
    try { await _gsi.signOut(); } catch (_) {}
    await _auth.signOut();
  }

  Stream<fb.User?> authStateChanges() => _auth.authStateChanges();

}

5. 錯誤處理 features/auth/domain/auth_error.dart

enum AuthError {
  network,   // 網路問題
  cancelled, // 使用者取消
  failed,    // 其他登入失敗
}

class AuthException implements Exception {
  final AuthError code;
  final String? message; 
  const AuthException(this.code, [this.message]);

  @override
  String toString() => 'AuthException($code, $message)';
}

Day23 重點回顧

重點 內容
第三方登入 Firebase Auth 聚合,開發時長較短
Apple 登入 Apple 首次回傳的姓名/Email 要記得立即保存
Google 登入 Android 要添加指紋 SHA-1/256 至 Firebase

上一篇
[ Day 22 ] Flutter 資料儲存 實戰應用篇—穿越到Coding世界的勇者啊,你知道裝備可以放哪嗎(2) #雲端資料庫
下一篇
[ Day 24 ] Flutter 多國語系 — App翻譯蒟蒻, 上架各國必備的好幫手!
系列文
Flutter :30天打造念佛App,跨平台應用從Mobile到VR,讓極樂世界在眼前實現!24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言