Vue SPA 前端鉴权
![Cover Image Vue SPA 前端鉴权](https://george.betterde.com/covers/20190618-vue-spa.jpg)
所需工具⌗
- Vue Cli 3.*
- Node
安装所需工具⌗
安装 Node 环境⌗
前往 Node 官网下载
安装 Vue Cli⌗
npm install -g @vue/cli
# OR
yarn global add @vue/cli
安装完成后,需要到
创建项目⌗
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$
安装依赖⌗
vue add router
vue add vuex
vue add axios
上面分别为项目添加了 vue-router、vuex、axios 依赖。
项目实现⌗
用户状态管理⌗
因为用户可能有两种状态,一种是已登陆一种是未登陆,那么前端如何来保存用户登陆状态呢?这里可以使用 Vuex 来进行存储。但是只有 Vuex 是不够的,因为当用户手动刷新浏览器后,Vue 整个生命周期就重新加载了,所以还要配合浏览器提供的 Local Storage
或 Session Storage
来保证用户刷新而不丢失用户状态。
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);
})
});
}
}
});
系统状态管理⌗
在全局保存 Layout
的信息
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)
});
}
}
}
请求401拦截⌗
"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);
// 定义请求拦截器
request.interceptors.request.use(config => {
// 遍历请求参数
for(let key in config.params) {
// 如果参数的值为空,则删除这个 Key
if (config.params.hasOwnProperty(key) && (config.params[key] === "" || config.params[key] === null)) {
delete config.params[key];
}
}
// 如果有用户的 access token 则在请求头中加上
if (store.state.account.access_token) {
config.headers.Authorization = `Bearer ${store.state.account.access_token}`;
}
return config;
}, error => {
return Promise.reject(error);
});
// 定义响应拦截器
request.interceptors.response.use(
response => {
return response.data;
},
error => {
// 如果状态码为 401 则清空 Vuex 中的状态,并跳转到登陆页面
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;
前端路由拦截⌗
仅仅实现了API 401状态码拦截还是不够的,还需要对前端路由做响应的鉴权。
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')
},
{
// 会匹配所有路径
path: '*',
name: "notfound",
meta: {
requiresAuth: false
},
component: () => import('../views/NotFound.vue')
}
]
});
/**
* Routing to intercept
*/
router.beforeEach((to, from, next) => {
// 如果跳转到NotFound页面则提前设置视图的Layout 为 guest
if (to.name === 'notfound') {
store.commit('SET_LAYOUT_CURRENT', 'guest');
}
// 如果从NotFound 页面返回,并且需要认证的话,则设置视图的Layout 为 backend
if (from.name === 'notfound' && to.meta.requiresAuth === true) {
store.commit('SET_LAYOUT_CURRENT', 'backend')
}
// 当用户的 access token 存在且有效,前往的路由是 登陆页面时,直接跳转到首页
if (store.state.account.access_token && to.path === '/signin') {
next({
path: "/"
})
}
/**
* 路由鉴权主要逻辑
*/
if (to.matched.some(record => record.meta.requiresAuth)) {
const access_token = store.state.account.access_token;
//通过access_token判断用户是否已经登录
if (!access_token) {
next({
path: '/signin'
})
} else {
next()
}
} else {
next()
}
});
export default router;
页面布局⌗
由于登陆页面
和后台页面
的布局不同所以没法服用,为了在登陆后跳转路由不重新渲染公共模块,如菜单、顶部导航条等组件,所以需要在 App.vue
中实现根据用户登陆状态动态切换页面布局的逻辑。
<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 用户信息变更,切换页面布局
profile(value) {
if (value.id === '') {
this.$store.commit('SET_LAYOUT_CURRENT', 'guest');
} else {
this.$store.commit('SET_LAYOUT_CURRENT', 'backend');
}
},
/**
* 当access_token的state发生变化时
* 说明用户登录信息验证成功,并且拿到了access_token
* 此时调用fetchProfile 这个action 获取用户信息
*/
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>
到此基本上前端有关用户鉴权的逻辑都已经实现,剩下的只要专心的去写业务逻辑即可!
I hope this is helpful, Happy hacking…