Продолжаем работать с Firebase. В этом уроке будем тестировать сервис аутентификации. Идентификация пользователей необходима в большинстве приложений. Это позволяет разделять доступ, надежно хранить личные данные пользователей в облаке и обеспечить персонализированный опыт на всех устройствах пользователя. [wpanchor id=»1″]
Firebase предоставляет бэкенд, простой в использовании SDK и готовые библиотеки пользовательского интерфейса для реализации аутентификации пользователей в вашем приложении. Он поддерживает аутентификацию как с помощью email и пароля, так и с помощью таких популярных поставщиков идентификации, как Google, Facebook, Twitter и GitHub.
Сервис аутентификации тесно интегрируется с другими сервисами Firebase, использует отраслевые стандарты, такие как OAuth 2.0 и OpenID Connect, так что он может быть легко интегрирован с вашим бэкэндом. Сегодня мы рассмотрим метод аутентификации пользователей с помощью адреса электронной почты и пароля. Firebase Authentication SDK предоставляет методы для создания и управления пользователями, которые используют адреса электронной почты и пароли для входа в систему.
Будем использовать пример, созданный разработчиками Firebase на GitHub. Для этого клонируйте или скачайте репозиторий и откройте проект с именем auth в Android Studio.
Чтобы интегрировать проект с Firebase, откройте меню Tools/Firebase и в окне ассистента выберите Authentication. нажмите кнопку в шаге 1, чтобы связать ваше приложение с Firebase.
В случае успеха в консоли Firebase вы увидите новый проект с именем auth. Зайдите в него и выберите слева пункт меню Authentication. На вкладке «Способ входа» включите пункт «Адрес электронной почты/пароль».
В процессе регистрации пользователей в приложении информация о них будет появляться на вкладке «Пользователи». Здесь можно управлять пользователями — например, добавить пользователя в базу приложения, а также отключить или удалить пользователя.
Вернемся к проекту в Android Studio. Если вы видите в консоли ошибки — сделайте ребилд проекта.
Нас интересует EmailPasswordActivity. Это активити связано с файлом макета экрана activity_emailpassword.xml. Давайте посмотрим его структуру. Здесь расположены логотип Firebase, информационные текстовые поля, поля ввода email и пароля, а также кнопки входа и создания аккаунта. Это весь пользовательский интерфейс.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/main_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/grey_100" android:orientation="vertical" android:weightSum="4"> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="3" android:gravity="center_horizontal" android:orientation="vertical"> <ImageView android:id="@+id/icon" style="@style/ThemeOverlay.FirebaseIcon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="@string/desc_firebase_lockup" android:src="@drawable/firebase_lockup_400" /> <TextView android:id="@+id/title_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/title_bottom_margin" android:text="@string/emailpassword_title_text" android:theme="@style/ThemeOverlay.MyTitleText" /> <TextView android:id="@+id/status" style="@style/ThemeOverlay.MyTextDetail" android:text="@string/signed_out" /> <TextView android:id="@+id/detail" style="@style/ThemeOverlay.MyTextDetail" tools:text="Firebase User ID: 123456789abc" /> </LinearLayout> <RelativeLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:background="@color/grey_300" android:gravity="center_vertical"> <LinearLayout android:id="@+id/email_password_fields" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingLeft="16dp" android:paddingRight="16dp"> <EditText android:id="@+id/field_email" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="@string/hint_email" android:inputType="textEmailAddress" /> <EditText android:id="@+id/field_password" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="@string/hint_password" android:inputType="textPassword" /> </LinearLayout> <LinearLayout android:id="@+id/email_password_buttons" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_below="@+id/email_password_fields" android:orientation="horizontal" android:paddingLeft="16dp" android:paddingRight="16dp"> <Button android:id="@+id/email_sign_in_button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/sign_in" android:theme="@style/ThemeOverlay.MyDarkButton" /> <Button android:id="@+id/email_create_account_button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/create_account" android:theme="@style/ThemeOverlay.MyDarkButton" /> </LinearLayout> <Button android:id="@+id/sign_out_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/sign_out" android:theme="@style/ThemeOverlay.MyDarkButton" android:visibility="gone" /> </RelativeLayout> </LinearLayout>
Откроем класс EmailPasswordActivity. Он унаследован от класса BaseActivity, который в свою очередь является наследником AppCompatActivity. Это сделано только для того, чтобы инкапсулировать методы отображения и скрытия окна ProgressDialog, которое вызывается при ожидании ответа сервера, и сократить таким образом код.
import android.app.ProgressDialog; import android.support.annotation.VisibleForTesting; import android.support.v7.app.AppCompatActivity; public class BaseActivity extends AppCompatActivity { @VisibleForTesting public ProgressDialog mProgressDialog; public void showProgressDialog() { if (mProgressDialog == null) { mProgressDialog = new ProgressDialog(this); mProgressDialog.setMessage(getString(R.string.loading)); mProgressDialog.setIndeterminate(true); } mProgressDialog.show(); } public void hideProgressDialog() { if (mProgressDialog != null && mProgressDialog.isShowing()) { mProgressDialog.dismiss(); } } @Override public void onStop() { super.onStop(); hideProgressDialog(); } }
В классе EmailPasswordActivity мы наблюдаем объявление элементов пользовательского интерфейса, а также объекта класса FirebaseAuth и его слушателя AuthStateListener.
// [START declare_auth] private FirebaseAuth mAuth; // [END declare_auth] // [START declare_auth_listener] private FirebaseAuth.AuthStateListener mAuthListener; // [END declare_auth_listener]
Класс FirebaseAuth — это точка входа в Firebase Authentication SDK. А интерфейс FirebaseAuth.AuthStateListener вызывается, когда происходит изменение в состоянии аутентификации.
Разработчики рекомендуют отслеживать текущее состояние экземпляра FirebaseAuth с помощью слушателей. Cлушатель получает уведомления каждый раз, когда определенное событие происходит с объектом FirebaseAuth.
В методе onCreate находим все элементы экрана и присваиваем слушатели кнопкам. Далее инициализируем экземпляр FirebaseAuth и создаем слушатель AuthStateListener с методом onAuthStateChanged, чтобы отслеживать, когда пользователь входит или выходит.
// [START initialize_auth] mAuth = FirebaseAuth.getInstance(); // [END initialize_auth] // [START auth_state_listener] mAuthListener = new FirebaseAuth.AuthStateListener() { @Override public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { FirebaseUser user = firebaseAuth.getCurrentUser(); if (user != null) { // User is signed in Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid()); } else { // User is signed out Log.d(TAG, "onAuthStateChanged:signed_out"); } // [START_EXCLUDE] updateUI(user); // [END_EXCLUDE] } }; // [END auth_state_listener]
Согласно документации, слушатель получает уведомления в следующих ситуациях:
- Когда объект FirebaseAuth завершает инициализацию и пользователь уже авторизован в предыдущей сессии, или был перенаправлен из потока входа другого поставщика идентификации.
- Когда пользователь выполняет вход (текущий пользователь установлен)
- Когда пользователь выполняет выход (текущий пользователь становится null)
- Когда маркер доступа (access token) текущего пользователя обновляется. Это может произойти в следующих случаях:
- если истек срок действия токена
- пользователь повторно выполняет проверку подлинности
- пользователь меняет свой пароль.
При этом Firebase выдает новые маркеры доступа и помечает старые как недействительные. При смене пароля автоматически выполняется выход пользователя на каждом устройстве, по соображениям безопасности.
Но вернемся к коду. Получаем экземпляр класса FirebaseUser, который представляет сведения о профиле пользователя в базе данных пользователей проекта Firebase. Он также содержит вспомогательные методы для изменения или получения информации о профиле, а также для управления состоянием аутентификации пользователя.
Ниже здесь вызывается метод updateUI, принимающий экземпляр текущего пользователя. В этом методе происходит вывод информации о текущем пользователе в текстовые поля на экране приложения, а также регулируется видимость элементов пользовательского интерфейса в зависимости от того, авторизован пользователь или нет.
private void updateUI(FirebaseUser user) { hideProgressDialog(); if (user != null) { mStatusTextView.setText(getString(R.string.emailpassword_status_fmt, user.getEmail())); mDetailTextView.setText(getString(R.string.firebase_status_fmt, user.getUid())); findViewById(R.id.email_password_buttons).setVisibility(View.GONE); findViewById(R.id.email_password_fields).setVisibility(View.GONE); findViewById(R.id.sign_out_button).setVisibility(View.VISIBLE); } else { mStatusTextView.setText(R.string.signed_out); mDetailTextView.setText(null); findViewById(R.id.email_password_buttons).setVisibility(View.VISIBLE); findViewById(R.id.email_password_fields).setVisibility(View.VISIBLE); findViewById(R.id.sign_out_button).setVisibility(View.GONE); } }
В методах onStart() и onStop() присваиваем слушатель экземпляру класса FirebaseAuth и удаляем слушатель соответственно.
// [START on_start_add_listener] @Override public void onStart() { super.onStart(); mAuth.addAuthStateListener(mAuthListener); } // [END on_start_add_listener] // [START on_stop_remove_listener] @Override public void onStop() { super.onStop(); if (mAuthListener != null) { mAuth.removeAuthStateListener(mAuthListener); } } // [END on_stop_remove_listener]
В методе createAccount, который срабатывает при нажатии кнопки создания аккаунта, сперва выполняется проверка на пустоту полей ввода email и пароля, из которых метод получает строковые переменные email и password. Логика этой проверки прописана в методе validateForm.
private boolean validateForm() { boolean valid = true; String email = mEmailField.getText().toString(); if (TextUtils.isEmpty(email)) { mEmailField.setError("Required."); valid = false; } else { mEmailField.setError(null); } String password = mPasswordField.getText().toString(); if (TextUtils.isEmpty(password)) { mPasswordField.setError("Required."); valid = false; } else { mPasswordField.setError(null); } return valid; }
Далее метод createUserWithEmailAndPassword пытается создать новую учетную запись пользователя, связанную с указанным адресом электронной почты и паролем. В базе данных проекта Firebase email служит уникальным идентификатором, а также используется для отправки письма сброса пароля.
private void createAccount(String email, String password) { Log.d(TAG, "createAccount:" + email); if (!validateForm()) { return; } showProgressDialog(); // [START create_user_with_email] mAuth.createUserWithEmailAndPassword(email, password) .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() { @Override public void onComplete(@NonNull Task<AuthResult> task) { Log.d(TAG, "createUserWithEmail:onComplete:" + task.isSuccessful()); // If sign in fails, display a message to the user. If sign in succeeds // the auth state listener will be notified and logic to handle the // signed in user can be handled in the listener. if (!task.isSuccessful()) { Toast.makeText(EmailPasswordActivity.this, R.string.auth_failed, Toast.LENGTH_SHORT).show(); } // [START_EXCLUDE] hideProgressDialog(); // [END_EXCLUDE] } }); // [END create_user_with_email] }
В случае успешного создания учетной записи пользователя, будет автоматически выполнен вход этого пользователя в вашем приложении. После успешного завершения этой операции вызывается событие onAuthStateChanged(FirebaseAuth) во всех зарегистрированных слушателях FirebaseAuth.AuthStateListeners.
Создание учетной записи пользователя может завершиться ошибкой, если учетная запись уже существует, email некорректный или пароль недостаточно сильный. Вот список исключений, которые могут быть выброшены при этом:
- FirebaseAuthWeakPasswordException — пароль не является достаточно сильным
- FirebaseAuthInvalidCredentialsException — email адрес имеет неправильный формат
- FirebaseAuthUserCollisionException — уже существует учетная запись с таким email
Метод addOnCompleteListener добавляет слушатель, который вызывается, когда задача завершается.
В методе onComplete интерфейса OnCompleteListener можно реализовать оповещение об успешном входе и прописать логику поведения приложения.
Если вход не выполнен, выводится сообщение об ошибке для пользователя. Можно отлавливать здесь вышеупомянутые исключения и выводить более осмысленные сообщения с причиной ошибки.
О том, что такое исключения и как с ними работать, смотрите в видеоуроках, здесь и здесь.
Далее здесь метод signIn, который выполняется при нажатии кнопки входа в пользовательском интерфейсе. Тело метода похоже на уже рассмотренный метод createAccount. Метод signIn также принимает на вход email и пароль, проверяет их и затем выполняет попытку авторизации пользователя с помощью метода signInWithEmailAndPassword.
private void signIn(String email, String password) { Log.d(TAG, "signIn:" + email); if (!validateForm()) { return; } showProgressDialog(); // [START sign_in_with_email] mAuth.signInWithEmailAndPassword(email, password) .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() { @Override public void onComplete(@NonNull Task<AuthResult> task) { Log.d(TAG, "signInWithEmail:onComplete:" + task.isSuccessful()); // If sign in fails, display a message to the user. If sign in succeeds // the auth state listener will be notified and logic to handle the // signed in user can be handled in the listener. if (!task.isSuccessful()) { Log.w(TAG, "signInWithEmail:failed", task.getException()); Toast.makeText(EmailPasswordActivity.this, R.string.auth_failed, Toast.LENGTH_SHORT).show(); } // [START_EXCLUDE] if (!task.isSuccessful()) { mStatusTextView.setText(R.string.auth_failed); } hideProgressDialog(); // [END_EXCLUDE] } }); // [END sign_in_with_email] }
После успешного завершения этой операции также вызывается событие onAuthStateChanged(FirebaseAuth) во всех зарегистрированных слушателях FirebaseAuth.AuthStateListeners.
Исключения, которые выбрасываются в случае неудачи:
- FirebaseAuthInvalidUserException возникает, если учетная запись пользователя с таким email не существует или отключена
- FirebaseAuthInvalidCredentialsException возникает, если введен неправильный пароль
Следующий метод — signOut — выполняется при нажатии кнопки выхода и вызывает одноименный метод класса FirebaseAuth. Выход из аккаунта текущего пользователя и удаление его из кэша.
private void signOut() { mAuth.signOut(); updateUI(null); }
И метод onClick обрабатывает нажатия кнопок на экране:
@Override public void onClick(View v) { int i = v.getId(); if (i == R.id.email_create_account_button) { createAccount(mEmailField.getText().toString(), mPasswordField.getText().toString()); } else if (i == R.id.email_sign_in_button) { signIn(mEmailField.getText().toString(), mPasswordField.getText().toString()); } else if (i == R.id.sign_out_button) { signOut(); } }
Остальные методы мы уже рассмотрели.
Весь код класса EmailPasswordActivity.java:
import android.os.Bundle; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.auth.AuthResult; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseUser; public class EmailPasswordActivity extends BaseActivity implements View.OnClickListener { private static final String TAG = "EmailPassword"; private TextView mStatusTextView; private TextView mDetailTextView; private EditText mEmailField; private EditText mPasswordField; // [START declare_auth] private FirebaseAuth mAuth; // [END declare_auth] // [START declare_auth_listener] private FirebaseAuth.AuthStateListener mAuthListener; // [END declare_auth_listener] @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_emailpassword); // Views mStatusTextView = (TextView) findViewById(R.id.status); mDetailTextView = (TextView) findViewById(R.id.detail); mEmailField = (EditText) findViewById(R.id.field_email); mPasswordField = (EditText) findViewById(R.id.field_password); // Buttons findViewById(R.id.email_sign_in_button).setOnClickListener(this); findViewById(R.id.email_create_account_button).setOnClickListener(this); findViewById(R.id.sign_out_button).setOnClickListener(this); // [START initialize_auth] mAuth = FirebaseAuth.getInstance(); // [END initialize_auth] // [START auth_state_listener] mAuthListener = new FirebaseAuth.AuthStateListener() { @Override public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { FirebaseUser user = firebaseAuth.getCurrentUser(); if (user != null) { // User is signed in Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid()); } else { // User is signed out Log.d(TAG, "onAuthStateChanged:signed_out"); } // [START_EXCLUDE] updateUI(user); // [END_EXCLUDE] } }; // [END auth_state_listener] } // [START on_start_add_listener] @Override public void onStart() { super.onStart(); mAuth.addAuthStateListener(mAuthListener); } // [END on_start_add_listener] // [START on_stop_remove_listener] @Override public void onStop() { super.onStop(); if (mAuthListener != null) { mAuth.removeAuthStateListener(mAuthListener); } } // [END on_stop_remove_listener] private void createAccount(String email, String password) { Log.d(TAG, "createAccount:" + email); if (!validateForm()) { return; } showProgressDialog(); // [START create_user_with_email] mAuth.createUserWithEmailAndPassword(email, password) .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() { @Override public void onComplete(@NonNull Task<AuthResult> task) { Log.d(TAG, "createUserWithEmail:onComplete:" + task.isSuccessful()); // If sign in fails, display a message to the user. If sign in succeeds // the auth state listener will be notified and logic to handle the // signed in user can be handled in the listener. if (!task.isSuccessful()) { Toast.makeText(EmailPasswordActivity.this, R.string.auth_failed, Toast.LENGTH_SHORT).show(); } // [START_EXCLUDE] hideProgressDialog(); // [END_EXCLUDE] } }); // [END create_user_with_email] } private void signIn(String email, String password) { Log.d(TAG, "signIn:" + email); if (!validateForm()) { return; } showProgressDialog(); // [START sign_in_with_email] mAuth.signInWithEmailAndPassword(email, password) .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() { @Override public void onComplete(@NonNull Task<AuthResult> task) { Log.d(TAG, "signInWithEmail:onComplete:" + task.isSuccessful()); // If sign in fails, display a message to the user. If sign in succeeds // the auth state listener will be notified and logic to handle the // signed in user can be handled in the listener. if (!task.isSuccessful()) { Log.w(TAG, "signInWithEmail:failed", task.getException()); Toast.makeText(EmailPasswordActivity.this, R.string.auth_failed, Toast.LENGTH_SHORT).show(); } // [START_EXCLUDE] if (!task.isSuccessful()) { mStatusTextView.setText(R.string.auth_failed); } hideProgressDialog(); // [END_EXCLUDE] } }); // [END sign_in_with_email] } private void signOut() { mAuth.signOut(); updateUI(null); } private boolean validateForm() { boolean valid = true; String email = mEmailField.getText().toString(); if (TextUtils.isEmpty(email)) { mEmailField.setError("Required."); valid = false; } else { mEmailField.setError(null); } String password = mPasswordField.getText().toString(); if (TextUtils.isEmpty(password)) { mPasswordField.setError("Required."); valid = false; } else { mPasswordField.setError(null); } return valid; } private void updateUI(FirebaseUser user) { hideProgressDialog(); if (user != null) { mStatusTextView.setText(getString(R.string.emailpassword_status_fmt, user.getEmail())); mDetailTextView.setText(getString(R.string.firebase_status_fmt, user.getUid())); findViewById(R.id.email_password_buttons).setVisibility(View.GONE); findViewById(R.id.email_password_fields).setVisibility(View.GONE); findViewById(R.id.sign_out_button).setVisibility(View.VISIBLE); } else { mStatusTextView.setText(R.string.signed_out); mDetailTextView.setText(null); findViewById(R.id.email_password_buttons).setVisibility(View.VISIBLE); findViewById(R.id.email_password_fields).setVisibility(View.VISIBLE); findViewById(R.id.sign_out_button).setVisibility(View.GONE); } } @Override public void onClick(View v) { int i = v.getId(); if (i == R.id.email_create_account_button) { createAccount(mEmailField.getText().toString(), mPasswordField.getText().toString()); } else if (i == R.id.email_sign_in_button) { signIn(mEmailField.getText().toString(), mPasswordField.getText().toString()); } else if (i == R.id.sign_out_button) { signOut(); } } }
Осталось запустить приложение на устройстве и протестировать его работу. Процесс тестирования подробно смотрите в видео.
На этом все, вот ссылка на документацию. В следующих уроках мы рассмотрим аутентификацию при помощи аккаунта Google, Facebook и Twitter.