Firebase 入門(3) ~ セキュリティの向上
Authentication
Database ルール修正
今のままだと、データベースがどのユーザーでもアクセスできてしまうので、アクセス制限をする。まず、Database のルールを以下のように変更。内容としては、認証されたuidと同じ場合のみアクセスを許可している。
{ "rules": { "users": { "$uid": { ".read": "$uid === auth.uid", ".write": "$uid === auth.uid" } } } }
Firebase Authentication
Firebase には認証を管理する Authentication というサービスがある。それを使ってユーザーを認証し、uidを取得し、データベースにアクセスできるようにする。今回は Authentication を有効にして、「ログイン方法」の「メール/パスワード」のみを有効にした。
firebaseui
Firebase はデフォルトでログインフォームUIを提供している。それが firebaseui だ。まずは npmモジュールをインストールする。
$ npm i firebaseui --save
firebaseui 自体の使い方は簡単で、FirebaseUI インスタンスを Auth インスタンスで初期化して、インスタンスを設定オブジェクトでスタートしてやるだけでいい。
import firebase from 'firebase/app'; import 'firebase/database'; import 'firebase/auth'; import firebaseui from 'firebaseui' const config = { apiKey: "<API_KEY>", authDomain: "<PROJECT_ID>.firebaseapp.com", databaseURL: "https://<DATABASE_NAME>.firebaseio.com", projectId: "<PROJECT_ID>", storageBucket: "<BUCKET>.appspot.com", messagingSenderId: "<SENDER_ID>", }; firebase.initializeApp(config); var uiConfig = { callbacks: { signInSuccessWithAuthResult: function(authResult, redirectUrl) { return true; }, uiShown: function() { document.getElementById('loading').style.display = 'none'; } }, signInFlow: 'popup', signInSuccessUrl: '/', signInOptions: [ firebase.auth.EmailAuthProvider.PROVIDER_ID ], tosUrl: '/term_of_service', privacyPolicyUrl: '/privacy_policy' }; var ui = new firebaseui.auth.AuthUI(firebase.auth()); ui.start('#firebaseui-auth-container', uiConfig);
ui.start()
の第一引数で指定したIDの要素にログインフォームが表示される。
React で使う
React で使うなら、以下のような感じだろうか? LoginUI 関数の中が本体。多重読み込みを防ぐために、useEffect の 第二引数にダミーのstateを突っ込んでいるのがちょっとダサい。あと、この後データベースとの連携もあるので useReducer を使ってアクションを作っているが、なくても動く。
import * as React from 'react'; const { useState, useEffect, createContext, useReducer, useContext } = React; import firebase from "firebase/app"; import 'firebase/database'; import 'firebase/auth'; import firebaseui from 'firebaseui'; const DispatchContext = createContext(() => {}); const reducer = (state, action) => { switch(action.type) { case 'change_user': if(state.uid === action.payload.uid) { return state } return { username: action.payload.username, uid: action.payload.uid } case 'change_username': if(state.username === action.payload.username) { return state } return { username: action.payload.username, uid: state.uid } } }; const LoginUI = () => { const dispatch = useContext(DispatchContext); const [temp, _] = useState(0); useEffect(() => { const deffer = () => {} firebase.auth().onAuthStateChanged(function(user) { if(firebase.auth().currentUser !== null) { dispatch({type: 'change_user', payload: { username: user.displayName, uid: user.uid }}) return deffer } var uiConfig = { callbacks: { signInSuccessWithAuthResult: function(authResult, redirectUrl) { return true; }, uiShown: function() { // document.getElementById('loading').style.display = 'none'; } }, signInFlow: 'popup', signInSuccessUrl: '/', signInOptions: [ firebase.auth.EmailAuthProvider.PROVIDER_ID ], tosUrl: '/term_of_service', privacyPolicyUrl: '/privacy_policy' }; var ui = new firebaseui.auth.AuthUI(firebase.auth()); ui.start('#firebaseui-auth-container', uiConfig); return deffer }); }, [temp]); return ( <> <div id="firebaseui-auth-container"></div> </> ); } export default () => { const [state, dispatch] = useReducer(reducer, { username: "", uid: "", }); const config = { apiKey: "<API_KEY>", authDomain: "<PROJECT_ID>.firebaseapp.com", databaseURL: "https://<DATABASE_NAME>.firebaseio.com", projectId: "<PROJECT_ID>", storageBucket: "<BUCKET>.appspot.com", messagingSenderId: "<SENDER_ID>", }; if (!firebase.apps.length) { firebase.initializeApp(config); } return ( <> <div>{state.username}</div> <DispatchContext.Provider value={dispatch}> <LoginUI /> </DispatchContext.Provider> </> ); };
Database と連携
App コンポーネントがレンダリングされたタイミングでデータベースにアクセスしてユーザー情報を取得するようなものを作成した。先ほどの例の default 関数に useEffect Hook で実装する。users/<uid>
のパスにアクセスして何も取得できない場合は、stateの情報をもとに箱を作成する。state が更新されるたびにデータベースにアクセスしにいかないように、state.uidが更新されたタイミングでアクセスするようにもした。
useEffect(() => { if(state.uid =="") { return () => {} } database.ref('users/' + state.uid).once('value').then((snapshot) => { if(snapshot.val() === null) { database.ref('users/' + state.uid).set({ username: state.username }); dispatch({type: 'change_username', payload: { username: state.username}}) } else { const newUsername = snapshot.val().username; dispatch({type: 'change_username', payload: { username: newUsername}}) } }) }, [state.uid])
おまけ (Signout ボタン)
デバッグで必要だったのでSignoutボタンをつけた。
export default 関数に Logout コンポーネントを追加して、Logoutコンポーネント側でボタンを作って onClickイベント関数の中で firebase.auth().signOut() メソッドを読んでいるだけ。
const LogoutUI = () => { const signOut = () => { firebase.auth().signOut().then(function() { console.log("signed out") }).catch(function(error) { console.log(error) }); } return ( <button onClick={signOut}>Sign out</button> ) } export default () => { // 省略 return ( <> <div>{state.username}</div> <DispatchContext.Provider value={dispatch}> <LoginUI /> <LogoutUI /> </DispatchContext.Provider> </> ); }