꿈꾸는 시스템 디자이너

Flutter 강좌 - [Firebase] 구글 계정으로 Fireabse 로그인(Sign In) 본문

Development/Flutter

Flutter 강좌 - [Firebase] 구글 계정으로 Fireabse 로그인(Sign In)

독행소년 2019. 11. 11. 16:29

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

 

이번 강좌에서는 구글 계정을 이용해서 Firebase에 로그인하는 방법에 대해서 알아본다. 이번 강좌도 Flutter Fireabse 앱과 프로젝트를 이용해서 진행되므로 기존 강좌에 대한 이해가 필요하다.

 

직전 강좌에서 메일 주소와 비밀번호를 이용해 Firebase 인증에 회원가입 및 로그인하는 방법에 대해서 알아봤다. 이번 강좌에서는 이전 강좌에서 구현한 내용에 구글 계정을 통해 로그인하는 로직을 추가해보도록 한다.

우선 실행화면부터 확인해보자. 지난 강좌에서 구현한 화면에서 구글 계정으로 로그인을 시도하는 버튼이 추가되었다.

 

기존의 이메일과 비밀번호를 이용하여 Firebase에 로그인하는 메커니즘은 다음과 같다.

  1. 메일 주소와 비밀번호로 회원가입
  2. 회원가입에 성공하면 인증메일 발송
  3. 인증메일 링크를 통해 메일 주소 인증
  4. 메일 주소 인증이 완료된 경우에 한해서만 로그인 성공

회원가입과 로그인 과정 외에도 인증메일을 발송 및 메일 주소 인증과정이 필요했다.

 

이번 강좌에서 구현할 구글 계정을 이용한 Firebase 로그인의 메커니즘은 다음과 같다.

  1. 구글 계정을 통해 로그인 시도
  2. 성공 시 회원가입 및 로그인 완료. 메일 주소 인증 불필요

이미 구글에서 인증이 완료된 계정을 이용하여 Firebase에 로그인을 시도하는 것이기 때문에 회원가입 절차 없이 로그인 단계에서 회원가입까지 완료된다. 유효한 메일 주소를 이용한 것이기 때문에 메일 주소를 인증할 필요도 없다. 기존의 이메일/비밀번호를 이용하는 방법과 비교하면 구현과 사용 모두 훨씬 간단하다.

 

1. Firebase 콘솔 설정

Firebase 콘솔에 접속하여 Flutter Firebase 프로젝트를 선택한 후 인증 메뉴의 로그인 방법 메뉴로 이동한다. 로그인 제공업체 항목 중 Google 항목을 사용으로 설정한 후 저장버튼을 누른다.

 

2. 플러그인 설정

이미 지난 강좌에서 관련 플러그인들이 추가되어 별도로 추가할 플러그인은 없다.

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2
  google_sign_in: ^4.0.11
  http: ^0.12.0+2
  firebase_auth: ^0.14.0+5
  provider: ^3.1.0+1
  shared_preferences: ^0.5.3+4
  logger: ^0.7.0+2

 

3. 소스코드 구현

이번 강좌에서는 기존 FlutterFirebase 프로젝트를 이용한다.

  • firebase_provider.dart 파일 수정
import 'package:flutter/cupertino.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:logger/logger.dart';
import 'package:google_sign_in/google_sign_in.dart';

Logger logger = Logger();

class FirebaseProvider with ChangeNotifier {
  final FirebaseAuth fAuth = FirebaseAuth.instance; // Firebase 인증 플러그인의 인스턴스
  final GoogleSignIn _googleSignIn = GoogleSignIn();

  FirebaseUser _user; // Firebase에 로그인 된 사용자

  String _lastFirebaseResponse = ""; // Firebase로부터 받은 최신 메시지(에러 처리용)

  FirebaseProvider() {
    logger.d("init FirebaseProvider");
    _prepareUser();
  }

  FirebaseUser getUser() {
    return _user;
  }

  void setUser(FirebaseUser value) {
    _user = value;
    notifyListeners();
  }

  // 최근 Firebase에 로그인한 사용자의 정보 획득
  _prepareUser() {
    fAuth.currentUser().then((FirebaseUser currentUser) {
      setUser(currentUser);
    });
  }

  // 이메일/비밀번호로 Firebase에 회원가입
  Future<bool> signUpWithEmail(String email, String password) async {
    try {
      AuthResult result = await fAuth.createUserWithEmailAndPassword(
          email: email, password: password);
      if (result.user != null) {
        // 인증 메일 발송
        result.user.sendEmailVerification();
        // 새로운 계정 생성이 성공하였으므로 기존 계정이 있을 경우 로그아웃 시킴
        signOut();
        return true;
      }
    } on Exception catch (e) {
      logger.e(e.toString());
      List<String> result = e.toString().split(", ");
      setLastFBMessage(result[1]);
      return false;
    }
  }

  // 이메일/비밀번호로 Firebase에 로그인
  Future<bool> signInWithEmail(String email, String password) async {
    try {
      var result = await fAuth.signInWithEmailAndPassword(
          email: email, password: password);
      if (result != null) {
        setUser(result.user);
        logger.d(getUser());
        return true;
      }
      return false;
    } on Exception catch (e) {
      logger.e(e.toString());
      List<String> result = e.toString().split(", ");
      setLastFBMessage(result[1]);
      return false;
    }
  }

  // 구글 계정을 이용하여 Firebase에 로그인
  Future<bool> signInWithGoogleAccount() async {
    try {
      final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
      final GoogleSignInAuthentication googleAuth =
          await googleUser.authentication;
      final AuthCredential credential = GoogleAuthProvider.getCredential(
          accessToken: googleAuth.accessToken, idToken: googleAuth.idToken);
      final FirebaseUser user =
          (await fAuth.signInWithCredential(credential)).user;
      assert(user.email != null);
      assert(user.displayName != null);
      assert(!user.isAnonymous);
      assert(await user.getIdToken() != null);

      final FirebaseUser currentUser = await fAuth.currentUser();
      assert(user.uid == currentUser.uid);
      setUser(user);
      return true;
    } on Exception catch (e) {
      logger.e(e.toString());
      List<String> result = e.toString().split(", ");
      setLastFBMessage(result[1]);
      return false;
    }
  }

  // Firebase로부터 로그아웃
  signOut() async {
    await fAuth.signOut();
    setUser(null);
  }

  // 사용자에게 비밀번호 재설정 메일을 영어로 전송 시도
  sendPasswordResetEmailByEnglish() async {
    await fAuth.setLanguageCode("en");
    sendPasswordResetEmail();
  }

  // 사용자에게 비밀번호 재설정 메일을 한글로 전송 시도
  sendPasswordResetEmailByKorean() async {
    await fAuth.setLanguageCode("ko");
    sendPasswordResetEmail();
  }

  // 사용자에게 비밀번호 재설정 메일을 전송
  sendPasswordResetEmail() async {
    fAuth.sendPasswordResetEmail(email: getUser().email);
  }

  // Firebase로부터 회원 탈퇴
  withdrawalAccount() async {
    await getUser().delete();
    setUser(null);
  }

  // Firebase로부터 수신한 메시지 설정
  setLastFBMessage(String msg) {
    _lastFirebaseResponse = msg;
  }

  // Firebase로부터 수신한 메시지를 반환하고 삭제
  getLastFBMessage() {
    String returnValue = _lastFirebaseResponse;
    _lastFirebaseResponse = null;
    return returnValue;
  }
}

필드에 GoogleSignIn의 인스턴스인 _googleSignIn 필드가 추가되고 signInWithGoogleAccount 메서드 하나만 추가된다.

메서드의 구현 내용을 살펴보면 현재 구글 계정 정보를 획득하여 구글로부터 인증을 진행하여 accessToken과 idToken을 획득한다. 이 토큰들을 이용해서 신임장(AuthCredential)을 구글로부터 발급받고, 이 신임장으로 Firebase에 로그인을 시도한다.

로그인에 성공하면 필드 _user의 값을 최신 사용자 값으로 갱신한 후 true를 반환하고, 실패할 경우 Exception 메시지를 저장한 후 false를 반환하여 SnackBar가 출력되도록 하다.

 

  • signin_page.dart

로그인을 시도하는 SignInPage의 소스코드는 다음과 같다.

import 'package:flutter/material.dart';
import 'package:flutter_firebase/firebase_provider.dart';
import 'package:flutter_firebase/mail/signup_page.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';

SignInPageState pageState;

class SignInPage extends StatefulWidget {
  @override
  SignInPageState createState() {
    pageState = SignInPageState();
    return pageState;
  }
}

class SignInPageState extends State<SignInPage> {
  TextEditingController _mailCon = TextEditingController();
  TextEditingController _pwCon = TextEditingController();
  bool doRemember = false;

  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
  FirebaseProvider fp;

  @override
  void initState() {
    super.initState();
    getRememberInfo();
  }

  @override
  void dispose() {
    setRememberInfo();
    _mailCon.dispose();
    _pwCon.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    fp = Provider.of<FirebaseProvider>(context);

    logger.d(fp.getUser());
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(title: Text("Sign-In Page")),
      body: ListView(
        children: <Widget>[
          Container(
            margin: const EdgeInsets.only(left: 20, right: 20, top: 10),
            child: Column(
              children: <Widget>[
                //Header
                Container(
                  height: 50,
                  decoration: BoxDecoration(color: Colors.amber),
                  child: Center(
                    child: Text(
                      "Sign In to Your Account",
                      style: TextStyle(
                          color: Colors.blueGrey,
                          fontSize: 18,
                          fontWeight: FontWeight.bold),
                    ),
                  ),
                ),

                // Input Area
                Container(
                  decoration: BoxDecoration(
                    border: Border.all(color: Colors.amber, width: 1),
                  ),
                  child: Column(
                    children: <Widget>[
                      TextField(
                        controller: _mailCon,
                        decoration: InputDecoration(
                          prefixIcon: Icon(Icons.mail),
                          hintText: "Email",
                        ),
                      ),
                      TextField(
                        controller: _pwCon,
                        decoration: InputDecoration(
                          prefixIcon: Icon(Icons.lock),
                          hintText: "Password",
                        ),
                        obscureText: true,
                      ),
                    ].map((c) {
                      return Padding(
                        padding: const EdgeInsets.symmetric(
                            vertical: 10, horizontal: 10),
                        child: c,
                      );
                    }).toList(),
                  ),
                )
              ],
            ),
          ),
          // Remember Me
          Container(
            margin: const EdgeInsets.symmetric(horizontal: 20),
            child: Row(
              children: <Widget>[
                Checkbox(
                  value: doRemember,
                  onChanged: (newValue) {
                    setState(() {
                      doRemember = newValue;
                    });
                  },
                ),
                Text("Remember Me")
              ],
            ),
          ),

          // Alert Box
          (fp.getUser() != null && fp.getUser().isEmailVerified == false)
              ? Container(
                  margin:
                      const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
                  decoration: BoxDecoration(color: Colors.red[300]),
                  child: Column(
                    children: <Widget>[
                      Padding(
                        padding: const EdgeInsets.all(10.0),
                        child: Text(
                          "Mail authentication did not complete."
                          "\nPlease check your verification email.",
                          style: TextStyle(color: Colors.white),
                        ),
                      ),
                      RaisedButton(
                        color: Colors.lightBlue[400],
                        textColor: Colors.white,
                        child: Text("Resend Verify Email"),
                        onPressed: () {
                          FocusScope.of(context)
                              .requestFocus(new FocusNode()); // 키보드 감춤
                          fp.getUser().sendEmailVerification();
                        },
                      )
                    ],
                  ),
                )
              : Container(),

          // Sign In Button
          Container(
            margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
            child: RaisedButton(
              color: Colors.indigo[300],
              child: Text(
                "SIGN IN",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                FocusScope.of(context).requestFocus(new FocusNode()); // 키보드 감춤
                _signIn();
              },
            ),
          ),
          // Sign In with Google Account Button
          Container(
            margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
            child: RaisedButton(
              color: Colors.indigo[300],
              child: Text(
                "SIGN IN WITH GOOGLE ACCOUNT",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                FocusScope.of(context).requestFocus(new FocusNode()); // 키보드 감춤
                _signInWithGoogle();
              },
            ),
          ),
          Container(
            margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
            padding: const EdgeInsets.only(top: 50),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text("Need an account?",
                    style: TextStyle(color: Colors.blueGrey)),
                FlatButton(
                  child: Text(
                    "Sign Up",
                    style: TextStyle(color: Colors.blue, fontSize: 16),
                  ),
                  onPressed: () {
                    Navigator.push(context,
                        MaterialPageRoute(builder: (context) => SignUpPage()));
                  },
                )
              ],
            ),
          )
        ],
      ),
    );
  }

  void _signIn() async {
    _scaffoldKey.currentState
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(
        duration: Duration(seconds: 10),
        content: Row(
          children: <Widget>[
            CircularProgressIndicator(),
            Text("   Signing-In...")
          ],
        ),
      ));
    bool result = await fp.signInWithEmail(_mailCon.text, _pwCon.text);
    _scaffoldKey.currentState.hideCurrentSnackBar();
    if (result == false) showLastFBMessage();
  }

  void _signInWithGoogle() async {
    _scaffoldKey.currentState
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(
        duration: Duration(seconds: 10),
        content: Row(
          children: <Widget>[
            CircularProgressIndicator(),
            Text("   Signing-In...")
          ],
        ),
      ));
    bool result = await fp.signInWithGoogleAccount();
    _scaffoldKey.currentState.hideCurrentSnackBar();
    if (result == false) showLastFBMessage();
  }

  getRememberInfo() async {
    logger.d(doRemember);
    SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      doRemember = (prefs.getBool("doRemember") ?? false);
    });
    if (doRemember) {
      setState(() {
        _mailCon.text = (prefs.getString("userEmail") ?? "");
        _pwCon.text = (prefs.getString("userPasswd") ?? "");
      });
    }
  }

  setRememberInfo() async {
    logger.d(doRemember);

    SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs.setBool("doRemember", doRemember);
    if (doRemember) {
      prefs.setString("userEmail", _mailCon.text);
      prefs.setString("userPasswd", _pwCon.text);
    }
  }

  showLastFBMessage() {
    _scaffoldKey.currentState
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(
        backgroundColor: Colors.red[400],
        duration: Duration(seconds: 10),
        content: Text(fp.getLastFBMessage()),
        action: SnackBarAction(
          label: "Done",
          textColor: Colors.white,
          onPressed: () {},
        ),
      ));
  }
}

기존 소스에서 버튼이 하나 추가되었고, 구글 계정으로 로그인을 시도하는 _signInWithGoogle 메서드가 하나 추가되었다.

 

4. 테스트

우선 Firebase 콘솔의 사용자 항목을 확인하자. 지난 강좌에서 등록했던 사용자 정보만에 출력되고 있다.

 

앱에서 "SIGN IN WITH GOOGLE ACCOUNT" 버튼을 클릭하여 로그인을 시도한다. 

 

다음과 같이 로그인된 페이지가 출력되면 성공이다.

기존에 이메일/패스워드로 로그인했을때와의 차이점은 Name 항목에 이름값이 출력된다는 것이다. 이 이름은 구글 계정으로부터 획득된 값이다.

Firebase 콘솔도 확인해본다. 구글 계정으로 가입된 사용자가 추가된 것을 확인할 수 있다.

 

앱으로 돌아가서 비밀번호 재설정 메일을 발송한 후 비밀번호를 재설정(실제로는 처음 설정) 한 후 로그아웃을 하고, 메일 주소와 설정한 비밀번호로 로그인을 시도해보자.

로그인이 가능하다.

구글 계정으로 가입한 경우라도 비밀번호를 설정하면 메일 주소와 비밀번호로도 로그인이 가능해진다.

Firebase 콘솔을 확인하면 메일주소/비밀번호로 제공업체가 변경되어 표시된다.

 

로그아웃 한 다음 구글 계정으로 다시 로그인을 시도한 후 Firebase 콘솔을 확인해보자.

제공업체 항목에 구글계정과 메일 주소/비밀번호가 병행 표기되고 있다. 메일 주소를 primary key처럼 사용하여 복수의 제공업체를 사용할 수 있다는 것을 확인할 수 있다.

 

Comments