꿈꾸는 시스템 디자이너

Flutter 강좌 - [Firebase] 이메일을 이용한 Firebase 인증(Authentication) 방법 본문

Development/Flutter

Flutter 강좌 - [Firebase] 이메일을 이용한 Firebase 인증(Authentication) 방법

독행소년 2019. 11. 11. 14:15

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에서 제공하는 인증(Authentication) 기능 중 이메일과 비밀번호를 이용하여 Firebase에 로그인하는 방법에 대해서 알아본다.

이번 강좌도 지난 강좌에서 구현한 Flutter Firebase 앱을 기반으로 진행된다.

 

1. Firebase 환경 설정

우선 Firebase 콘솔로 접속한 후 Flutter Firebase 프로젝트를 선택하고 개발 메뉴 중 인증(Authentication) 메뉴로 진입한다.

지난 강좌에서 Firebase의 인증 기능을 이용하면 회원가입 및 로그인 기능을 직접 구현하지 않고 Firebase의 인증으로 대행할 수 있다고 설명했었다. 사용자 항목에서는 현재 Flutter Firebase 서비스(프로젝트)에 가입된 사용자의 정보를 확인할 수 있다.

 

로그인 방법 항목에서는 Firebase 인증에서 제공하는 로그인 방법을 설정할 수 있다. 여러 방법이 있는데 이번 강좌에서는 첫 번째 항목인 이메일/비밀번호 방식을 이용한다.

 

이메일/비밀번호 항목을 선택하여 사용 설정을 선택한 후 저장버튼을 클릭한다.

 

템플릿 항목을 확인해보자.

서비스에 가입한 사용자에게 제공할 수 있는 메일의 템플릿을 확인할 수 있다.

이메일 주소 인증은 회원 가입한 회원의 메일 주소로 인증 메일을 전송할 때 사용할 메일의 템플릿이다. 

이 외에도 비밀번호 재설정 및 이메일 주소 변경 등의 템플릿이 제공된다.

화면 하단 메뉴를 통해 템플릿의 언어도 설정할 수 있다.

 

2. 서비스 설계

이번 강좌에서 구현할 서비스의 구성은 다음과 같다.

  • firebase_provider.dart: Firebaes 인증 기능을 제공하는 Provider 구현
  • auth_page.dart: 인증 서비스의 첫 번째 페이지
  • signin_page.dart: 로그인(Sign In) 처리 페이지
  • signup_page.dart: 회원 가입(Sign Up) 처리 페이지
  • signedin_page.dart: 로그인 완료(Signed In) 페이지

 

3. 플러그인 설정

pubspec.yaml 파일에 다음의 플러그인을 추가한 후 packages get 명령어로 플러그인을 설치한다.

dependencies:
  firebase_auth: ^0.14.0+5
  provider: ^3.1.0+1
  shared_preferences: ^0.5.3+4
  logger: ^0.7.0+2
  • firebase_auth: Firebase의 인증 기능을 제공하는 플로그인
  • provider: 여러 페이지에서 인증 정보를 공유하기 위해 사용할 Provider 기능을 제공하는 플러그인
  • shared_preferences: 사용자의 메일주소와 비밀번호를 저장하기 위해 사용할 플러그인
  • logger: 로그을 출력하기 위한 플러그인(옵션)

 

4. Source Code 구현

  • main.dart

기존의 Flutter Firebase 앱의 main.dart 파일을 다음과 같이 수정한다.

import 'package:flutter/material.dart';
import 'google_signin/google_signin_demo.dart';
import 'package:provider/provider.dart';
import 'firebase_provider.dart';
import 'mail/auth_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<FirebaseProvider>(
            builder: (_) => FirebaseProvider())
      ],
      child: 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()));
            },
          ),
          ListTile(
            title: Text("Firebase Auth"),
            onTap: () {
              Navigator.push(
                  context, MaterialPageRoute(builder: (context) => AuthPage()));
            },
          )
        ].map((child) {
          return Card(
            child: child,
          );
        }).toList(),
      ),
    );
  }
}

HomePage 클래스부터 살펴보면 두 개의 ListTile을 가지는 ListView를 구성하고 있다.

첫 번째 ListTile을 탭 하면 이전 강좌에서 개발한 구글 로그인 데모 페이지가 생성된다. 두 번째 ListTile을 탭 하면 이번 강좌에서 구현할 이메일 기반의 Firebase 인증 페이지가 생성된다.

MyApp 클래스는 HomePage를 실행하기 위한 MaterialApp을 생성하는 기능과 함께 Firebase 로그인 인증을 처리하기 위한 Provider인 FirebaseProvider 클래스를 providers 항목에 주입하고 있다. Flutter의 Provider 기능에 대한 이해가 필요한 경우 아래의 Provider 사용법 강좌를 먼저 확인한다.

2019/10/31 - [Development/Flutter] - Flutter 강좌 - Provider 사용법 | How to use Flutter Provider

 

  • firebase_provider.dart

Firebase 인증 기능을 제공하는 Provider의 소스 구현은 다음과 같다.

import 'package:flutter/cupertino.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:logger/logger.dart';

Logger logger = Logger();

class FirebaseProvider with ChangeNotifier {
  final FirebaseAuth fAuth = FirebaseAuth.instance; // Firebase 인증 플러그인의 인스턴스
  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로부터 로그아웃
  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;
  }
}

Provider는 main.dart 파일의 MyApp 클래스의 providers 속성으로 주입되면서 생성되는데 생성과 함께 최근 Firebase에 로그인했던 사용자 정보를 획득하여 _user 필드에 저장한다.

최근 사용자 정보가 존재할 경우 로그인 페이지(signin_page)를 생략하고 로그인된 페이지(signedin_page)로 할 수 있다. 반대로 _user 필드 값이 null일 경우 로그인 페이지(signin_page)로 진입하도록 할 것이다. 이 내용은 뒤에서 다시 설명한다.

 

  • auth_page.dart

AuthPage는 HomePage에서 호출하는 Firebase 인증 서비스의 첫 번째 클래스로 그 구현은 다음과 같다.

import 'package:flutter/material.dart';
import 'package:flutter_firebase/firebase_provider.dart';
import 'package:flutter_firebase/mail/signedin_page.dart';
import 'package:flutter_firebase/mail/signin_page.dart';
import 'package:provider/provider.dart';

AuthPageState pageState;

class AuthPage extends StatefulWidget {
  @override
  AuthPageState createState() {
    pageState = AuthPageState();
    return pageState;
  }
}

class AuthPageState extends State<AuthPage> {
  FirebaseProvider fp;

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

    logger.d("user: ${fp.getUser()}");
    if (fp.getUser() != null && fp.getUser().isEmailVerified == true) {
      return SignedInPage();
    } else {
      return SignInPage();
    }
  }
}

build 메서드의 구현 내용을 보면 FirebaseProvider의 인스턴스(fp)를 이용해서 Firebase에 로그인한 최근 사용자의 정보 유무에 따라 SignedInPage와 SignInPage를 호출하고 있다.

최근 사용자 정보가 null이 아니더라도 그 사용자가 메일 인증을 완료하지 않았다면 SignInPage로 진입하게 된다.

여기서 알고 넘어가야 할 점은 fp.getUser()를 통해 참조하는 FirebaseProvider의 _user 필드의 값에 변화가 발생하게 되면 이 변화가 실시간으로 감지되어 위의 build 메서드가 재호출 되고 화면에 출력되는 페이지도 자동으로 변하게된다. 이는 Navigator를 이용해서 화면 이동 로직을 직접 구현하지 않아도 상태 변화에 따라 화면 전환이 자동으로 발생한다는 점이다. Provider의 강점이라 할 수 있겠다.

 

  • signin_page.dart

실행화면은 다음과 같다.

이메일과 비밀번호를 입력할 수 있는 텍스트 필드와 그 값을 저장할 수 있는 체크박스, 그리고 로그인을 시도하는 버튼으로 구성되며, 추가로 회원가입 페이지로 이동하기 위한 Sign Up 버튼도 존재한다.

 

로그인 화면을 제공하는 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();
              },
            ),
          ),
          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();
  }

  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: () {},
        ),
      ));
  }
}

 

SIGN-IN 버튼을 클릭하면 다음과 같이 _signIn 메서드가 호출된다.

  void _signIn() async {
	...
    bool result = await fp.signInWithEmail(_mailCon.text, _pwCon.text);
    ...
    if (result == false) showLastFBMessage();
  }

메서드 내용을 살펴보면 텍스트 필드에 입력된 메일 주소와 패스워드를 파라미터로 FirebaseProvider의 signInWithEmail 메서드를 호출하고 그 결과를 기다린다. 실패일 경우 showLastFBMessage 메서드가 호출되고 성공일 경우 별다른 조치 없이 메서드는 종료된다.

FirebaseProvider의 signInWithEmail 메서드의 구현은 다음과 같다.

    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;
    }
  }

FirebaseAuth의 인스턴스를 통해 로그인을 시도하고 성공할 경우 필드 _user에 로그인된 사용자의 정보를 주입하고 true를 반환한다. 만약 로그인 도중 Exception이 발생하면 에러 메시지를 파싱 하여 setLastFBMessage 메서드를 통해 필드 _lastFirebaseResponse에 저장하고 false를 반환한다.

즉 로그인에 성공하면 필드 _user가 최신 사용자의 정보로 갱신되고 실패하면 exception 메시지가 SnackBar로 출력된다. 필드 _user가 갱신되면 이를 참조하고 있던 AuthPageState의 build 메서드가 재호출 되어 SignedInPage로 진입하게 되므로 Navigator를 이용해서 현재 페이지를 popup 하고 다시 SignedInPage를 push 하는 로직을 구현하지 않아도 된다.

현재까지 구현된 내용으로 로그인을 시도하면 다음과 같은 Exception들이 발생하고 SnackBar가 출력되는 것을 확인할 수 있다.

 

 

  • signup_page.dart

회원가입 화면은 다음과 같다. 이메일 주소와 패스워드를 입력한 후 SIGN UP 버튼을 클릭하는 방식이다.

 

소스 코드의 구현은 다음과 같다. SignInPage의 구성과 유사하다.

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

SignUpPageState pageState;

class SignUpPage extends StatefulWidget {
  @override
  SignUpPageState createState() {
    pageState = SignUpPageState();
    return pageState;
  }
}

class SignUpPageState extends State<SignUpPage> {
  TextEditingController _mailCon = TextEditingController();
  TextEditingController _pwCon = TextEditingController();

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

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

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

    return Scaffold(
        key: _scaffoldKey,
        appBar: AppBar(title: Text("Sign-Up 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(
                        "Create 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(),
                    ),
                  )
                ],
              ),
            ),

            // Sign Up Button
            Container(
              margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
              child: RaisedButton(
                color: Colors.indigo[300],
                child: Text(
                  "SIGN UP",
                  style: TextStyle(color: Colors.white),
                ),
                onPressed: () {
                  FocusScope.of(context)
                      .requestFocus(new FocusNode()); // 키보드 감춤
                  _signUp();
                },
              ),
            ),
          ],
        ));
  }

  void _signUp() async {
    _scaffoldKey.currentState
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(
        duration: Duration(seconds: 10),
        content: Row(
          children: <Widget>[
            CircularProgressIndicator(),
            Text("   Signing-Up...")
          ],
        ),
      ));
    bool result = await fp.signUpWithEmail(_mailCon.text, _pwCon.text);
    _scaffoldKey.currentState.hideCurrentSnackBar();
    if (result) {
      Navigator.pop(context);
    } else {
      showLastFBMessage();
    }
  }

  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: () {},
        ),
      ));
  }
}

SIGN UP 버튼을 클릭하면 _signUp 메서드가 호출되고 메서드 내에서 FirebaseProvider의 signUpWithEmail을 호출하고 가입에 성공하면 현재 페이지를 pop 하고 실패할 경우 showLastFBMessage 메서드를 호출하여 발생한 Exception 메시지를 SnackBar로 출력한다.

여기서 Navigator를 통해 페이지를 직접 pop 하는 이유는 SignUpPage는 Provider에 의해 생성된 페이지가 아니라 SignInPage에서 Navigator.push 코드에 의해 생성된 페이지이기 때문이다.

 

호출되는 signUpWithEmail 메서드의 구현과 함께 확인해보자.

  // 이메일/비밀번호로 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;
    }
  }

 

회원가입에 성공할 경우 최근 사용자를 로그아웃한 후에 true를 반환한다.

일반적으로 회원가입에 성공하면 가입과 함께 자동으로 로그인 처리를 해서 로그인된 화면으로 이동시킨다. 위 코드에서도 필드 _user에 result.user 값을 주입한 후 로그인된 페이지로 이동시킬 수도 있다. 하지만 회원가입에 사용된 메일 주소의 유효성을 입증할 수 없으므로 인증메일을 발송한 후 사용자가 인증메일을 확인할 때까지 로그인을 허용하지 않아야 한다.

 

현재까지의 회원가입 페이지의 구현 사항은 다음과 같다.

 

 

Firebase의 콘솔에 접속하면 다음과 같이 사용자가 추가된 것을 확인할 수 있다.

 

자 이제 가입한 메일 주소와 비밀번호로 로그인을 시도해보자.

로그인을 시도하면 메일 인증을 하지 않은 사용자라는 경고가 표시되면, 인증메일의 링크를 통해 메일 인증을 완료하면 로그인된 페이지로 진입하게 된다.

 

  • signedin_page.dart

로그인된 페이지의 구현은 다음과 같다. 사용자의 정보를 ListView로 출력한다.

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

SignedInPageState pageState;

class SignedInPage extends StatefulWidget {
  @override
  SignedInPageState createState() {
    pageState = SignedInPageState();
    return pageState;
  }
}

class SignedInPageState extends State<SignedInPage> {
  FirebaseProvider fp;
  TextStyle tsItem = const TextStyle(
      color: Colors.blueGrey, fontSize: 13, fontWeight: FontWeight.bold);
  TextStyle tsContent = const TextStyle(color: Colors.blueGrey, fontSize: 12);

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

    double propertyWith = 130;
    return Scaffold(
      appBar: AppBar(title: Text("Singed In Page")),
      body: ListView(
        children: <Widget>[
          Container(
            margin: const EdgeInsets.only(left: 20, right: 20, top: 10),
            child: Column(
              children: <Widget>[
                //Hader
                Container(
                  height: 50,
                  decoration: BoxDecoration(color: Colors.amber),
                  child: Center(
                    child: Text(
                      "Signed In User Info",
                      style: TextStyle(
                          color: Colors.blueGrey,
                          fontSize: 18,
                          fontWeight: FontWeight.bold),
                    ),
                  ),
                ),

                // User's Info Area
                Container(
                  decoration: BoxDecoration(
                    border: Border.all(color: Colors.amber, width: 1),
                  ),
                  child: Column(
                    children: <Widget>[
                      Row(
                        children: <Widget>[
                          Container(
                            width: propertyWith,
                            child: Text("UID", style: tsItem),
                          ),
                          Expanded(
                            child: Text(fp.getUser().uid, style: tsContent),
                          )
                        ],
                      ),
                      Divider(height: 1),
                      Row(
                        children: <Widget>[
                          Container(
                            width: propertyWith,
                            child: Text("Email", style: tsItem),
                          ),
                          Expanded(
                            child: Text(fp.getUser().email, style: tsContent),
                          )
                        ],
                      ),
                      Divider(height: 1),
                      Row(
                        children: <Widget>[
                          Container(
                            width: propertyWith,
                            child: Text("Name", style: tsItem),
                          ),
                          Expanded(
                            child: Text(fp.getUser().displayName ?? "",
                                style: tsContent),
                          )
                        ],
                      ),
                      Divider(height: 1),
                      Row(
                        children: <Widget>[
                          Container(
                            width: propertyWith,
                            child: Text("Phone Number", style: tsItem),
                          ),
                          Expanded(
                            child: Text(fp.getUser().phoneNumber ?? "",
                                style: tsContent),
                          )
                        ],
                      ),
                      Divider(height: 1),
                      Row(
                        children: <Widget>[
                          Container(
                            width: propertyWith,
                            child: Text("isEmailVerified", style: tsItem),
                          ),
                          Expanded(
                            child: Text(fp.getUser().isEmailVerified.toString(),
                                style: tsContent),
                          )
                        ],
                      ),
                      Divider(height: 1),
                      Row(
                        children: <Widget>[
                          Container(
                            width: propertyWith,
                            child: Text("Provider ID", style: tsItem),
                          ),
                          Expanded(
                            child:
                                Text(fp.getUser().providerId, style: tsContent),
                          )
                        ],
                      ),
                    ].map((c) {
                      return Padding(
                        padding: const EdgeInsets.symmetric(
                            vertical: 10, horizontal: 10),
                        child: c,
                      );
                    }).toList(),
                  ),
                )
              ],
            ),
          ),

          // Sign In Button
          Container(
            margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
            child: RaisedButton(
              color: Colors.indigo[300],
              child: Text(
                "SIGN OUT",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                fp.signOut();
              },
            ),
          ),

          // Send Password Reset Email by English
          Container(
            margin: const EdgeInsets.only(left: 20, right: 20, top: 10),
            child: RaisedButton(
              color: Colors.orange[300],
              child: Text(
                "Send Password Reset Email by English",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                fp.sendPasswordResetEmailByEnglish();
              },
            ),
          ),

          // Send Password Reset Email by Korean
          Container(
            margin: const EdgeInsets.only(left: 20, right: 20, top: 0),
            child: RaisedButton(
              color: Colors.orange[300],
              child: Text(
                "Send Password Reset Email by Korean",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                fp.sendPasswordResetEmailByKorean();
              },
            ),
          ),

          // Send Password Reset Email by Korean
          Container(
            margin: const EdgeInsets.only(left: 20, right: 20, top: 10),
            child: RaisedButton(
              color: Colors.red[300],
              child: Text(
                "Withdrawal (Delete Account)",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                fp.withdrawalAccount();
              },
            ),
          ),
        ],
      ),
    );
  }
}

 

실행화면은 다음과 같다.

사용자의 정보로는 UID, Email, Name, PhoneNumber 등이 출력된다. 회원 가입 시 메일 주소와 패스워드만 입력했으므로 획득할 수 있는 어보는 UID와 Email 정도다. 인증메일을 통해 메일 주소를 인증하였으므로 isEmailVerified 값으로 true가 반환되고 있다.

추가로 제공되는 기능은 로그아웃(Sign Out) 기능과 비밀번호 재설정 메일 전송 기능, 그리고 회원 탈퇴 기능이다.

비밀번호 재설정 메일 전송을 위한 FirebaseProvider의 코드는 다음과 같다.

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

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

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

Firebase 콘솔에서도 확인했던 것처럼 Firebase에서는 다양한 언어의 템플릿을 제공한다. 만약 글로벌 서비스를 제공한다면 가입 회원의 언어에 맞게 템플릿 언어도 설정해줘야 하는데 이때 setLanguageCode 메서드를 이용하면 된다.

 

FirebaseProvider의 로그아웃 처리 코드는 다음과 같다.

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

signOut 메서드 호출과 함께 setUser 메서드로 필드 _user 값을 null로 설정하여 SignInPage로 자동 이동되게 된다.

 

FirebaseProvider의 회원 탈퇴 코드는 다음과 같다.

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

역시 회원 탈퇴 후 setUser 메서드를 통해 _user 값을 null로 설정하여 SignInPage로 자동 이동되게 된다.

 

이번 강좌에서는 Firebase의 인증 기능 중 이메일/비밀번호를 이용하는 인증 서비스를 구현했다. 다음 강좌에서는 이메일/비밀번호 인증과 구글 인증을 통합한 인증 서비스에 대해서 알아본다.

Comments