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 ... |
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('=', '');
}
}
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)';
}
重點 | 內容 |
---|---|
第三方登入 | Firebase Auth 聚合,開發時長較短 |
Apple 登入 | Apple 首次回傳的姓名/Email 要記得立即保存 |
Google 登入 | Android 要添加指紋 SHA-1/256 至 Firebase |