daiki0508'足跡

Firebaseを使ってメール/パスワード認証を実装してみた

前々からFirebaseについて色々と勉強したいと思っていたが、5月のGWで時間に余裕が出来て勉強したのでその内容を共有するということと、メモ書きとして残すために記事を書きました。

f:id:daiki0508:20210518120350p:plain

目次

Firebaseって?

まずFirebaseを知らない方に簡単に説明すると、Firebaseとは

Firebaseは、2011年にFirebase, Inc.が開発したモバイル・Webアプリケーション開発プラットフォームで、その後2014年にGoogleに買収された。 2020年3月現在、Firebaseプラットフォームには19の製品があり、9GAGを含む150万以上のアプリが利用されている。

とあり、Firebaseを使うことで様々なサードパーティーの認証を実装出来たり、Firebaseにデータを保存したり、クライアント端末に通知を送信する...etcといったことが比較的簡単に実装できるようになります。

筆者の環境

IDE:AndroidStudio4.21
ホストPC:Windows10
端末:Galaxy S8 SCV36
端末OS:Android9
使用言語:Java

サンプルアプリの作成

それでは今回の記事で作るサンプルアプリを紹介します。
また、これ以降ではコード前文の解説ではなく抜粋しての解説ですので、全ての処理を知りたい方はこちらのコードの全文を見ながら読むことを推奨します。

概要

起動直後のアプリのトップ画面は以下の画像のようになっています。

f:id:daiki0508:20210518114339p:plain

アカウントがある場合は「メールアドレスとパスワード」を入力してログインボタンをタップするとログインできます。
アカウントが無い場合は「SignUp」のスイッチを切り替えて、同じく「メールアドレスとパスワード」を入力してサインアップします。

f:id:daiki0508:20210518114522p:plain

なお、今回は特にメールアドレスの存在確認といった処理は行いません
(※後日、記事を出すかも…?)

「サインイン」と「サインアップ」の両方とも、認証フローが正常に終了すれば画面下部にユーザ情報が表示されるようにしています。

f:id:daiki0508:20210518114646p:plain

反対に、認証フローが失敗(メールアドレス or パスワードが違う)したならばトーストを表示させるようにしています

f:id:daiki0508:20210518114939p:plain

また、画面右上のオプションメニューからサインアウトすることが出来るようにもしています。

f:id:daiki0508:20210518114722p:plain

xmlの編集

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#dfe"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_marginTop="15dp"
        android:text="@string/hello"
        android:textSize="25sp" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/description"
        android:layout_marginTop="5dp"
        android:text="@string/description"
        android:textSize="20sp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:weightSum="1.1"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/mail_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:text="@string/mail"
            android:textSize="17sp" />

        <EditText
            android:id="@+id/mail_edit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:layout_marginStart="5dp"
            android:layout_weight="1"
            android:hint="@string/sample_mail"
            android:maxLines="1"
            android:maxLength="50"
            android:inputType="textEmailAddress"/>
    </LinearLayout>

        <com.google.android.material.textfield.TextInputLayout
            style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="2dp"
            android:layout_marginStart="5dp"
            android:layout_marginEnd="5dp"
            android:hint="@string/sample_pass"
            app:passwordToggleEnabled="true">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/password_edit"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textPassword"
                android:maxLines="1"
                android:maxLength="20"/>
        </com.google.android.material.textfield.TextInputLayout>

    <Button
        android:id="@+id/execute_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:text="@string/login_button"
        android:onClick="execute" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:weightSum="1">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@string/signup"
            android:layout_weight="0.95"
            android:gravity="right"/>

       <Switch
            android:id="@+id/signup_switch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
           android:layout_weight="0"
           tools:ignore="UseSwitchCompatOrMaterialXml" />
    </LinearLayout>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:id="@+id/result"
        android:layout_weight="1"
        android:textSize="15sp" />

</LinearLayout>

最新のAndroidStudioを使ってる方はプロジェクトを作成した時点でxmlは「ConstraintLayout」になっていると思いますが、今回は「LinerLayout」を使用します。

com.google.android.material.textfield.TextInputLayout

app:passwordToggleEnabled="true"

上記の記述でユーザーは入力したパスワードをパスワード欄右のアイコンから確認できるようになります。

com.google.android.material.textfield.TextInputEditText

このフィールド内でパスワード入力欄の様々な設定(文字数制限やインプットタイプの指定等)が出来ます。

Firebaseとの連携設定

レイアウトや文字の記述が終わったら次は処理の記述なのですが、その前にFirebaseでアプリの登録を行い、連携を完了させておきましょう。

この記事では詳しい解説は行いませんが、Firebaseの公式ドキュメントでやり方が記述されているのでそちらをご覧ください。

firebase.google.com

SwitchListenerの定義

まず最初に「サインイン」と「サインアップ」の切り替えを行う「スイッチ」の処理を記述しましょう。*1

private class SignUpSwitchListener implements CompoundButton.OnCheckedChangeListener{
    @Override
    public void onCheckedChanged(CompoundButton button, boolean isChecked){
        flag = isChecked;
        if (flag){
            execute_b.setText(getString(R.string.signup));
        }else {
            execute_b.setText(getString(R.string.login_button));
        }

    }
}

onCheckedChanged

このメソッドの第2引数にswitchのon/offがtruefalseで格納されています。
そしてそれをflagというboolean型のグローバル変数に格納してその後の条件式や、他のメソッドでも用います。

ここではswitchの切り替えによってボタンテキストの中身を変更しています。

サインアップ時の処理

次に別クラスAuthenticationFlowClassを作成してその中にまずはサインアップ処理を記述していきます。

public class AuthenticationFlowClass extends MainActivity{
    private final FirebaseAuth mAuth;
    private final MainActivity mainActivity;

    AuthenticationFlowClass (MainActivity mainActivity){
        this.mAuth = MainActivity.mAuth;
        this.mainActivity = mainActivity;
    }
    void CreateUser(String mail, String pass){
        mAuth.createUserWithEmailAndPassword(mail, pass)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        if (task.isSuccessful()){
                            Log.d("success","createUserWithEmail:success");
                            FirebaseUser user = mAuth.getCurrentUser();
                            mainActivity.updateUI(Objects.requireNonNull(user));
                        }else {
                            Log.w("Error","createUserWithEmail:failure",task.getException());
                            Toast.makeText(mainActivity,"エラーが発生しました",Toast.LENGTH_SHORT).show();
                        }
                    }
                });
    }
}

createUserWithEmailAndPassword

この内部でアカウント作成成功時と失敗時の処理がそれぞれ行われています。
第1引数にメールアドレス、第2引数にパスワードを渡して呼ぼ出します。
この時のmAuthはMainActivityのonCreate*2FirebaseAuth.getInstance()で初期化されています。

成功時はupdateUIに現在のログインしているユーザの情報を取得するgetCurrentUser()によって格納されたFirebaseUserを渡します。
(※updateUIについては後述します)

失敗時はトーストでエラーが発生しましたと表示されるようになっています。

サインイン時の処理

void SignIn(String email, String pass){
    mAuth.signInWithEmailAndPassword(email, pass)
            .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                @Override
                public void onComplete(@NonNull Task<AuthResult> task) {
                    if (task.isSuccessful()){
                        FirebaseUser user = mAuth.getCurrentUser();
                        mainActivity.updateUI(Objects.requireNonNull(user));
                    }else {
                        Toast.makeText(mainActivity,"Authentication failed.",Toast.LENGTH_SHORT).show();
                    }
                }
            });
}

signInWithEmailAndPassword

この内部でサインインの認証処理と、その結果を処理します。
第1引数はメールアドレス、第2引数はパスワードを渡して呼び出します。

成功時と失敗時の処理はサインアップ時と然程違いはありません。

updateUI処理

このupdateUIではメイン画面の下部にユーザ情報を表示させる画面表示処理です。

protected void updateUI(FirebaseUser user){
    String email = "Email:" + user.getEmail() + "\n";
    String emailVerified = "Verified:" +  user.isEmailVerified() + "\n";
    String uid = "Uid:" + user.getUid() + "\n";

    String result_str = email + emailVerified + uid;
    resultText.setText(result_str);
}

getEmail

FirebaseUserにはそれぞれの呼び出し元においてgetCurrentUser()された現在のログインしているユーザの情報が格納されています。
その中でも`getEmailはユーザのメールアドレスを取得するメソッドです。

user.isEmailVerified

このメソッドにはユーザのメールアドレスが認証されている(正しいメールアドレス)かどうかの情報を取得できるメソッドです。

ここでは解説していない確認メールの送信処理を別途、アプリ内に記述して処理を行うことでこの値がtrueになります。

getUid

現在ログインしているユーザのユーザーID(一意の値)を取得することが出来るメソッドです。

認証フローを呼び出す

サインイン時の処理サインアップ時の処理で認証フローの定義は行ったので、次はボタンを押したらその処理が呼び出されるコードを記述*3していきます。

public void execute(View view) {
    String mail_str = mail_e.getText().toString();
    String pass_str = pass_e.getText().toString();

    EditCheck(mail_str,pass_str);
}

private void EditCheck(String email,String pass){
    if (email.length() > 0 && pass.length() > 0) {
        if (flag){
            afc.CreateUser(email, pass);
        }else {
            afc.SignIn(email, pass);
        }
    } else if (email.length() == 0) {
        Toast.makeText(this, "mailアドレスが入力されていません", Toast.LENGTH_SHORT).show();
        if (pass.length() == 0) {
            Toast.makeText(this, "passwordが入力されていません。", Toast.LENGTH_SHORT).show();
        }
    } else {
        Toast.makeText(this, "passwordが入力されていません。", Toast.LENGTH_SHORT).show();
    }
}

execute

メールアドレスとパスワードを入力するエディットボックスからそれぞれ文字列を抽出して変数に代入しています。
またそれをEditCheckという入力値チェックの関数に渡しています。

EditCheck

レイアウトファイルにも記載してメールアドレスとパスワードには文字数制限を掛けておきましたが、念のためJavaファイルの方でも文字数制限を記述しておきます。
また、そもそも入力が行われていないときや、制限文字数を超えて入力した場合はトーストを表示させて、認証フローに処理が飛ばないようにしています。

適切な文字数が入力された場合には、SwitchListenerで変化するflagによって処理を分岐して認証処理を開始*4します。

サインアウト処理

アプリをアンインストールしたり、開きなおせば必然的にサインアウトも行われるでしょうが、それでは少し不便なのでユーザが明示的にサインアウトを行えるようにしましょう。

@Override
public boolean onCreateOptionsMenu(Menu menu){
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu_options_menu_list,menu);

    return super.onCreateOptionsMenu(menu);
}

@Override
public boolean onOptionsItemSelected(MenuItem item){
    int itemId = item.getItemId();

    if (itemId == R.id.signout){
        mAuth.signOut();
        finish();
        overridePendingTransition(0,0);
        startActivity(getIntent());
        overridePendingTransition(0,0);
    }

    return super.onOptionsItemSelected(item);
}

signOut

ここではオプションメニューを用いてサインアウトを行えるようにしています。
(※オプションメニューの詳しい解説は行いません)

サインアウトを行うにはFirebaseAuth.getInstance.signOutでOKです。

onStart()のOverride

最後に現在ログインしていてサインアウトをしていないにも関わらず、アプリを開きなおすたびにメールアドレスとパスワードの再入力を求めるのはセキュリティの高いアプリ(銀行...etc)でない限り、不便です。

よってアクティビティライフサイクルのonStartメソッドをオーバーライドしてログインが行われていたら、updateUIを呼び出すような処理を追加します。

@Override
public void onStart(){
    super.onStart();

    FirebaseAuth mAuth = FirebaseAuth.getInstance();
    FirebaseUser currentUser = mAuth.getCurrentUser();
    if (currentUser != null){
        updateUI(currentUser);
    }
}

まとめ

今回はFirebaseでメールアドレスとパスワードを用いた認証を行う方法を解説しました。
やはりFirebaseを使うと自分でユーザ認証の実装を行うという面倒くさいことをしなくてよくなるのは大きなメリットですよね。

参考・引用

https://firebase.google.com/docs/android/setup?hl=ja

https://firebase.google.com/?hl=ja

https://ja.wikipedia.org/wiki/Firebase

*1:onCreateでリスナの設定も忘れないように

*2:https://github.com/daiki0508/FirebaseMailAndPW

*3:レイアウトファイルにボタンに対してonClick属性を記述済み

*4:onCreateにコンストラクタの初期化を記述