Firebaseを使ったGoogleアカウントのユーザー認証についてまとめました。認証はFirebase Authentication、画像のストレージはFirebase Storage、ユーザ情報はFirebase Firestoreを使っています。
目次
事前準備
認証をする前にfirebase.jsに以下の設定を書いて、Firebaseプロジェクトと連携しておきます。
・firebase.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import firebase from 'firebase'; // Firebaseプロジェクトの設定 if (!firebase.apps.length) { firebase.initializeApp({ apiKey: '{ウェブAPIキー}', authDomain: '{プロジェクトID}.firebaseapp.com', databaseURL: 'https://{プロジェクトID}.firebaseio.com', projectId: '{プロジェクトID}', storageBucket: '{プロジェクトID}.appspot.com', messagingSenderId: '1234567890' // cloudmessagingを使う場合は設定 }) } export const firestore = firebase.firestore(); export const storage = firebase.storage(); |
Google認証全体像
Firebase Authenticationを使ったGoogle認証の全体像は以下のようになります。こちらのコードは以下を参考にしています。
参考:https://github.com/FujiyamaYuta/nuxt-firebase-project
それぞれの処理をより深く理解するために、1つ1つの処理を追って自分の言葉で解説していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | <template> <b-modal :active.sync="isLoginModalActive" :width="420" scroll="keep"> <div class="card"> <div class="card-content"> <div class="media"> <div class="media-left"></div> <div class="media-content"> <p class="title is-4">新規登録/ログイン</p> </div> </div> <div class="content"> <button class="button sosial-button is-medium is-info is-fullwidth" style="background-color:white;color:red;border:2px solid; border-color:red;" @click="google" > <span class="icon"> <i class="fab fa-google"></i> <span>Google</span> </span> </button> </div> </div> </div> </b-modal> </template> <script> import 'bulma/css/bulma.min.css' import '@fortawesome/fontawesome-free/css/all.min.css' import 'bulma-social/bin/bulma-social.min.css' import firebase from 'firebase' import { firestore, storage } from '~/plugins/firebase.js' export default { data() { return { isLoginModalActive: false } }, methods: { login() { this.isLoginModalActive = true }, // Google認証を行う関数 google() { // ② Google認証 const auth = () => { return new Promise((resolve, reject) => { const authUI = new firebase.auth.GoogleAuthProvider() firebase .auth() .signInWithPopup(authUI) .then((result) => { resolve(result) }) .catch((error) => { const errorCode = error.code const errorMessage = error.message const email = error.email const credential = error.credential reject(error) }) }) } // ③ 認証後のユーザー情報を取得してオブジェクト化 const getAccountData = (result) => { return new Promise((resolve, reject) => { let userObject = {} let user = result.user userObject.token = result.credential.accessToken userObject.refreshToken = user.refreshToken userObject.uid = user.uid userObject.displayName = user.displayName userObject.photoURL = user.photoURL userObject.uid = user.uid userObject.email = user.email userObject.isNewUser = result.additionalUserInfo.isNewUser userObject.providerId = result.additionalUserInfo.providerId resolve(userObject) }) } // 同期的に順番に処理を実行する Promise.resolve() .then(this.setPersistence) .then(auth) .then(getAccountData) .then((userObject) => this.createPhotoURL(userObject)) .then((userObject) => this.setPublicUserData(userObject)) .then((userObject) => this.setPrivateUserData(userObject)) .then((userObject) => this.setLocalUserData(userObject)) .catch((error) => this.onRejectted(error)) }, onRejectted(error) { this.$buefy.toast.open({ duration: 5000, message: `ログインに失敗しました。`, position: 'is-bottom', type: 'is-danger' }) this.isLoginModalActive = false }, createPublicObj(obj) { let publicObj = {} publicObj.uid = obj.uid publicObj.providerId = obj.providerId publicObj.isNewUser = obj.isNewUser publicObj.photoURL = obj.photoURL publicObj.displayName = obj.displayName return publicObj }, createPrivateObj(obj) { let privateObj = {} privateObj.uid = obj.uid privateObj.providerId = obj.providerId privateObj.isNewUser = obj.isNewUser privateObj.email = obj.email privateObj.token = obj.token privateObj.refreshToken = obj.refreshToken return privateObj }, // ① 認証状態を明示的にセットする setPersistence() { return new Promise((resolve, reject) => { firebase .auth() .setPersistence(firebase.auth.Auth.Persistence.LOCAL) .then((result) => { resolve() }) }) }, // ⑤ 公開可能なユーザー情報をFirestoreに登録 setPublicUserData(userObject) { return new Promise((resolve, reject) => { let publicUser = firestore.collection('users').doc(userObject.uid) // ** usersに登録するObjのみを登録する publicUser .set(this.createPublicObj(userObject), { merge: true }) .then((result) => { resolve(userObject) }) }) }, // ⑥ 非公開のユーザー情報をFirestoreに登録 setPrivateUserData(userObject) { return new Promise((resolve, reject) => { let privateUsers = firestore .collection('privateUsers') .doc(userObject.uid) // privateUsersに登録するObjのみを登録する privateUsers .set(this.createPrivateObj(userObject), { merge: true }) .then((result) => { resolve(userObject) }) }) }, // ⑦ ローカルストレージに保持するユーザー情報を設定 setLocalUserData(userObject) { return new Promise((resolve, reject) => { let user = firestore.collection('users').doc(userObject.uid) user .get() .then((doc) => { if (doc.exists) { localStorage.setItem('photoURL', doc.data().photoURL) localStorage.setItem('uid', userObject.uid) localStorage.setItem('token', userObject.token) localStorage.setItem('displayName', doc.data().displayName) this.$buefy.toast.open({ duration: 5000, message: `ログインに成功しました`, position: 'is-bottom', type: 'is-success' }) this.isLoginModalActive = false location.reload() resolve(userObject) } }) .catch((error) => { console.log('ログインに失敗しました。Error getting document:', error) }) }) }, // ④ 取得したアイコンのURLをFirestorageに保存して、そのURLをFirestoreに登録する準備 createPhotoURL(userObject) { return new Promise((resolve, reject) => { let url = userObject.photoURL let xhr = new XMLHttpRequest() xhr.responseType = 'blob' xhr.onload = function(event) { let blob = xhr.response let storageRef = storage.ref() let mountainsRef = storageRef.child( `user/${userObject.uid}/image.jpg` ) let uploadTask = mountainsRef.put(blob) uploadTask.then((snapshot) => { uploadTask.snapshot.ref.getDownloadURL().then((downloadURL) => { console.log(downloadURL) // firestorageに登録したURLを登録するオブジェクトに代入 userObject.photoURL = downloadURL resolve(userObject) }) }) } // メッセージを受け取った後に返信する xhr.open('GET', url) xhr.send() }) } } } </script> <style> .sosial-button { margin-top: 1rem; } </style> |
ユーザー認証の処理を順番に実行
メソッドチェーンで順番に処理を実行していきます。
then内のいずれかでエラー(reject)が発生した場合にonRejecttedメソッドが呼び出されます。
Promise.resolve() メソッドは、引数の値でPromiseオブジェクトを返します。その値がプロミスであった場合は、そのプロミスが返されます。その値がthenable (すなわちthenメソッドを持っている場合) であれば、返されるプロミスは thenable を追跡し、その最終的な状態になります。
今回の場合は返り値がなくその後の処理もないため、①から⑦まで順番に実行して終了します。
1 2 3 4 5 6 7 8 9 10 | // 順番に処理を実行する Promise.resolve() .then(this.setPersistence) // ① .then(auth) // ② .then(getAccountData) // ③ .then((userObject) => this.createPhotoURL(userObject)) // ④ .then((userObject) => this.setPublicUserData(userObject)) // ⑤ .then((userObject) => this.setPrivateUserData(userObject)) // ⑥ .then((userObject) => this.setLocalUserData(userObject)) // ⑦ .catch((error) => this.onRejectted(error)) |
① 認証状態の永続性をセット
setPersistenceメソッドで認証状態の永続性をセットします。
認証状態の永続性のタイプは「local」、「session」、「none」の3つがあります。詳しい説明などはこちらの公式サイトを参照ください。
認証状態の永続性は、firebase.auth().setPersistence メソッドを呼び出すことによって指定または変更できます。
1 2 3 4 5 6 7 8 9 10 11 | // ① 認証状態の永続性をセットする setPersistence() { return new Promise((resolve, reject) => { firebase .auth() .setPersistence(firebase.auth.Auth.Persistence.LOCAL) .then((result) => { resolve() }) }) }, |
② Google認証
Googleの認証を行います。
firebase.auth.GoogleAuthProvider()メソッドでGoogle認証のプロバイドを呼び出します。それを引数に firebase.auth().signInWithPopup()メソッドで認証画面のポップアップを表示します。
Google認証完了後、認証の結果のプロミスを返します。エラーコードやエラーメッセージなどはcatch内の方法で取得できるため、Google認証に失敗した時のエラーハンドリングで使うことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // ② Google認証 const auth = () => { return new Promise((resolve, reject) => { const authUI = new firebase.auth.GoogleAuthProvider() firebase .auth() .signInWithPopup(authUI) .then((result) => { resolve(result) }) .catch((error) => { const errorCode = error.code const errorMessage = error.message const email = error.email const credential = error.credential reject(error) }) }) } |
③ ユーザー情報を取得し、オブジェクト化
Google認証の結果を元にユーザー情報を取得して、オブジェクト化します。オブジェクト化した結果のプロミスを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // ③ 認証後のユーザー情報を取得してオブジェクト化 const getAccountData = (result) => { return new Promise((resolve, reject) => { let userObject = {} let user = result.user userObject.token = result.credential.accessToken userObject.refreshToken = user.refreshToken userObject.uid = user.uid userObject.displayName = user.displayName userObject.photoURL = user.photoURL userObject.uid = user.uid userObject.email = user.email userObject.isNewUser = result.additionalUserInfo.isNewUser userObject.providerId = result.additionalUserInfo.providerId resolve(userObject) }) } |
④ アイコン画像をFirestorageに保存し、そのパスを取得
GoogleのアイコンのURLからXMLHttpRequestでアイコン画像を取得します。
xhr.responseTypeでレスポンスの方をblobに定義しています。let blob = xhr.responseで取得した画像のblobオブジェクトをblobに渡しています。
その後、Storageのuser/${userObject.uid}/image.jpgパスを用意し、そこにアイコン画像データを保存します。
getDownloadURL()メソッドで、保存したアイコン画像のURLパスを取得し、userObject.photoURLに渡しています。
つまりGoogle認証時に取得したGoogle内に保持しているアイコン画像のパスからXMLHttpRequestでアイコン画像をblobタイプで取得し、それをFirestorageに保存し、保存したURLのパスを取得しFirestoreに保存するために再度userObject.photoURLにパスを渡しているといった処理になります。
xhr.send()の処理は、「Unchecked runtime.lastError: The message port closed before a response was received.」のエラーを回避するため、特に何もなくても返信しています。
参考:https://qiita.com/noenture/items/3978f638f2ffb8ff0995
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // ④ 取得したアイコンのURLをFirestorageに保存して、そのURLをFirestoreに登録する準備 createPhotoURL(userObject) { return new Promise((resolve, reject) => { let url = userObject.photoURL let xhr = new XMLHttpRequest() xhr.responseType = 'blob' xhr.onload = function(event) { let blob = xhr.response let storageRef = storage.ref() let mountainsRef = storageRef.child( `user/${userObject.uid}/image.jpg` ) let uploadTask = mountainsRef.put(blob) uploadTask.then((snapshot) => { uploadTask.snapshot.ref.getDownloadURL().then((downloadURL) => { // Firestorageに登録したURLを登録するオブジェクトに代入 userObject.photoURL = downloadURL resolve(userObject) }) }) } // メッセージを受け取った後に返信する xhr.open('GET', url) xhr.send() }) } |
⑤ 公開可能なユーザー情報をFirestoreに登録
ユーザー情報の中でも公開可能な情報をFirestoreに登録します。createPublicObjメソッドを呼び出しpublicObjに公開可能な情報をセットし、Firestoreのusersコレクション内にuidをkeyにして登録しています。
mergeオプションをつけるとフィールドの追加をしています。
1 2 3 4 5 6 7 8 9 10 11 12 | // ⑤ 公開可能なユーザー情報をFirestoreに登録 setPublicUserData(userObject) { return new Promise((resolve, reject) => { let publicUser = firestore.collection('users').doc(userObject.uid) // usersに登録するObjのみを登録する publicUser .set(this.createPublicObj(userObject), { merge: true }) .then((result) => { resolve(userObject) }) }) }, |
1 2 3 4 5 6 7 8 9 10 | createPublicObj(obj) { let publicObj = {} publicObj.uid = obj.uid publicObj.providerId = obj.providerId publicObj.isNewUser = obj.isNewUser publicObj.photoURL = obj.photoURL publicObj.displayName = obj.displayName return publicObj }, |
⑥ 非公開なユーザー情報をFirestoreに登録
公開可能のユーザ情報登録と同じように、非公開のユーザ情報も登録していきます。非公開情報ではメールやトークンが登録されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // ⑥ 非公開のユーザー情報をFirestoreに登録 setPrivateUserData(userObject) { return new Promise((resolve, reject) => { let privateUsers = firestore .collection('privateUsers') .doc(userObject.uid) // privateUsersに登録するObjのみを登録する privateUsers .set(this.createPrivateObj(userObject), { merge: true }) .then((result) => { resolve(userObject) }) }) }, |
1 2 3 4 5 6 7 8 9 10 | createPrivateObj(obj) { let privateObj = {} privateObj.uid = obj.uid privateObj.providerId = obj.providerId privateObj.isNewUser = obj.isNewUser privateObj.email = obj.email privateObj.token = obj.token privateObj.refreshToken = obj.refreshToken return privateObj }, |
⑦ ローカルストレージに保持するユーザー情報をセット
Firestoreに保存した公開可能なユーザ情報から「アイコン画像パス」、「uid」、「トークン」、「表示名」を取得し、Local Storageにセットして、認証完了です。
ユーザーがログイン状態になり、更新情報を画面に反映させるためにリロードしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // ⑦ ローカルストレージに保持するユーザー情報を設定 setLocalUserData(userObject) { return new Promise((resolve, reject) => { let user = firestore.collection('users').doc(userObject.uid) user .get() .then((doc) => { if (doc.exists) { localStorage.setItem('photoURL', doc.data().photoURL) localStorage.setItem('uid', userObject.uid) localStorage.setItem('token', userObject.token) localStorage.setItem('displayName', doc.data().displayName) this.$buefy.toast.open({ duration: 5000, message: `ログインに成功しました`, position: 'is-bottom', type: 'is-success' }) this.isLoginModalActive = false location.reload() resolve(userObject) } }) .catch((error) => { console.log(error) }) }) }, |
エラーハンドリング
ユーザー認証のメソッドチェーン内でエラーが発生した場合は、catch内のonRejecttedメソッドが呼び出され、ログインに失敗するようハンドリングしています。
1 2 3 4 5 6 7 8 9 | onRejectted(error) { this.$buefy.toast.open({ duration: 5000, message: `ログインに失敗しました。`, position: 'is-bottom', type: 'is-danger' }) this.isLoginModalActive = false }, |
参考:
https://github.com/FujiyamaYuta/nuxt-firebase-project
https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest
https://qiita.com/ueokande/items/807a6c9a64c3874a0f83
https://qiita.com/toshihirock/items/e49b66f8685a8510bd76
https://qiita.com/noenture/items/3978f638f2ffb8ff0995