35일차에는 Firebase에서 Authentication을 사용해 보았다.
학습한 내용
- firebase_auth
- auth의 uid를 사용해 추가 유저 정보 담기
앱을 만들 때 사용자의 신원 정보를 사용하면 데이터를 클라우드에 안전하게 저장할 수 있다. 또한 사용자의 모든 기기에서 동일한 경험을 제공할 수 있다. 이러한 인증 기능을 Firebase에서 지원한다.
다양한 방식을 지원하는데 크게 아래의 4가지로 분류할 수 있다.
- 비밀번호 인증 (id, password)
- 이메일 링크 인증
- 전화번호 인증
- SNS 인증
이메일과 비밀번호를 사용하여 회원가입을 하는 방식은 아래의 코드를 사용한다.
FirebaseAuth.instance.createUserWithEmailAndPassword(
email: "test@naver.com",
password: "password",
);
이메일과 비밀번호를 사용하여 로그인을 하는 방식은 아래의 코드를 사용한다.
FirebaseAuth.instance.signInWithEmailAndPassword(
email: "test@naver.com",
password: "password",
);
로그인된 사용자의 정보는 User
저장되는데 아래의 정보들을 가지고 있다.
FirebaseAuth.instance.currentUser //현재 유저
FirebaseAuth.instance.currentUser!.uId //유저 고유 번호
FirebaseAuth.instance.currentUser!.email //이메일
FirebaseAuth.instance.currentUser!.emailVerified //이메일 인증 여부
FirebaseAuth.instance.currentUser!.photoURL //프로필 사진
FirebaseAuth.instance.currentUser!.displayName //닉네임
이외의 추가적인 유저의 정보를 저장하고 싶다면 firestore를 사용해 데이터를 저장하는데 문서의 id
를 현재 사용자의 uId
로 설정하는 방식을 사용할 수 있다. 자세한 방식은 아래와 같다.
추가 유저정보
- 저장
- 인증방식을 통해 유저 정보 획득
- 유저 정보의 uId 획득
- Cloud Firestore에 컬렉션 생성
- 컬렉션에 Documnet를 생성하는데 id를 uId로 설정
- 읽기
- 인증방식을 통해 유저 정보 획득
- 유저 정보의 uId 획득
- Cloud Firestore의 컬렉션에서 uId에 해당하는 Document를 불러옴
- Firebase의 Authentication의 메서드들
- Firebase의 Facebook Authentication
- Firebase의 email verification
앱에서 대부분의 경우 사용자의 인증 상태(로그인 또는 로그아웃)을 알아야 한다.
Firebase 인증을 사용하면 Stream
을 통해 이러한 상태를 실시간으로 구독할 수 있다. 호출된 스트림은 사용자의 현재 인증 상태에 대한 즉각적인 이벤트를 제공한 다음 인증 상태가 변경될 때마다 후속 이벤트를 제공한다.
이를 지원하는 세 가지 메서드를 살펴보고자 한다.
- FirebaseAuth.instance.authStateChanges()
- FirebaseAuth.instance.idTokenChanges()
- FirebaseAuth.instance.userChanges()
authStateChanges()
는 User
에 대한 변화를 Stream
으로 전송해 준다. 아래와 같은 방식으로 리스너를 등록해 사용할 수 있다. User
가 없는 경우에는 null
이 들어온다.
FirebaseAuth.instance
.authStateChanges()
.listen((User user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
다음 상황이 발생하면 이벤트가 실행된다.
- 리스너가 등록된 직후
- 사용자가 로그인한 경우
- 현재 사용자가 로그아웃한 경우
idTokenChanges()
는 authStateChages()
와 사용 방식이 같지만 현재 사용자의 토큰이 변경된 경우도 관찰하여 이벤트를 적용할 수 있다.
FirebaseAuth.instance
.idTokenChanges()
.listen((User? user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
다음 상황이 발생하면 이벤트가 실행된다.
- 리스너가 등록된 직후
- 사용자가 로그인한 경우
- 현재 사용자가 로그아웃한 경우
- 현재 사용자의 토큰이 변경된 경우
userChanges()
는 authStateChanges()
와 idTokenChanges()
모두의 상위 집합이다.
authStateChanges()
와 idTokenChanges()
의 이벤트가 발생하는 상황에 더해서 FirebaseAuth.instance.currentUser
에서 제공하는 여러 메서드가 호출되는 경우에도 이벤트가 실행된다.
사용 방식은 앞의 두 메서드와 동일하다.
FirebaseAuth.instance
.userChanges()
.listen((User? user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
다음 상황이 발생하면 이벤트가 실행된다.
- 리스너가 등록된 직후
- 사용자가 로그인한 경우
- 현재 사용자가 로그아웃한 경우
- 현재 사용자의 토큰이 변경된 경우
FirebaseAuth.instance.currentUser
에서 제공하는 다음 메서드가 호출되는 경우
- reload()
- unlink()
- updateEmail()
- updatePassword()
- updatePhoneNumber()
- updateProfile()
이외에도 다양한 Firebase의 Authentication의 기능들은 아래의 공식문서에서 찾아볼 수 있다.
Firebase의 인증은 다양한 소셜 로그인 기능을 제공한다. 이번에는 Facebook의 로그인 기능을 적용해 보고자 한다. 안드로이드를 기준으로 진행했으며, 아래 공식 문서의 Facebook
탭을 참고하여 작성하였다.
Firebase - 문서 - 빌드 - Flutter - 제휴 ID 및 소셜
먼저 Facebook 개발자 앱을 설정해야 한다. 아래의 Facebook 개발자 사이트로 이동해 페이스북 계정으로 로그인한다.
My Apps
탭으로 이동하고, 앱 만들기 버튼을 눌러 새로운 앱을 만든다.
로그인 기능을 구현하기 위해 앱 유형은 소비자로 선택한다.
앱 이름을 정하고, 앱을 생성한다.
앱을 생성하면 아래와 같이 대시보드에 들어올 수 있다. 제품의 계정 추가에서 Facebook 로그인의 설정 버튼을 클릭한다.
원하는 플랫폼을 선택한다. 이번에는 Android를 선택해 진행했다.
1번과 2번은 설치가 필요할 경우 진행하고, 필요하지 않으면 넘어가도 된다. 3번의 과정에서 패키지 이름과 기본 액티비티 클래스 이름을 입력한다.
(패키지 이름은 프로젝트의 Menifest.xml
파일에서 찾을 수 있으며 액티비티 클래스 이름은 해당 패키지 이름에 .MainActivity
를 더해 설정한다.)
터미널에 아래 주어진 명령을 입력하여 28자리 해시코드를 받아 입력한다. 해당 명령에서 대문자로 작성된 부분은 자신의 환경에 맞게 수정해야한다.
(debug.keystore
의 기본 비밀번호는 android
이다.)
다음 과정에서 SSO 활성화를 "예"로 선택한다.
이제 Manifest.xml
을 수정해야 하는데 우선 string.xml
파일을 작성해야 한다. 해당 파일이 없다면 새로운 파일로 생성해서 작성한다.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">app_name</string>
<string name="facebook_app_id">1234</string>
<string name="fb_login_protocol_scheme">fb1234</string>
<string name="facebook_client_token">56789</string>
</resources>
app_id
는 Facebook 대시보드의 상단에 있다.fb_login_protocol_scheme
는fb + app_id
로 작성하면 된다.facebook_client_token
은 대시보드의설정 > 고급설정
에서 확인할 수 있다.
설정을 마치면 Manifest.xml
파일에 다음의 내용들을 추가한다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.firebase_app">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 인터넷 권한 추가-->
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="firebase_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- 추가 내용 시작 -->
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
<meta-data android:name="com.facebook.sdk.ClientToken" android:value="@string/facebook_client_token"/>
<activity android:name="com.facebook.FacebookActivity"
android:configChanges=
"keyboard|keyboardHidden|screenLayout|screenSize|orientation"
android:label="@string/app_name" />
<activity
android:name="com.facebook.CustomTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/fb_login_protocol_scheme" />
</intent-filter>
</activity>
<!-- 여기까지 추가 내용 -->
...
</application>
</manifest>
다음으로 Firebase의 콘솔에서 Facebook 로그인 제공업체를 사용으로 설정한다. 앱 ID와 앱 비밀번호는 Facebook 앱 대시보드의 설정 > 기본 설정
에서 확인할 수 있고, OAuth 리다이렉션 URL은 Facebook 대시보드의 Facebook 로그인 > 설정 > 유효한 OAuth 리다이렉션 URL
에 입력해야 한다.
설정을 마치면 Facebook 로그인 기능을 사용할 수 있다.
먼저 추가로 flutter_facebook_auth
플러그인을 설치한 뒤 코드를 작성했다.
import 'package:firebase_app/controller/auth_controller.dart';
import 'package:firebase_app/controller/login_controller.dart';
import 'package:firebase_app/firebase_options.dart';
import 'package:firebase_app/view/page/login_page.dart';
import 'package:firebase_app/view/page/main_page.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() async {
//firebase 초기화
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
Widget build(BuildContext context) {
return GetMaterialApp(
initialBinding: BindingsBuilder(() {
Get.put(AuthController());
}),
getPages: [
GetPage(name: MainPage.route, page: () => const MainPage()),
GetPage(name: LoginPage.route, page: () => const LoginPage()),
],
initialRoute: LoginPage.route,
);
}
}
main.dart
는 컨트롤러를 바인딩하고, 페이지 라우트를 설정한다. 초기 페이지는 로그인 페이지로 설정했다.
import 'package:firebase_app/view/page/login_page.dart';
import 'package:firebase_app/view/page/main_page.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
import 'package:get/get.dart';
class AuthController extends GetxController {
final Rxn<User> user = Rxn<User>();
//페이스북 로그인
signInWithFacebook() async {
//페이스북 로그인
final LoginResult loginResult = await FacebookAuth.instance.login();
//액세스 토큰을 통해서 credential을 생성
final OAuthCredential facebookAuthCredential =
FacebookAuthProvider.credential(loginResult.accessToken!.token);
//credential을 사용해 Firebase 로그인
FirebaseAuth.instance.signInWithCredential(facebookAuthCredential);
}
//페이스북 로그아웃
logOut() async {
await FacebookAuth.instance.logOut();
FirebaseAuth.instance.signOut();
}
void onInit() {
super.onInit();
FirebaseAuth.instance.authStateChanges().listen((value) {
user(value);
if (value != null) {
Get.toNamed(MainPage.route);
} else {
Get.toNamed(LoginPage.route);
}
});
}
}
AuthController
에서는 페이스북 로그인을 수행하는 signInWithFacebook()
과 로그아웃을 수행하는 logOut()
메서드를 만들었다. 유저 정보가 있으면 메인 페이지로 이동하도록 authStateChanges()
의 리스너를 onInit()
에 등록했다.
페이스북을 통한 Firebase 로그인은 아래의 과정을 통해 수행된다.
Facebook을 통한 Firebase 로그인 과정
1. Facebook 로그인 수행
2. 로그인된 사용자의 액세스 토큰을 통해 credential 생성
3. credential을 사용해 Firebase에 로그인
import 'package:firebase_app/controller/auth_controller.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
class LoginPage extends GetView<AuthController> {
const LoginPage({super.key});
static const route = '/login';
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: controller.signInWithFacebook,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
FaIcon(FontAwesomeIcons.facebook),
SizedBox(width: 8),
Text('페이스북 로그인'),
],
),
),
),
);
}
}
로그인 페이지는 버튼을 하나 생성해 누르면 페이스북 로그인을 수행하도록 구현했다.
import 'package:firebase_app/controller/auth_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MainPage extends GetView<AuthController> {
const MainPage({super.key});
static const route = '/main';
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text('로그인 되었습니다.'),
Text(
style: const TextStyle(color: Colors.green),
controller.user.value!.email!,
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: controller.logOut,
child: const Text('로그아웃'),
),
],
),
),
);
}
}
메인 페이지는 로그인 후에 진입 가능하며 로그인된 유저의 이메일을 출력하고, 로그아웃 버튼을 클릭하면 로그아웃을 수행해 로그인 페이지로 이동된다.
인증된 사용자가 올바른 유저인지 구별하기 위해서 이메일로 가입된 유저에 한해 Email Verification을 진행할 수 있다.
Email Verification을 진행하고 아래와 같이 Flutter 화면에 표시하고자 한다.
- 이메일 인증을 하지 않은 유저의 경우
- 이메일 인증이 진행된 유저의 경우
dependencies:
cupertino_icons: ^1.0.2
firebase_auth: ^4.2.10
firebase_core: ^2.7.1
flutter:
sdk: flutter
get: ^4.6.5
pubspec.yaml
에 필요한 패키지를 설치했다.
import 'package:firebase_app/controller/auth_controller.dart';
import 'package:firebase_app/controller/login_controller.dart';
import 'package:firebase_app/controller/signup_controller.dart';
import 'package:firebase_app/firebase_options.dart';
import 'package:firebase_app/view/page/login_page.dart';
import 'package:firebase_app/view/page/main_page.dart';
import 'package:firebase_app/view/page/signup_page.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() async {
//firebase 초기화
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
Widget build(BuildContext context) {
return GetMaterialApp(
theme: ThemeData(useMaterial3: true),
initialBinding: BindingsBuilder(() {
Get.put(AuthController());
Get.put(LoginController());
Get.put(SignupController());
}),
getPages: [
GetPage(name: MainPage.route, page: () => const MainPage()),
GetPage(name: LoginPage.route, page: () => const LoginPage()),
GetPage(name: SignupPage.route, page: () => const SignupPage()),
],
initialRoute: LoginPage.route,
);
}
}
main.dart
에서는 컨트롤러들을 바인딩하고 페이지의 라우트를 설정했다. 초기 페이지로는 LoginPage
를 지정했다.
import 'package:firebase_app/view/page/login_page.dart';
import 'package:firebase_app/view/page/main_page.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:get/get.dart';
class AuthController extends GetxController {
final Rxn<User> user = Rxn<User>();
login(id, pw) =>
FirebaseAuth.instance.signInWithEmailAndPassword(email: id, password: pw);
signup(id, pw) => FirebaseAuth.instance
.createUserWithEmailAndPassword(email: id, password: pw);
signout() => FirebaseAuth.instance.signOut();
sendEmailVerification() =>
FirebaseAuth.instance.currentUser?.sendEmailVerification();
void onInit() {
super.onInit();
FirebaseAuth.instance.authStateChanges().listen((value) {
user(value);
if (value != null) {
Get.toNamed(MainPage.route);
} else {
Get.toNamed(LoginPage.route);
}
});
}
}
AuthController
는 계정을 관리한다. 유저의 정보를 user
에 저장하고, 로그인, 회원가입, 로그아웃, 인증 이메일 전송 메서드를 작성했다.
onInit()
에서는 유저의 로그인이나 로그아웃을 관찰하여 메인페이지 또는 로그인페이지로 이동한다.
import 'package:firebase_app/controller/auth_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class LoginController extends GetxController {
var idController = TextEditingController();
var pwController = TextEditingController();
login() {
var controller = Get.find<AuthController>();
controller.login(idController.text, pwController.text);
}
}
LoginController
는 로그인에 사용할 아이디와 패스워드의 텍스트 필드 컨트롤러를 가지고 있으며, AuthController
의 login
을 호출하여 로그인을 수행한다.
import 'package:firebase_app/controller/auth_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class SignupController extends GetxController {
var idController = TextEditingController();
var pwController = TextEditingController();
signup() {
var controller = Get.find<AuthController>();
controller.signup(idController.text, pwController.text);
}
}
SignupController
는 회원가입에 사용할 아이디와 패스워드의 텍스트 필드 컨트롤러를 가지고 있으며, AuthController
의 signup
을 호출하여 회원가입을 수행한다.
import 'package:firebase_app/controller/login_controller.dart';
import 'package:firebase_app/view/page/signup_page.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class LoginPage extends GetView<LoginController> {
const LoginPage({super.key});
static const route = '/login';
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('로그인'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: controller.idController,
decoration: const InputDecoration(hintText: '아이디'),
),
TextField(
controller: controller.pwController,
decoration: const InputDecoration(hintText: '패스워드'),
),
ElevatedButton(
onPressed: controller.login,
child: const Text('로그인'),
),
TextButton(
onPressed: () => Get.toNamed(SignupPage.route),
child: const Text('회원가입'),
),
],
),
),
);
}
}
LoginPage
는 아이디와 패스워드를 입력받아 로그인을 수행한다.
로그인 버튼을 클릭하면 로그인이 진행되며, 회원가입 버튼을 클릭하면 회원가입 페이지로 이동한다.
import 'package:firebase_app/controller/signup_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class SignupPage extends GetView<SignupController> {
const SignupPage({super.key});
static const route = '/signup';
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('회원가입'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: controller.idController,
decoration: const InputDecoration(hintText: '아이디'),
),
TextField(
controller: controller.pwController,
decoration: const InputDecoration(hintText: '패스워드'),
),
ElevatedButton(
onPressed: controller.signup,
child: const Text('회원가입'),
),
],
),
),
);
}
}
SignupPage
는 아이디와 패스워드를 입력받아 회원가입을 수행한다.
import 'package:firebase_app/controller/auth_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MainPage extends GetView<AuthController> {
const MainPage({super.key});
static const route = '/main';
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(controller.user.value!.email ?? ''),
const SizedBox(height: 8),
if (controller.user.value!.emailVerified)
const Row(
children: [
Icon(color: Colors.green, Icons.check_circle),
Text(
style: TextStyle(color: Colors.green),
'이메일이 인증된 사용자 입니다.',
),
],
)
else
Row(
children: [
const Text(
style: TextStyle(color: Colors.red),
'이 곳을 눌러 이메일 인증을 해주세요.',
),
const SizedBox(width: 8),
TextButton(
onPressed: controller.sendEmailVerification,
child: const Text('인증 메일 전송'),
)
],
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: controller.signout,
child: const Text('로그아웃'),
),
],
),
),
),
);
}
}
MainPage
는 로그인 된 사용자의 이메일을 출력하고 이메일 인증 여부에 따라 메세지를 출력한다. 인증 되지 않은 경우 인증 메일 전송 버튼을 클릭해 메일을 보낼 수 있다.
로그아웃 버튼을 클릭하면 AuthController
의 signout
을 호출해 로그아웃을 수행한다.
Firebase는 이메일로 회원가입을 수행한 경우 인증 메일을 전송하여 사용자를 인증할 수 있도록 기능을 제공한다.
이 때, 전송되는 이메일 템플릿을 수정할 수 있는데 우선 아래와 같이 Firebase 콘솔에서 Athentication으로 들어가 Templates
탭으로 이동한다.
작성되어 있는 이메일 형식의 옆의 연필 모양 아이콘을 클릭하여 템플릿을 수정할 수 있다. 템플릿을 아래와 같이 수정해 보았다.
아쉽게도 발송자 이름, 발송 주소, 답장 주소, 제목 까지는 수정할 수 있지만, 메세지 내용부분은 수정이 불가능하다.
이외에도 비밀번호 재설정의 템플릿, 이메일 주소 변경 시 템플릿 등도 수정이 가능하다.
먼저 앱을 실행하면 로그인화면이 출력된다.
회원가입 버튼을 눌러 회원가입 페이지로 이동하고 아래와 같이 회원가입을 진행했다.
회원가입을 진행하면 로그인이 자동으로 진행되어 아래와 같이 메인페이지로 이동된다. 아직 이메일 인증을 수행하지 않아 인증을 진행해 달라는 메세지가 출력된다.
해당 페이지에서 인증 메일 전송 버튼을 누르면 로그인된 이메일로 인증 메일이 전송된다. 앞에서 변경한 템플릿이 적용된 것을 알 수 있다. 링크를 클릭하면 인증이 수행된다.
인증을 완료한 뒤 다시 로그인을 해보면 인증이 완료된 사용자라는 메세지가 출력되는 것을 확인할 수 있다.
내일이 아마도 플러터 강의가 마지막일 것 같다. 목요일 부터는 월말평가에 들어가고 끝나면 프로젝트를 시작한다. 지금까지 모든 과제를 당일에 끝냈는데 오늘은 끝낼 수가 없다. 2번 과제를 하려고 페이스북을 새로 가입했는데 자기 혼자 계정을 차단해버렸다. 계정 확인을 완료해서 풀리려면 하루가 걸린다고 해서 내일 마저 해야될 것 같다. 다른 과제는 다 했는데...ㅠㅠ (근데 내일 페이스북 계정 확인 끝났는데 차단 안 풀리면 어쩌지...?)어쨌든 후기는 오늘 미리 쓰자면 firebase의 auth 기능은 내용을 정리하기 보다 직접 많이 써봐야 될 것 같다. 그리고 Firebase는 다른 블로그들의 글보다 공식 문서에 정리가 너무 잘 되어있다. 공식 문서를 꼼꼼히 살펴보는게 좋을 것 같다.
(페이스북 계정 차단이 하루만에 풀려서 과제 2번 내용을 추가했습니다.)