iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Mobile Development

我的 Flutter 進化論:30 天打造「Crew Up!」的架構之旅系列 第 14

Day 14 - Firebase Authentication:從 Google 登入到完整認證系統

  • 分享至 

  • xImage
  •  

大家好,歡迎來到第十四天!在 Day 13,我們完成了 Firebase 專案的設定。今天,我們要來實作 Crew Up! 的認證系統,讓使用者能夠安全地登入和使用我們的 App。

Firebase Authentication 是 Google 提供的完整認證解決方案,支援多種登入方式,包括 Google、Facebook、Email/Password 等。對於 Crew Up! 這樣的社交應用,我們選擇 Google 登入作為主要認證方式,因為:

  • 使用者體驗佳:一鍵登入,無需記住密碼
  • 安全性高:Google 的 OAuth 2.0 機制
  • 整合容易:與 Firebase 生態系統完美整合
  • 跨平台支援:iOS、Android、Web 都能使用

認證系統架構設計

在 Crew Up! 專案中,我們遵循 Clean Architecture 原則,將認證邏輯分層實作:

Data Layer - AuthRemoteDataSource

注意:以下是簡化版本,適合理解基本概念。實際生產環境會包含重試機制、詳細錯誤處理、Token 管理、網路感知等進階功能。

// lib/features/auth/data/datasources/auth_remote_datasource.dart

// (imports omitted)

class AuthRemoteDataSource implements AuthDataSource {
  final FirebaseAuth _firebaseAuth;

  AuthRemoteDataSource({
    FirebaseAuth? firebaseAuth,
  }) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;

  @override
  Future<UserModel> signInWithGoogle() async {
    final GoogleSignIn googleSignIn = GoogleSignIn.instance;
    final GoogleSignInAccount googleUser = await googleSignIn.authenticate();
    final GoogleSignInAuthentication googleAuth = googleUser.authentication;

      final credential = GoogleAuthProvider.credential(
        accessToken: googleAuth.accessToken,
        idToken: googleAuth.idToken,
      );

    final userCredential = await _firebaseAuth.signInWithCredential(credential);
    final firebaseUser = userCredential.user;
    
    if (firebaseUser == null) {
      throw AppException.unknown('Google 登入失敗:無法獲取使用者資訊');
    }

    return UserModel.fromFirebaseUser(firebaseUser);
  }
}

Repository Layer - AuthRepositoryImpl

注意:以下是簡化版本,實際專案會繼承 EnhancedBaseRepository 並使用統一的錯誤處理策略。

// lib/features/auth/data/repositories/auth_repository_impl.dart

// (imports omitted)

class AuthRepositoryImpl implements AuthRepository {
  final AuthDataSource _remoteDataSource;

  AuthRepositoryImpl({required AuthDataSource remoteDataSource})
      : _remoteDataSource = remoteDataSource;

  @override
  Future<Result<User>> signInWithGoogle() async {
    final userModel = await _remoteDataSource.signInWithGoogle();
    return Success(userModel.toEntity());
  }

  @override
  Stream<Result<User?>> authStateChanges() {
    return _remoteDataSource.authStateChanges().map(
      (userModel) => Success(userModel?.toEntity()),
    );
  }
}

Presentation Layer - AuthNotifier

AuthNotifier 現在完全依賴 Firebase SDK 的認證狀態管理,提供簡潔且可靠的認證流程。詳細實作請參考下方的「Firebase SDK 的自動認證狀態管理」章節。

實際應用:LoginScreen 實作

現在讓我們看看如何在 UI 層面使用這些認證功能:

// lib/features/auth/presentation/screens/login_screen.dart

// (imports omitted)

class LoginScreen extends ConsumerWidget {
  const LoginScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authState = ref.watch(authNotifierProvider);
    final localizations = S.of(context);

    return Scaffold(
      backgroundColor: AppColors.surfaceBackground2,
      body: SafeArea(
        child: Padding(
          padding: AppSpacing.horizontalL,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // Logo
              SvgPicture.asset('assets/login/logo.svg', width: 137, height: 54),
              const SizedBox(height: AppSpacing.xl),

              // Google 登入按鈕
              SizedBox(
                width: double.infinity,
                height: 48,
                child: ElevatedButton(
                  onPressed: authState.isLoading ? null : () => _handleGoogleSignIn(ref),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: AppColors.surfaceWhite,
                    foregroundColor: AppColors.textTertiary,
                    shape: const RoundedRectangleBorder(
                      borderRadius: AppRadius.buttonMediumRadius,
                      side: BorderSide(color: AppColors.surfaceBorder),
                    ),
                  ),
                  child: authState.isLoading
                      ? const CircularProgressIndicator(color: AppColors.textTertiary)
                      : Text(
                          localizations.googleLogin,
                          style: AppTypography.body1.copyWith(
                            color: AppColors.textTertiary,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _handleGoogleSignIn(WidgetRef ref) async {
    await ref.read(authNotifierProvider.notifier).signInWithGoogle();
  }
}

進階功能:Firebase SDK 的自動認證狀態管理

Firebase SDK 的自動認證狀態管理

Firebase Authentication SDK 提供了完整的認證狀態管理,包括自動 Token 刷新、跨 App 重啟的狀態持久化等。在 CrewUp 專案中,我們完全依賴 Firebase SDK 的內建機制:

// lib/features/auth/presentation/providers/auth_provider.dart

// (imports omitted)

// Repository Provider - 依賴注入 Repository 實作
@riverpod
AuthRepository authRepository(Ref ref) {
  final remoteDataSource = ref.read(authRemoteDataSourceProvider);
  return AuthRepositoryImpl(remoteDataSource);
}

// DataSource Provider - 提供 Firebase 認證資料來源
@riverpod
AuthDataSource authRemoteDataSource(Ref ref) => AuthRemoteDataSource();

// Use Case Provider - 提供 Google 登入業務邏輯
@riverpod
SignInWithGoogleUseCase signInWithGoogleUseCase(Ref ref) {
  final repository = ref.read(authRepositoryProvider);
  return SignInWithGoogleUseCase(repository);
}

// Auth State Provider - 完全依賴 Firebase SDK 的認證狀態管理
@riverpod
class AuthNotifier extends _$AuthNotifier {
  StreamSubscription<Result<domain.User?>>? _authSubscription;

  @override
  Future<domain.User?> build() async {
    final repository = ref.read(authRepositoryProvider);

    // 監聽認證狀態變化 - Firebase SDK 是唯一的真相來源
    _authSubscription = repository.authStateChanges().listen((result) {
      result.fold(
        (user) => state = AsyncValue.data(user),
        (error) => state = AsyncValue.error(error, StackTrace.current),
      );
    });

    // Provider 釋放時取消訂閱 - 避免記憶體洩漏
    ref.onDispose(() {
      _authSubscription?.cancel();
    });

    // 獲取當前用戶 - 支援 App 重啟後的狀態恢復
    final currentUserResult = await repository.getCurrentUser();
    return currentUserResult.fold(
      (user) => user,
      (error) => null,
    );
  }

  /// Google 登入 - 使用 UseCase 處理業務邏輯
  Future<void> signInWithGoogle() async {
    try {
      state = const AsyncValue.loading();
      final signInUseCase = ref.read(signInWithGoogleUseCaseProvider);
      final user = await signInUseCase();
      state = AsyncValue.data(user);
    } on Exception catch (e) {
      state = AsyncValue.error(e, StackTrace.current);
    }
  }
}

Firebase SDK 的自動 Token 管理

Firebase SDK 使用雙 Token 系統來管理認證狀態:

  • ID Token:用於識別用戶身份,有效期約 1 小時
  • Refresh Token:用於自動刷新 ID Token,有效期較長

自動刷新機制:

// Firebase SDK 會自動處理 Token 刷新
// 當 ID Token 即將過期時,SDK 會自動使用 Refresh Token 獲取新的 ID Token
// 開發者無需手動處理這個過程

跨 App 重啟的狀態持久化

Firebase SDK 會自動將認證狀態保存到本地儲存,當 App 重新啟動時,會自動恢復用戶的登入狀態:

// 在 App 啟動時,Firebase SDK 會自動檢查本地儲存的認證狀態
// 如果找到有效的 Refresh Token,會自動刷新 ID Token
// 這確保了用戶在 App 重啟後仍然保持登入狀態

認證狀態的統一管理

在 Crew Up! 專案中,我們使用 Riverpod 來統一管理認證狀態:

// 在任何 Widget 中都可以監聽認證狀態
final authState = ref.watch(authNotifierProvider);

authState.when(
  data: (user) {
    if (user != null) {
      // 用戶已登入
      return HomeScreen();
    } else {
      // 用戶未登入
      return LoginScreen();
    }
  },
  loading: () => LoadingScreen(),
  error: (error, stack) => ErrorScreen(error),
);

實作重點與最佳實踐

錯誤處理策略

在認證流程中,我們需要處理各種可能的錯誤情況:

// 網路錯誤
if (error is SocketException) {
  // 顯示網路連接錯誤
}

// 用戶取消登入
if (error is GoogleSignInException && 
    error.code == GoogleSignInExceptionCode.canceled) {
  // 用戶主動取消,不顯示錯誤
}

// 其他認證錯誤
else {
  // 顯示一般錯誤訊息
}

安全性考量

  • Token 安全:Firebase SDK 會自動處理 Token 的安全儲存
  • HTTPS 強制:所有認證請求都必須使用 HTTPS
  • CORS 設定:Web 平台需要正確設定 CORS 政策

使用者體驗優化

  • Loading 狀態:在認證過程中顯示適當的載入指示器
  • 錯誤回饋:提供清晰的錯誤訊息和重試選項
  • 無縫體驗:利用 Firebase SDK 的自動狀態管理

實際應用場景

首次登入流程

  1. 用戶點擊「Google 登入」按鈕
  2. 開啟 Google 登入頁面
  3. 用戶完成 Google 認證
  4. Firebase 驗證認證結果
  5. 創建或更新用戶資料
  6. 導航到主頁面

自動登入流程

  1. App 啟動時檢查本地認證狀態
  2. 如果找到有效的 Refresh Token,自動刷新 ID Token
  3. 更新認證狀態
  4. 導航到相應頁面

登出流程

  1. 清除 Firebase 認證狀態
  2. 清除 Google Sign-In 狀態
  3. 更新本地狀態
  4. 導航到登入頁面

總結

今天我們完成了 Crew Up! 的 Firebase Authentication 整合:

核心成果

  • 完整的認證架構:從 Data Layer 到 Presentation Layer 的完整實作
  • Google 登入整合:一鍵登入的流暢體驗
  • 自動狀態管理:Firebase SDK 的強大功能
  • 錯誤處理機制:完善的錯誤處理和用戶回饋

技術要點

  • Clean Architecture 與 Firebase Authentication 的完美結合
  • Riverpod 2.0 的狀態管理最佳實踐
  • 多國語系與設計系統的整合
  • 完整的錯誤處理策略

下一步

明天,我們將深入探討 Cloud Firestore 的實作,學習如何在 Clean Architecture 中整合 NoSQL 資料庫,建立可擴展的資料架構,並實作活動管理、用戶資料等核心功能。

期待與您在 Day 15 相見!


相關資源

專案資訊

  • 專案名稱: Crew Up!
  • 開發日誌: Day 14 - Firebase Authentication:從 Google 登入到完整認證系統
  • 文章日期: 2025-09-28
  • 技術棧: Flutter 3.8+, Firebase Auth, Google Sign-In, Riverpod 2.0

上一篇
Day 13 - Firebase 專案設定:從多環境管理到雲端整合
下一篇
Day 15 - Cloud Firestore:即時資料庫設計與優化
系列文
我的 Flutter 進化論:30 天打造「Crew Up!」的架構之旅16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言