简体   繁体   中英

Firebase Auth and Vue-router

I'm trying to authenticate a Vue.js app using firebase.

I have an issue where if trying to access a login-protected URL directly while logged in, the router will load and check for auth state before firebase.js has time to return the auth response. This results in the user being bounced to the login page (while they are already logged in).

How do I delay vue-router navigation until auth state has been retrieved from firebase? I can see that firebase stores the auth data in localStorage, would it be safe to check if that exists as a preliminary authentication check? Ideally the end result would be to show a loading spinner or something while the user is authenticated, then they should be able to access the page they navigated to.

router/index.js

let router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/login',
      name: 'Login',
      component: Login
    },
    {
      path: '/example',
      name: 'Example',
      component: Example,
      beforeEnter: loginRequired
    }
})

function loginRequired (to, from, next) {
  if (authService.authenticated()) {
    next()
  } else {
    next('/login')
  }
}

auth.js

import * as firebase from 'firebase'

var config = {
    // firebase config
}

firebase.initializeApp(config)

var authService = {

  firebase: firebase,
  user: null,

  authenticated () {
    if (this.user == null) {
      return false
    } else {
      return !this.user.isAnonymous
    }
  },

  setUser (user) {
    this.user = user
  },

  login (email, password) {
    return this.firebase.auth().signInWithEmailAndPassword(email, password)
      .then(user => {
        this.setUser(user)
      })
  },

  logout () {
    this.firebase.auth().signOut().then(() => {
      console.log('logout done')
    })
  }
}

firebase.auth().onAuthStateChanged(user => {
  authService.setUser(user)
})

export default authService

app.vue

<template>
  <div id="app">
    <p v-if="auth.user !== null">Logged in with {{ auth.user.email }}</p>
    <p v-else>not logged in</p>
    <router-view v-if="auth.user !== null"></router-view>
  </div>
</template>

<script>
import authService from './auth'

export default {
  name: 'app',
  data () {
    return {
      auth: authService
    }
  }
}
</script>

Firebase always triggers an auth state change event on startup but it's not immediate.

You'll need to make authService.authenticated return a promise in order to wait for Firebase to complete its user/auth initialisation.

const initializeAuth = new Promise(resolve => {
  // this adds a hook for the initial auth-change event
  firebase.auth().onAuthStateChanged(user => {
    authService.setUser(user)
    resolve(user)
  })
})

const authService = {

  user: null,

  authenticated () {
    return initializeAuth.then(user => {
      return user && !user.isAnonymous
    })
  },

  setUser (user) {
    this.user = user
  },

  login (email, password) {
    return firebase.auth().signInWithEmailAndPassword(email, password)
  },

  logout () {
    firebase.auth().signOut().then(() => {
      console.log('logout done')
    })
  }
}

You don't need to call setUser from the signInWith... promise as this will already be handled by the initializeAuth promise.

To build on Richard's answer, for those who are using regular Vue (not Vuex)

main.js

//initialize firebase
firebase.initializeApp(config);
let app: any;
firebase.auth().onAuthStateChanged(async user => {
if (!app) {
    //wait to get user
    var user = await firebase.auth().currentUser;

    //start app
    app = new Vue({
      router,
      created() {
        //redirect if user not logged in
        if (!user) {
          this.$router.push("/login");
        }
      },
      render: h => h(App)
    }).$mount("#app");
  }
});

router.js

//route definitions
//...
router.beforeEach((to, from, next) => {
  const currentUser = firebase.auth().currentUser;
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);

  if (requiresAuth && !currentUser) {
    const loginpath = window.location.pathname;   
    next({ name: 'login', query: { from: loginpath } });
  } else if (!requiresAuth && currentUser) {
    next("defaultView");
  } else {
    next();
  }
});

I just had this same problem and ended up delaying the creation of the Vue object until the first onAuthStatedChanged.

# main.js
// wait for first firebase auth change before setting up vue
import { AUTH_SUCCESS, AUTH_LOGOUT } from "@/store/actions/auth";
import { utils } from "@/store/modules/auth";
let app;
firebase.auth().onAuthStateChanged(async user => {
  if (!app) {
    if (user) {
      await store.dispatch(AUTH_SUCCESS, utils.mapUser(user));
    } else {
      await store.dispatch(AUTH_LOGOUT);
    }
    app = new Vue({
      router,
      store,
      i18n,
      render: h => h(App)
    }).$mount("#app");
  }
});

And then in my route I check as normal and if they end up on login route I just push them to my overview page which is kind of my dashboard page.

#router.js
router.beforeEach((to, from, next) => {
  let authenticated = store.getters.isAuthenticated;
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // this route requires auth, check if logged in
    // if not, redirect to login page.
    if (!authenticated) {
      next({
        name: "Login",
        query: { redirect: to.fullPath }
      });
    } else {
      next();
    }
  } else {
    // doesn't require auth, but if authenticated already and hitting login then go to overview
    if (authenticated && to.name === "Login") {
      next({
        name: "Overview"
      });
    }
    next(); // make sure to always call next()!
  }
});

You have two options:

1) Use beforeRouteEnter from the component:

export default {
        name: "example",
        ....
        beforeRouteEnter(to, from, next){
            if (authService.authenticated()) {
                next()
            } else {
                next('/login')
            }
        },
}

2) use beforeResolve from the router.

router.beforeResolve((to, from, next) => {

    if(to.fullPath === '/example' && !authService.authenticated()){
        next('/login')
    }else{
        next()
    }
})

Life-circle of Vue route guards

To delay the auth state, all you need to do is

  • a) Before the app is mounted, use the firebase auth onAuthStateChanged method.
  • b) In your router file, add a meta to the parent route where you want to add the login protection
  • c) Use a router.beforeEach for the authentication and how you want to redirect the users to.I will provide the examples for each case below: Stage a)
    firebase.auth().onAuthStateChanged(function(user) {
        console.log(user)
        new Vue({
            router,
            render: h => h(App),
          }).$mount('#app')
        }
    });

Stage b)

    ....
    ....
    ....
    {
        path: "/dashboard",
        name: "dashboard",
        component: Dashboard,
        meta: { requiresAuth: true },//Add this
        children: [
            {
                path: "products",
                name: "products",
                component: Products,
            },
        ],
    ....
    ....
    ....

Stage c)

    router.beforeEach((to, from, next) => {
        const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
        const currentUser = firebase.auth().currentUser
        if(requiresAuth && !currentUser) {
            next("/")
        } else if(requiresAuth && currentUser) {
            next()
        }else{
            next()
        }
    })

I believe you're good to go this way.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM