꿈꾸는 시스템 디자이너

Flutter 강좌 - [Firebase] 구글 로그인(Google Sign in) 사용법 본문

Development/Flutter

Flutter 강좌 - [Firebase] 구글 로그인(Google Sign in) 사용법

독행소년 2019. 11. 4. 16:04

Flutter Code Examples 강좌를 추천합니다.

  • 제 블로그에서 Flutter Code Examples 프로젝트를 시작합니다.
  • Flutter의 다양한 예제를 소스코드와 실행화면으로 제공합니다.
  • 또한 모든 예제는 Flutter Code Examples 앱을 통해 테스트 가능합니다.

Flutter Code Examples 강좌로 메뉴로 이동

Flutter Code Examples 강좌 목록 페이지로 이동

Flutter Code Examples 앱 설치 | Google Play Store로 이동

 

Flutter Code Examples - Google Play 앱

Are you a beginner at Flutter? Check out the various features of Flutter through the demo. Source code for all demos is also provided.

play.google.com


 

Flutter 강좌 시즌2 목록 : https://here4you.tistory.com/149

 

이번 강좌에서는 이전시간에 구현한 Flutter Firebase 앱에서 스마트폰의 구글 계정에 접근하여 구글 계정의 정보를 획득하는 방법에 대해서 알아본다.

우선 기본 개념에 대해서 알아본다.

서버 기반의 서비스를 제공하고자 할 때 사용자의 로그인 없이 불특정 다수에게 서비스를 제공할 수도 있지만, 서비스에 가입한 사용자에게만 서비스를 제공할 수도 있다. 후자의 경우에는 서비스 이용 전에 로그인 단계를 거쳐 사용자가 서비스를 이용할 권한이 있는지를 확인하게 된다.

기존에는 서비스 가입과 로그인 기능을 서비스 제공자가 직접 개발해서 운영했었지만 사용자들은 회원 가입 과정을 불편해한다. 그래서 구글, 페이스북, 트위터 등의 주요 SNS의 계정을 이용해서 쉽게 서비스에 가입하고 로그인을 하는 기능들이 유행하고 있다.

이번 강좌에서는 사용자의 스마트폰에 연결된 구글 계정의 정보를 획득하는 방법에 대해서 알아본다.

이번 강좌는 이전 강좌에서 개발한 Flutter Firebase 소스코드를 이용해서 진행한다.

 

1. 구글 API 콘솔 설정

앱이 사용자(스마트폰 주인)의 구글 계정 정보를 획득하기 위해서는 구글 API의 도움이 필요하다. 개발자가 임의로 사용자의 계정에 접근해서는 안되기 때문에 구글에서 직접 지원하는 구글 API를 통해서 계정 정보에 접근하는 것이다.

우선 구글 API 콘솔에 접속한다.

https://console.developers.google.com

 

화면 상단에서 프로젝트를 선택한다. 전체 항목을 선택하면 기존의 Firebase에 추가한 프로젝트(Flutter Firebase)가 이미 등록되어 있다. Flutter Firebase를 선택한다.

 

 

앱이 사용자의 계정 정보에 접근하도록 하기 위해서는 OAuth 클라이언트와 동의 화면의 설정이 필요하다.

OAuth 클라이언트 ID를 생성하면 앱이 사용자의 구글 계정 정보에 접근하는 API를 이용할 수 있게 되고 OAuth 동의 화면의 설정을 통해 API 호출 시 사용자에게 계정 정보에 접근하는 것을 허락할지를 확인하는 화면을 출력하게 된다.

 

사용자 인증 정보 메뉴에 집입해서 사용자 인증 정보 만들기를 선택한 후 OAuth 클라이언트 ID 항목을 선택한다.

 

OAuth 클라이언트 ID 만들기 화면에 나타난다.

애플리케이션 유형을 Android로 선택하고 앱의 이름과 패키지 이름을 입력한다. 패키지 이름은 AndroidManifest.xml 파일이나 app 수준의 build.gredle 파일의 appID 값을 입력한다.

문제는 서명 인증서 지문이다. 서명 인증서란 keystore라고도 하며 앱의 개발자 누구인지를 증명하기 위한 인증서다. 이 인증서 내부에는 SHA-1 방식의 지문(암호)이 포함되는데 이 지문을 등록해야 한다.

keystore는 다시 릴리즈용과 디버그용이 존재한다. 현재는 테스트가 목적이므로 디버그용 keystore를 이용한다.

디버그용 keystore는 윈도우 기준 C:\Users\사용자\.android 폴더에 존재한다.

 

커맨드 창을 열어 .android 폴더로 이동한 후 다음과 같이 입력한다.

keytool -keystore debug.keystore -list -v

 

패스워드를 입력하라고 하는데 그냥 엔터를 누르면 다음과 같이 인증서의 내용이 출력된다. 그중 SHA-1에 해당하는 내용을 복사하여 콘솔에 입력한다.

 

생성 버튼을 눌러 클라이언트 ID를 생성한다.

 

확인 버튼을 누르면 OAuth 2.0 클라이언트 ID 항목에 Android 유형의 Flutter Firebase의 ID가 새롭게 생성된 것을 확인할 수 있다.

 

다음으로 OAuth 동의 화면 메뉴로 진입한다. 애플리케이션의 이름과 로고파일 그리고 지원 이메일을 입력한다.

 

승인된 도메인을 확인하고 동일한 값으로 홈페이지와 개인정보처리방침, 서비스 약관 항목에 입력한다. 향후 실제 서비스를 릴리스할 때에는 정식 사이트의 링크를 입력해야 하지만 현재는 테스트 목적이므로 동일한 도메인을 입력하고 저장버튼을 선택한다.

 

구글 API의 설정이 완료되었다. 

 

2. Firebase 콘솔 설정

지난 강좌에서 Firebase에 앱을 연동하면서 SHA-1 인증 지문의 입력을 생략했었다.

Firebase 콘솔에 접속하여 Flutter Firebase 프로젝트의 설정 메뉴로 진입한다. 메뉴 사단에 Android 앱 항목에서 지난 강좌에서 추가한 Flutter Firebase 앱의 설정 페이지에서 SHA-1 인증 지문을 추가한다. 그리고 최신 구성 파일 다운로드 항목에서 google-services.json 파일을 다시 다운로드하여 기존 파일(Android/app)을 대체한다. 실제 json 파일의 내용을 보면 인증 지문이 추가된 것을 확인할 수 있다.

 

3. 코드 구현

구글 인증을 이용하기 위해서는 다음과 같이 pubspec.yaml 파일에 플러그인을 추가한다.

dependencies:
  google_sign_in: ^4.0.11
  http: ^0.12.0+2

 

소스코드

import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(MaterialApp(
    title: "Flutter Firebase",
    home: HomePage(),
  ));
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Flutter Firebase")),
      body: ListView(
        children: <Widget>[
          ListTile(
            title: Text("Google Sign-In Demo"),
            subtitle: Text("google_sign_in Plugin"),
            onTap: () {
              Navigator.push(context,
                  MaterialPageRoute(builder: (context) => GoogleSignInDemo()));
            },
          )
        ].map((child) {
          return Card(
            child: child,
          );
        }).toList(),
      ),
    );
  }
}

GoogleSignInDemoState pageState;

GoogleSignIn _googleSignIn = GoogleSignIn(scopes: <String>[
  'email',
  'https://www.googleapis.com/auth/contacts.readonly',
]);

class GoogleSignInDemo extends StatefulWidget {
  @override
  GoogleSignInDemoState createState() {
    pageState = GoogleSignInDemoState();
    return pageState;
  }
}

class GoogleSignInDemoState extends State<GoogleSignInDemo> {
  GoogleSignInAccount _currentUser;
  String _contactText;

  @override
  void initState() {
    super.initState();
    _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount account) {
      setState(() {
        _currentUser = account;
      });
      if (_currentUser != null) {
        _handleGetContact();
      }
      _googleSignIn.signInSilently();
    });
  }

  Future<void> _handleGetContact() async {
    setState(() {
      _contactText = "Loading contact info...";
    });
    final http.Response response = await http.get(
        'https://people.googleapis.com/v1/people/me/connections'
        '?requestMask.includeField=person.names',
        headers: await _currentUser.authHeaders);

    if (response.statusCode != 200) {
      setState(() {
        _contactText = "People API gave a ${response.statusCode} "
            "response. Check logs for detailes.";
      });
      print("People API ${response.statusCode} response: ${response.body}");
      return;
    }

    final Map<String, dynamic> data = json.decode(response.body);
    final String namedContact = _pickFirstNamedContact(data);
    setState(() {
      if (namedContact != null) {
        _contactText = "I see you know $namedContact";
      } else {
        _contactText = "No contacts to display.";
      }
    });
  }

  String _pickFirstNamedContact(Map<String, dynamic> data) {
    final List<dynamic> connections = data['connections'];
    final Map<String, dynamic> contact = connections?.firstWhere(
      (dynamic contact) => contact['names'] != null,
      orElse: () => null,
    );
    if (contact != null) {
      final Map<String, dynamic> name = contact['names'].firstWhere(
        (dynamic name) => name['displayName'] != null,
        orElse: () => null,
      );
      if (name != null) {
        return name['displayName'];
      }
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text("Google Sign-In Demo")),
        body: ConstrainedBox(
          constraints: const BoxConstraints.expand(),
          child: _buildBody(),
        ));
  }

  Widget _buildBody() {
    if (_currentUser != null) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          ListTile(
            leading: GoogleUserCircleAvatar(
              identity: _currentUser,
            ),
            title: Text(_currentUser.displayName ?? ""),
            subtitle: Text(_currentUser.email ?? ""),
          ),
          const Text("Signed in successfully."),
          Text(_contactText ?? ""),
          RaisedButton(
            child: const Text("SIGN OUT"),
            onPressed: _handleSignOut,
          ),
          RaisedButton(
            child: const Text("REFRESH"),
            onPressed: _handleGetContact,
          )
        ],
      );
    } else {
      return Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          const Text("You are not currently signed in"),
          RaisedButton(
            child: const Text("SIGN IN"),
            onPressed: _handleSignIn,
          )
        ],
      );
    }
  }

  Future<void> _handleSignIn() async {
    try {
      await _googleSignIn.signIn();
    } catch (error) {
      print(error);
    }
  }

  Future<void> _handleSignOut() async {
    _googleSignIn.disconnect();
  }
}

 

앱을 실행해보자. 에뮬레이터의 경우 구글 서비스가 설치되어 있지 않기 때문에 실제폰을 이용해야 한다.

 

본인의 경우 SIGN IN 버튼을 누르면 앱이 비정상 종료가 된다. 에러의 내용 중 일부는 다음과 같다.

I/lutter_firebas(23582): Rejecting re-init on previously-failed class java.lang.Class<com.google.android.gms.auth.api.signin.internal.SignInHubActivity>: java.lang.NoClassDefFoundError: Failed resolution of: Landroid/support/v4/app/FragmentActivity;
I/lutter_firebas(23582):   at android.content.Intent com.google.android.gms.auth.api.signin.internal.zzh.zzc(android.content.Context, com.google.android.gms.auth.api.signin.GoogleSignInOptions) ((null):1)
I/lutter_firebas(23582):   at android.content.Intent com.google.android.gms.auth.api.signin.GoogleSignInClient.getSignInIntent() ((null):20)
I/lutter_firebas(23582):   at void io.flutter.plugins.googlesignin.GoogleSignInPlugin$Delegate.signIn(io.flutter.plugin.common.MethodChannel$Result) (GoogleSignInPlugin.java:291)
I/lutter_firebas(23582):   at void io.flutter.plugins.googlesignin.GoogleSignInPlugin.onMethodCall(io.flutter.plugin.common.MethodCall, io.flutter.plugin.common.MethodChannel$Result) (GoogleSignInPlugin.java:77)
I/lutter_firebas(23582):   at void io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(java.nio.ByteBuffer, io.flutter.plugin.common.BinaryMessenger$BinaryReply) (MethodChannel.java:222)
I/lutter_firebas(23582):   at void io.flutter.embedding.engine.dart.DartMessenger.handleMessageFromDart(java.lang.String, byte[], int) (DartMessenger.java:96)
I/lutter_firebas(23582):   at void io.flutter.embedding.engine.FlutterJNI.handlePlatformMessage(java.lang.String, byte[], int) (FlutterJNI.java:656)
I/lutter_firebas(23582):   at void android.os.MessageQueue.nativePollOnce(long, int) (MessageQueue.java:-2)
I/lutter_firebas(23582):   at android.os.Message android.os.MessageQueue.next() (MessageQueue.java:326)
I/lutter_firebas(23582):   at void android.os.Looper.loop() (Looper.java:170)
I/lutter_firebas(23582):   at void android.app.ActivityThread.main(java.lang.String[]) (ActivityThread.java:6983)
I/lutter_firebas(23582):   at java.lang.Object java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object[]) (Method.java:-2)
I/lutter_firebas(23582):   at void com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run() (RuntimeInit.java:493)
I/lutter_firebas(23582):   at void com.android.internal.os.ZygoteInit.main(java.lang.String[]) (ZygoteInit.java:884)
I/lutter_firebas(23582): Caused by: java.lang.ClassNotFoundException: Didn't find class "android.support.v4.app.FragmentActivity" on path: DexPathList[[zip file "/data/app/com.h4u.flutter_firebase-x6j_AyQDO52Up0qg5_XNtQ==/base.apk"],nativeLibraryDirectories=[/data/app/com.h4u.flutter_firebase-x6j_AyQDO52Up0qg5_XNtQ==/lib/arm64, /data/app/com.h4u.flutter_firebase-x6j_AyQDO52Up0qg5_XNtQ==/base.apk!/lib/arm64-v8a, /system/lib64, /system/product/lib64]]
I/lutter_firebas(23582):   at java.lang.Class dalvik.system.BaseDexClassLoader.findClass(java.lang.String) (BaseDexClassLoader.java:134)
I/lutter_firebas(23582):   at java.lang.Class java.lang.ClassLoader.loadClass(java.lang.String, boolean) (ClassLoader.java:379)
I/lutter_firebas(23582):   at java.lang.Class java.lang.ClassLoader.loadClass(java.lang.String) (ClassLoader.java:312)
I/lutter_firebas(23582):   at android.content.Intent com.google.android.gms.auth.api.signin.internal.zzh.zzc(android.content.Context, com.google.android.gms.auth.api.signin.GoogleSignInOptions) ((null):1)
I/lutter_firebas(23582):   at android.content.Intent com.google.android.gms.auth.api.signin.GoogleSignInClient.getSignInIntent() ((null):20)
I/lutter_firebas(23582):   at void io.flutter.plugins.googlesignin.GoogleSignInPlugin$Delegate.signIn(io.flutter.plugin.common.MethodChannel$Result) (GoogleSignInPlugin.java:291)
I/lutter_firebas(23582):   at void io.flutter.plugins.googlesignin.GoogleSignInPlugin.onMethodCall(io.flutter.plugin.common.MethodCall, io.flutter.plugin.common.MethodChannel$Result) (GoogleSignInPlugin.java:77)
I/lutter_firebas(23582):   at void io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(java.nio.ByteBuffer, io.flutter.plugin.common.BinaryMessenger$BinaryReply) (MethodChannel.java:222)
I/lutter_firebas(23582):   at void io.flutter.embedding.engine.dart.DartMessenger.handleMessageFromDart(java.lang.String, byte[], int) (DartMessenger.java:96)
I/lutter_firebas(23582):   at void io.flutter.embedding.engine.FlutterJNI.handlePlatformMessage(java.lang.String, byte[], int) (FlutterJNI.java:656)
I/lutter_firebas(23582):   at void android.os.MessageQueue.nativePollOnce(long, int) (MessageQueue.java:-2)
I/lutter_firebas(23582):   at android.os.Message android.os.MessageQueue.next() (MessageQueue.java:326)
I/lutter_firebas(23582):   at void android.os.Looper.loop() (Looper.java:170)
I/lutter_firebas(23582):   at void android.app.ActivityThread.main(java.lang.String[]) (ActivityThread.java:6983)
I/lutter_firebas(23582):   at java.lang.Object java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object[]) (Method.java:-2)
I/lutter_firebas(23582):   at void com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run() (RuntimeInit.java:493)
I/lutter_firebas(23582):   at void com.android.internal.os.ZygoteInit.main(java.lang.String[]) (ZygoteInit.java:884)

대충 android.support.v4.app.FragmentActivty 관련된 클래스를 찾지 못한다는 뜻인데 이런류의 에러는 androidX 설정을 통해 해결할 수 있다.

프로젝트의 android 폴더의 gradle.properties 파일을 다음과 같이 수정한다.

org.gradle.jvmargs=-Xmx1536M
# For AndroidX
android.enableJetifier=true
android.useAndroidX=true

 

그리고 터미널에서 프로젝트를 clean 한다.

D:\workspace\Flutter\mystudy\flutter_firebase>flutter clean
Deleting 'build\'.
Deleting 'D:\workspace\Flutter\mystudy\flutter_firebase\.dart_tool\'.
Deleting 'D:\workspace\Flutter\mystudy\flutter_firebase\.android\'.
Deleting 'D:\workspace\Flutter\mystudy\flutter_firebase\.ios\'.

 

폰에서도 설치했던 앱을 삭제한 후 다시 앱을 실행한다.

 

SIGN IN 버튼을 클릭하면 OAuth 동의 화면이 나타나고 접근을 허용하면 구글 계정을 획득하는 것을 확인할 수 있다. 마지막 실행화면은 다음과 같다. 이름과 함께 메일 주소(모자이크 처리)가 출력되는 것을 확인할 수 있다.

 

그런데 화면을 다시 보면 Signed In에는 성공했지만 People API로부터 403 에러코드를 응답 받은 것이 확인된다. 실행창은 보면 Google People API가 활성화되지 않았기 때문에 발생한 것이다.

구글 API 콘솔에 다시 접속하여 라이브러리 메뉴로 진입해서 Google People API를 검색하여 진입한 후 사용 설정을 한다.

 

사용 설정을 하면 다음과 같이 People API가 활성화된다.

 

앱을 다시 실행해서 로그인을 시도하면 본인의 주소록 중 한명의 정보가 출력되는 것을 확인할 수 있다.

 

다만 실행창을 보면 Exception: Could not instantiate image codec. 이라는 exception이 출력되고 있는데 아직 원인과 해결책을 찾지 못했다.

 

이번 강좌에서는 google_sign_in 플러그인을 이용해서 앱에서 구글 계정의 정보에 접근하는 방법에 대해서 알아보았다. 다음 시간에는 접근한 구글 계정 정보를 이용해서 Firebase에 인증하는 방법에 대해서 알아볼 예정이다.

 

Comments