# Flutter - google_sign_in - google 第三方登入 ## 簡介 google_sign_in 是一個 Flutter 套件,用於實現 Google 登錄功能。它讓用戶可以通過 Google 帳戶進行身份驗證,以下是常用的套件功能: * Google 登錄:讓用戶可以使用 Google 帳戶輕鬆登入應用程式。 * 獲取用戶基本資訊:如姓名、電子郵件地址、頭像等。 * 多平台支持:適用於 Android、iOS 和 Web 平台。 * 登出功能:支援用戶退出其 Google 帳戶。 * 安全性:基於 OAuth 2.0 協議,確保用戶數據的安全性。 ``` flutter pub add google_sign_in ``` [官網](https://pub.dev/packages/google_sign_in) ## 實作 ### 前置 1. 在 Firebase Console 新增 Android 專案,並添加 SHA-1 及 SHA-256 2. 到 GCP 建立 OAuth 2.0 用戶端 ID ,若沒有 SHA-1 簽署憑證指紋,可以參考[官網或下方流程](https://developer.android.com/studio/publish/app-signing?hl=zh-tw),有用過相關服務會自己建立可以檢查 client id 是否對的上就好 * 如果才在 debug 階段,可以下以下指令取得: ``` keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android ``` * 如果部署上 firebase,可在 Firebase > App Distribution 取得:![image](https://hackmd.io/_uploads/HJwXh4m5Jl.png) * 如果部署上 play 商店可從,測試及發布 > 設定 > 應用程式簽署 取得 2. 填寫 OAuth 同意畫面需要的所有資訊就算非必填也要填寫 ![截圖 2024-12-03 下午5.10.58](https://hackmd.io/_uploads/ryAFiShmke.png) 3. 到 GCP [啟用 Google People API](https://console.cloud.google.com/apis/library/people.googleapis.com) 4. 到 firebase 建立專案並下載 google-services.json 放到專案資料夾 android > app 底下 ### 實作 ```dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_sign_in/google_sign_in.dart'; // 需要請求的權限 GoogleSignIn _googleSignIn = GoogleSignIn( scopes: [ 'email', ] ); class SignInDemo extends StatefulWidget { const SignInDemo({super.key}); @override State createState() => _SignInDemoState(); } class _SignInDemoState extends State<SignInDemo> { GoogleSignInAccount? _currentUser; String idToken = ''; String accessToken = ''; String email = ''; String displayName = ''; String id = ''; String photoUrl = ''; String serverAuthCode = ''; @override void initState() { super.initState(); // 登入人員有進行異動 _googleSignIn.onCurrentUserChanged .listen((GoogleSignInAccount? account) async { GoogleSignInAuthentication googleSignInAuthentication; if (account != null) { googleSignInAuthentication = await account.authentication; idToken = googleSignInAuthentication.idToken ?? ''; accessToken = googleSignInAuthentication.accessToken ?? ''; serverAuthCode = account.serverAuthCode ?? ''; } setState(() { _currentUser = account; email = account?.email ?? ''; displayName = account?.displayName ?? ''; id = account?.id ?? ''; photoUrl = account?.photoUrl ?? ''; }); }); _googleSignIn.signInSilently(); } Future<void> _handleSignIn() async { try { await _googleSignIn.signIn(); } catch (error) { if (!mounted) return; // 避免在 widget 被卸載後繼續使用 context ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('登入失敗:$error')), ); print(error); } } // #enddocregion SignIn Future<void> _handleSignOut() => _googleSignIn.disconnect(); Widget _buildBody() { final GoogleSignInAccount? user = _currentUser; if (user != null) { // The user is Authenticated return SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ ListTile( leading: GoogleUserCircleAvatar( identity: user, ), title: Text('${user.displayName}'), subtitle: Text(user.email), ), const Divider(), copyListTile(title: 'email', subtitle: email), copyListTile(title: 'displayName', subtitle: displayName), copyListTile(title: 'id', subtitle: id), copyListTile(title: 'photoUrl', subtitle: photoUrl), const Divider(), copyListTile(title: 'serverAuthCode', subtitle: serverAuthCode), copyListTile(title: 'idToken', subtitle: idToken), copyListTile(title: 'accessToken', subtitle: accessToken), const Divider(), const Text('Signed in successfully.'), const Divider(), ElevatedButton( onPressed: _handleSignOut, child: const Text('SIGN OUT'), ), ], ), ); } else { // The user is NOT Authenticated return Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ const Text('You are not currently signed in.'), ElevatedButton( onPressed: _handleSignIn, child: const Text('SIGN IN'), ) ], ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Google Sign In'), ), body: ConstrainedBox( constraints: const BoxConstraints.expand(), child: _buildBody(), )); } ListTile copyListTile({required String title, required String subtitle}) { return ListTile( title: Text(title), subtitle: Text(subtitle), onLongPress: () async { await Clipboard.setData(ClipboardData(text: subtitle)).then( (_) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('已複製 $title 到剪貼簿'), ), ); }, ); }, ); } } ``` ## 坑 1. ``` com.google.android.gms.common.api.ApiException: 10 ``` * 填寫 OAuth 同意畫面需要的所有資訊就算非必填也要 https://github.com/flutter/flutter/issues/33393#issuecomment-510395178 * 確認 SHA-1 及 SHA-256 正確 ###### tags: `flutter`