Vue SPA Frontend Authentication

Required Tools⌗
- Vue Cli 3.*
- Node
Installing Required Tools⌗
Installing Node Environment⌗
Go to Node official website to download
Installing Vue Cli⌗
npm install -g @vue/cli
# OR
yarn global add @vue/cli
After installation, you need to
Creating a Project⌗
vue create YOUR_PROJECT_NAME
# OR
vue ui
George-2:Node George$ vue create spa
Vue CLI v3.8.2
┌───────────────────────────┐
│ Update available: 3.8.4 │
└───────────────────────────┘
? Please pick a preset: default (babel, eslint)
Vue CLI v3.8.2
✨ Creating project in /Users/George/Develop/Node/spa.
🗃 Initializing git repository...
⚙ Installing CLI plugins. This might take a while...
yarn install v1.13.0
info No lockfile found.
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
✨ Done in 207.89s.
🚀 Invoking generators...
📦 Installing additional dependencies...
yarn install v1.13.0
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
✨ Done in 53.93s.
⚓ Running completion hooks...
📄 Generating README.md...
🎉 Successfully created project spa.
👉 Get started with the following commands:
$ cd spa
$ yarn serve
George-2:Node George$
Installing Dependencies⌗
vue add router
vue add vuex
vue add axios
The above commands add vue-router, vuex, and axios dependencies to the project.
Project Implementation⌗
User State Management⌗
Since users can have two states, logged in or not logged in, how does the frontend store the user’s login state? Here we can use Vuex for storage. But Vuex alone is not enough, because when the user manually refreshes the browser, Vue’s entire lifecycle is reloaded. So we also need to use the browser’s Local Storage
or Session Storage
to ensure that the user state is not lost after refreshing.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
state: {
access_token: localStorage.getItem('access_token'),
},
mutations: {
SET_ACCESS_TOKEN: (state, data) => {
if(data === false) {
Vue.set(state, 'access_token', '');
localStorage.removeItem('access_token');
} else {
Vue.set(state, 'access_token', data.access_token);
localStorage.setItem('access_token', data.access_token);
}
}
},
actions: {
signIn({commit}, params) {
return new Promise((resolve,reject) => {
Vue.axios.post('/signin', params).then(res => {
commit('SET_ACCESS_TOKEN', res.data);
resolve(res);
}).catch(err => {
reject(err);
})
});
}
}
});
System State Management⌗
Saving Layout
information globally
import Vue from 'vue'
import api from '../../apis'
import * as types from '../types'
export default {
state: {
layout: {
current: "guest"
},
menu: {
active: "/"
},
installed: false,
version: ""
},
mutations: {
SET_SYSTEM: (state, data) => {
Vue.set(state, 'installed', data.installed);
Vue.set(state, 'version', data.version);
},
SET_MENU_ACTIVE: (state, data) => {
Vue.set(state.menu, 'active', data)
},
SET_LAYOUT_CURRENT: (state, data) => {
Vue.set(state.layout, 'current', data)
}
},
actions: {
fetchSystem({ commit }) {
api.system.fetch().then(response => {
commit(types.SET_SYSTEM, response.data)
});
}
}
}
Request 401 Interception⌗
"use strict";
import Vue from 'vue';
import axios from 'axios'
import store from '../store'
import router from '../router'
// Full config: https://github.com/axios/axios#request-config
// axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || '';
// axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
// axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
let config = {
baseURL: process.env.baseURL || process.env.apiUrl || "",
timeout: 60 * 1000, // Timeout
withCredentials: true, // Check cross-site Access-Control
};
const request = axios.create(config);
// Define request interceptor
request.interceptors.request.use(config => {
// Iterate through request parameters
for(let key in config.params) {
// If the parameter value is empty, delete this key
if (config.params.hasOwnProperty(key) && (config.params[key] === "" || config.params[key] === null)) {
delete config.params[key];
}
}
// If the user's access token exists, add it to the request header
if (store.state.account.access_token) {
config.headers.Authorization = `Bearer ${store.state.account.access_token}`;
}
return config;
}, error => {
return Promise.reject(error);
});
// Define response interceptor
request.interceptors.response.use(
response => {
return response.data;
},
error => {
// If the status code is 401, clear the state in Vuex and redirect to the login page
if (error.response.status === 401) {
store.commit('SET_PROFILE', false);
store.commit('SET_ACCESS_TOKEN', false);
router.push("/signin");
}
return Promise.reject(error.response);
}
);
Plugin.install = function(Vue) {
Vue.axios = request;
window.axios = request;
Object.defineProperties(Vue.prototype, {
axios: {
get() {
return request;
}
},
$axios: {
get() {
return request;
}
},
});
};
Vue.use(Plugin);
export default Plugin;
Frontend Route Interception⌗
Implementing API 401 status code interception alone is not enough; we also need to authenticate the frontend routes.
import Vue from 'vue'
import store from '../store'
import Router from 'vue-router'
Vue.use(Router);
const router = new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'dashboard',
meta: {
requiresAuth: true
},
component: () => import('../views/Dashboard.vue'),
},
{
path: '/signin',
name: 'signin',
meta: {
requiresAuth: false
},
component: () => import('../views/SignIn.vue')
},
{
// Will match all paths
path: '*',
name: "notfound",
meta: {
requiresAuth: false
},
component: () => import('../views/NotFound.vue')
}
]
});
/**
* Routing to intercept
*/
router.beforeEach((to, from, next) => {
// If redirecting to NotFound page, set the view's Layout to guest in advance
if (to.name === 'notfound') {
store.commit('SET_LAYOUT_CURRENT', 'guest');
}
// If returning from NotFound page and authentication is required, set the view's Layout to backend
if (from.name === 'notfound' && to.meta.requiresAuth === true) {
store.commit('SET_LAYOUT_CURRENT', 'backend')
}
// When the user's access token exists and is valid, and the route is the login page, redirect directly to the homepage
if (store.state.account.access_token && to.path === '/signin') {
next({
path: "/"
})
}
/**
* Main logic for route authentication
*/
if (to.matched.some(record => record.meta.requiresAuth)) {
const access_token = store.state.account.access_token;
// Determine if the user is logged in through access_token
if (!access_token) {
next({
path: '/signin'
})
} else {
next()
}
} else {
next()
}
});
export default router;
Page Layout⌗
Since the layouts of the login page
and backend page
are different and cannot be reused, to avoid re-rendering common modules such as menus, top navigation bars, and other components after login and route navigation, we need to implement logic in App.vue
to dynamically switch page layouts based on the user’s login status.
<template>
<div id="app">
<component :is="layout.current"></component>
</div>
</template>
<script>
import {mapState} from 'vuex'
import Guest from './layouts/Guest'
import Backend from './layouts/Backend'
export default {
name: 'app',
data() {
return {
}
},
methods: {},
components: {
guest: Guest,
backend: Backend
},
computed: {
...mapState({
layout: state => state.system.layout,
profile: state => state.account.profile,
access_token: state => state.account.access_token
})
},
watch: {
// Watch for user information changes to switch page layouts
profile(value) {
if (value.id === '') {
this.$store.commit('SET_LAYOUT_CURRENT', 'guest');
} else {
this.$store.commit('SET_LAYOUT_CURRENT', 'backend');
}
},
/**
* When the access_token state changes
* it means the user login information verification is successful and the access_token has been obtained
* at this point, call the fetchProfile action to get user information
*/
access_token(value) {
if (value == null) {
window.console.log(value);
}
},
'$route' (to, from) {
if (from.path === '/' && to.name === 'notfound') {
this.$store.commit('SET_LAYOUT_CURRENT', 'guest');
}
if (from.path === '/') {
if (to.name !== 'notfound') {
this.$store.commit('SET_LAYOUT_CURRENT', 'backend');
}
}
if (to.name === 'signin') {
this.$store.commit('SET_LAYOUT_CURRENT', 'guest');
}
if (from.name === 'notfound') {
this.$store.commit('SET_MENU_ACTIVE', to.path);
}
this.$store.commit('SET_MENU_ACTIVE', to.path);
}
}
}
</script>
<style lang="scss">
</style>
At this point, the frontend logic related to user authentication has been implemented. All that remains is to focus on writing business logic!
I hope this is helpful, Happy hacking…