Firebase-Authentication

很強大的功能。

簡述

Firebase 基礎 中已經介紹過怎麼使用 Firebase 的 firestore 服務,這篇要來介紹 Authentication,也就是跟登入相關的功能。

附註:這邊一樣會用 firebase@8.2 來舉例,所以記得裝對版本。

題外話

寫完這篇後我才體會什麼叫做 SDK?其實這篇用到的登入登出 method 都是先經過 Firebase 包裝成 SDK 後,我們才可以用很簡單的一個 function 來完成的。

如果沒有被包裝成 SDK 的話,我們可能就要自己用 fetch 或是 XMLHttpRequest 之類的來自己處理,不會這麼輕鬆。

會寫這段只是因為我以前有做一個 筆記,當時還沒有很能體會 SDK 跟 API 到底實際差在哪,但現在突然被打通了,感覺還蠻神奇的。

後端的部分

Firebase 的專案建好後,回到主控台會看到側邊欄有一個「Authentication」的選項,點進去後再選取「get start」就會看到這個畫面:

auth-console

這邊是讓你選你要啟用哪種登入方式,這邊會用「電子郵件」來做舉例,所以就點選後把他啟用。

前端的部分

就和設定 firestore 的方式差不多,只是多了一個 auth 的部分而已:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import firebase from 'firebase/app'
import 'firebase/firestore'
import 'firebase/auth'

const firebaseConfig = {
apiKey: '...',
authDomain: '...',
projectId: '...',
storageBucket: '...',
messagingSenderId: '...',
appId: '...'
}

// init firebase
firebase.initializeApp(firebaseConfig)

// init services
const db = firebase.firestore()
const auth = firebase.auth()

export { db, auth }

關於 Firebase 的驗證原理

這邊只是想做個補充,Firebase 的驗證機制是透過 JWT 來實作的。當使用者登入成功時 Firebase 會產生一個 JWT,接著我們只要在接下來的 request 中都帶上 JWT 就可以達到驗證機制了。

如果你對 JWT 有點好奇的話,推薦參考我寫的這篇:關於 JWT(JSON-Web-Token),裡面有解釋 JWT 是怎麼產生的。

註冊功能

這邊先介紹透過「email」來註冊的流程,會以 custom hook 的方式來撰寫。

先來看段 code 再解釋:

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
import { useState } from 'react'
import { auth } from 'firebase/config'

export function useSignup() {
const [isPending, setIsPending] = useState(false)
const [error, setError] = useState(null)

const signup = async (email, password, displayName) => {
setIsPending(true)
setError(null)
try {
const response = await auth.createUserWithEmailAndPassword(email, password)
if (!response.user) throw new Error('Signup failed.')

// update profile
await response.user.updateProfile({ displayName })
setIsPending(false)
} catch (error) {
setError(error.message)
setIsPending(false)
}
}

return { error, isPending, signup }
}

簡單來說有兩個流程:

  1. createUserWithEmailAndPassword 來建立基本資料
  2. updateProfile 來更新 displayName

因為這兩個東西是拆成兩個 API 來做的,所以才要分兩個動作。

總之,在成功建立以後你可以到 Firebase 的主控台來確認:

auth-email-check

如果要刪除也是從這邊刪,把它當作資料庫就行了。

加上使用者頭像的功能

這邊只是當作補充的懶人包:

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
import { useEffect, useState } from 'react'
import { auth, storage } from 'firebase/config'
import { useAuthContext } from './useAuthContext'

export function useSignup() {
const [isPending, setIsPending] = useState(false)
const [isCancelled, setIsCancelled] = useState(false)
const [error, setError] = useState(null)
const { dispatch } = useAuthContext()

const signup = async (email, password, displayName, thumbnail) => {
setIsPending(true)
setError(null)
try {
// create user
const response = await auth.createUserWithEmailAndPassword(email, password)
if (!response.user) throw new Error('Signup failed.')

// upload thumbnail
const uploadPath = `thumbnail/${response.user.uid}/${thumbnail.name}`
const img = await storage.ref(uploadPath).put(thumbnail)
const imgUrl = await img.ref.getDownloadURL()
// update profile
await response.user.updateProfile({ displayName, photoURL: imgUrl })
// update global state
dispatch({
type: 'LOGIN',
payload: response.user
})
// update state
if (!isCancelled) {
setIsPending(false)
}
} catch (error) {
if (!isCancelled) {
setError(error.message)
setIsPending(false)
}
}
}

useEffect(() => {
return () => setIsCancelled(true)
}, [])

return { error, isPending, signup }
}

簡單來說就是:

  1. 設定要上傳的路徑:/thumbnail/${使用者uid}/${檔案名稱}
  2. 用把圖片上傳到 storage:await img.ref.getDownloadURL()
  3. 用圖片的 reference 取得 URL:await img.ref.getDownloadURL()
  4. 最後把 URL 寫入 Profile 中就行了

登出

其實關鍵只有一行而已,不過這邊一樣寫成 hook 的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { auth } from 'firebase/config'
import { useState } from 'react'
import { useAuthContext } from './useAuthContext'

export const useLogout = () => {
const { dispatch } = useAuthContext()
const [isPending, setIsPending] = useState(false)
const [error, setError] = useState(null)
const logout = async () => {
setIsPending(true)
setError(null)
try {
// 登出的 API
await auth.signOut()
dispatch({ type: 'LOGOUT' })
} catch (error) {
setError(error.message)
setIsPending(false)
}
}
return { logout, isPending, error }
}

登入

一樣關鍵的地方只有一行,這邊用 hook 來表示:

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
import { auth } from 'firebase/config'
import { useEffect, useState } from 'react'
import { useAuthContext } from './useAuthContext'

export const useLogIn = () => {
const { dispatch } = useAuthContext()
const [isPending, setIsPending] = useState(false)
const [isCanceled, setIsCanceled] = useState(false)
const [error, setError] = useState(null)

const login = async (eamil, password) => {
setIsPending(true)
setError(null)
try {
// 登入(帶入 email 和 password)
const response = await auth.signInWithEmailAndPassword(eamil, password)
dispatch({ type: 'LOGIN', payload: response.user })

if (!isCanceled) {
setIsPending(false)
}
} catch (error) {
if (!isCanceled) {
setError(error.message)
setIsPending(false)
}
}
}

useEffect(() => {
return () => setIsCanceled(true)
}, [])

return { login, isPending, error }
}

登入與登出的 Clean function

附註:雖然新的 React 好像有對這個做修正,所以就算不這樣做也沒關係,但我覺得最好還是暸解一下這個觀念比較好。

這只是用來避免下面這種情形:

  1. 我登入時畫面還在 loadnig,但我直接點去別的頁面
  2. 我登出時畫面還在 loading,但我馬上點去別的頁面

這兩種情形都會引發「明明元件已經被撤銷了,但我還試著要去更新 state 的情形(loading 狀態)」,所以這邊一樣要透過 clean function 來做一些事情。

React-這個 fetch 我剛剛要但現在又不要了 中有介紹過怎麼取消 fetch,但我們現在是用 Firebase 提供的 API 來實作,那該怎麼辦才好?

技術上來說我們沒有辦法真的「取消 request」這件事,不過我們可以控制「在 xxx 條件下才准許更新 state」,這也就是這邊的做法,直接來看 code:

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
import { auth } from 'firebase/config'
import { useEffect, useState } from 'react'
import { useAuthContext } from './useAuthContext'

export const useLogout = () => {
const { dispatch } = useAuthContext()
const [isPending, setIsPending] = useState(false)
const [isCanceled, setIsCanceled] = useState(false)
const [error, setError] = useState(null)

const logout = async () => {
setIsPending(true)
setError(null)
try {
await auth.signOut()
dispatch({ type: 'LOGOUT' })
// 如果沒有被取消才更新 state
if (!isCanceled) {
setIsPending(false)
}
} catch (error) {
// 如果沒有被取消才更新 state
if (!isCanceled) {
setError(error.message)
setIsPending(false)
}
}
}

useEffect(() => {
// 更新 flag
return () => setIsCanceled(true)
}, [])

return { logout, isPending, error }
}

改寫成這樣後,就算這個元件被撤銷了也不會再去更新 state,因為當下的條件不成立(isCanceled = true)。

不過這邊你可能會一個疑問是 dispatch 不用放進去判斷嗎?這不是也會更新 state?

記得我一開始說的嗎?我說過我們沒有辦法真的去「取消 request」,也就是說實際上還是把「登出」的 API 給打出去了,所以最後還是會完成「登出」這個動作。

如果這時候我又放進去判斷的話,那就會導致「畫面上顯示已登入,但後端那邊實際上是已登出」的問題,所以才不可以這樣做。

雖然還有另外一個原因是因為 dispatch 更新的 state 是 global 的,所以就算這個元件被撤銷了也沒關係,畢竟有可能會有其他的元件會需要用到它。

登入狀態初始化

前面已經介紹「註冊、登入、登出」這三個功能,接著要介紹的是初始化。

我們在一開始載入頁面時瀏覽器並不會使用者有沒有登入,所以一定是打一隻 API 去問說「使用者現在登入了嗎?」才會知道結果,因此這個動作就是「初始化」。

關於初始化的 API,我原本知道的作法是用 LocalStorage 中的 JWT 去打,但這邊要介紹的是另外一種 Firebase 提供的方法:onAuthStateChanged

簡單來說就是 Auth 版的即時資料,只要前後端某一方發生了登入、登出的行為就會立刻 trigger 這個 function,所以他寫起來就跟在抓 real-time-data 的方式很像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export const authReducer = (state, action) => {
switch (action.type) {
case 'INIT_USER':
return {
...state,
user: action.payload,
isUserInit: true
}
default:
return state
}
}

useEffect(() => {
// callback 會拿到 user 參數(null | object)
const unsubscribe = auth.onAuthStateChanged((user) => {
dispatch({
type: 'INIT_USER',
payload: user
})
unsubscribe()
})
}, [])

這邊做的事情有兩個:

  1. 去看目前 user 的登入狀態,如果有人登入就會拿到那包 Object,沒有的話就會是 null。
  2. 接著用 dispatch 來更新 user 的資料,並且把 isInitUser 設定為已完成。

最後是 unsubscribe,因為我希望這個動作只需要「做一次」就夠了,不需要每次 user 狀態更新時都執行一遍,所以在做完初始化以後就立刻「取消追蹤」。

順道一提,isInitUser 這個 state 是用來處理第一次載入時畫面會「閃一下」的問題。反正就是為了提升 UX 用的 state,你可以在 isInitUser = false 時先顯示載入畫面 or 不要顯示任何東西,等到初始化完後再顯示正確的畫面即可,這邊就不示範了。

JavaScript-關於 JSON 的秘密 Firbase-設定 Rules
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×