vue 实现用户登录验证+权限管理+动态路由

vue 实现用户登录验证+权限管理+动态路由

近期要实现一个公安偷窃案件管理系统的登陆功能,搜遍网上发现没有能直接copy的代码,只好自己摸索踩坑,好死不死终于脱坑,这边把实现过程尽可能详细复现一遍,就不展开原理说明(毕竟我也不会),以下包含用户登录验证+权限管理+动态路由等功能。

一、技术栈

vue+vuex+vue-router+ant-design-vue+axios+mockjs2

二、新建项目

前提条件:在个人电脑上安装好nodejs(我的是v12.18.3)之后,利用nodejs自带的npm安装好vue(我的是vue/cli 4.5.4),再在命令行中通过以下指令在指定目录下按装脚手vue-cli

npm install -g @vue/cli-init

新建自定义项目

vue init webpack your-project

由于代码检查、测试之类的功能(ESlint, unit tests, e2e tests)目前对我还不是必须的,所以我都没有选择安装,整个项目的配置项如下:

vue 实现用户登录验证+权限管理+动态路由_第1张图片

在开始Coding前,在项目根目录下好项目的依赖包:

cd your-project
npm i ant-design-vue axios lodash mockjs2 nprogress vuex store

为避免版本问题,把我个人包安装之后的package.json中的dependencies贴出来:

"dependencies": {
    "ant-design-vue": "^1.6.5",
    "axios": "^0.20.0",
    "core-js": "^3.6.5",
    "lodash": "^4.17.20",
    "mockjs2": "^1.0.8",
    "nprogress": "^0.2.0",
    "store": "^2.0.12",
    "vue": "^2.6.11",
    "vue-router": "^3.2.0",
    "vuex": "^3.4.0"
  },

三、功能实现

先讲思路,让大伙有个大概的印象,不至于看代码云里雾里。

  1. 我们将储存在将storage中的token作为用户是否登录的标志,如果当前storage中有token,表明当前系统已被登录
  2. 将系统所有页面分为两类,需要登录的Home.vue,不需要登录的Login.vue
  3. 前端每次跳转路由时,做以下判断:
    • 前往登录页面时
      • 如果已登录过,需要先注销退出并且清除token再跳转
      • 如果没有登录,直接跳转
        • 验证用户输入的用户名和密码,如果成功,往storage中填入token
    • 前往非登录页面时
      • 用户无需登录就能访问的页面(如注册、找回密码、访客模式等情况)
        • 直接跳转
      • 用户需要登录才能访问的页面
        • 如果用户还没登陆
          • 跳向登录页
        • 如果用户已经登陆
          • 如果用户拥有查看该页面的权限,直接跳转
          • 如果用户没有查看该页面的权限,弹出无权限提醒,跳转到登陆页

接下来从技术栈的角度补充几点:

  1. vue-routerbeforeEach方法中实现以上逻辑,判断前端跳转去向;
  2. 出于系统简单考虑,不引入后端,用mockjs2模拟的用户信息作为拦截axios发起的服务请求响应;
  3. 通过Vuex.StoreuserNameuserRole的状态管理;

贴上项目目录结构图(src):

src
	api
		login.js 		//请求接口
	directives
		auth.js			//自定义的权限管理指令
	mock
		service
			auth.js		//模拟用户登录数据
		util.js			//辅助生成数据
	request
		http.js			//axios请求、响应、异常拦截器
	router
		index.js		//路由管理
	store
		module
			user.js		//userName,userRole状态管理
		index.js		//vuex全局配置
		mutation-types.js//状态类型定义
	util
		auth.js			//路由跳转时的权限判断
	views
		User
			Login.vue	//登录页面
		Main
			Home.vue	//登录后的Home页面
			AdminPage.vue // 登录后管理员才能查看的页面
		403.Vue			//无权限提醒页面
	App.vue				//系统全局页面
	main.js				//系统全局包管理

接下来给大伙无脑贴几个主要代码

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import antd from 'ant-design-vue'
import Auth from './directives/auth'
Vue.config.productionTip = false
require('./mock/service/auth')
new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app')

/views/User/Login.vue






/store/module/user.js

import storage from 'store'
import {login, logout} from "@/api/login";
import {ACCESS_TOKEN,USER_ROLE} from "@/store/mutation-types";

const user = {
    state: {
        name: '',
        roles: [],
    },
    mutations: {
        SET_NAME: (state, name) => {
            state.name = name
        },
        SET_ROLES: (state, roles) => {
            state.roles = roles
        },
    },
    actions: {
        Login(context, userInfo) {
            return new Promise((resolve, reject) => {
                login(userInfo).then(response => {
                    const data = response.result
                    //token保存时长7天
                    storage.set(ACCESS_TOKEN, data['token'], 7 * 24 * 60 * 60 * 1000)
                    context.commit('SET_ROLES', data['roles'])
                    context.commit('SET_NAME', data['name'])
                    resolve()
                }).catch(error => {
                    reject(error)
                })
            })
        },
        Logout(context) {
            return new Promise((resolve) => {
                logout().then(() => {
                    resolve()
                }).catch(() => {
                    resolve()
                }).finally(() => {
                    context.commit('SET_ROLES', [])
                    context.commit('SET_NAME', '')
                    storage.remove(ACCESS_TOKEN)
                })
            })
        }
    }
}

export default user

/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import user from "@/store/module/user";
Vue.use(Vuex)

export default new Vuex.Store({
    modules: {user}
})

/store/mutation-types.js

// 使用常量代替Mutation事件类型
export const ACCESS_TOKEN = 'Access-Token'
export const USER_ROLE = 'User-Role'

/api/login.js

import request from '../request/http'

const userApi = {
    Login: '/auth/login',
    Logout: '/user/logout',
}

export function login(params) {
    console.log('login params',params)
    return request({
        url: userApi.Login,
        method: 'post',
        data: params
    })
}

export function logout() {
    return request({
        url:userApi.Logout,
        method:'post',
        header:{
            'Content-Type':'application/json;charset=UTF-8'
        }
    })
}

/mock/service/auth.js

import Mock from 'mockjs2'
import {builder} from "@/mock/util";

const username = ['admin', 'user']
const password = ['admin', 'user']
const role = ['admin', 'user']

const login = (options) => {
    const body = options.body && JSON.parse(options.body)
    console.log('mock:body', body)
    if (username[0] === body.username && password[0] === body.password && role[0] === body.role) {
        return builder({
            'id': Mock.mock('@guid'),
            'name': '一号管理员',
            'username': body.username,
            'roles': [body.role],
            'token': '4291d7da9005377ec9aec4a71ea8asd'
        }, '管理员账号登录成功', 200, {'Custom-Header': Mock.mock('@guid')})
    } else if (username[1] === body.username && password[1] === body.password && role[1] === body.role) {
        return builder({
            'id': Mock.mock('@guid'),
            'name': '二号普通用户',
            'username': body.username,
            'roles': [body.role],
            'token': '4sd1d7da9qw5377ec9aec4a71ea8qwe'
        }, '普通用户账号登录成功', 200, {'Custom-Header': Mock.mock('@guid')})
    } else {
        return builder({isLogin: true}, '账号密码错误', 401)
    }
}

Mock.mock(/\/auth\/login/, 'post', login)

/mock/util.js

const responseBody = {
    message: '',
    timestamp: 0,
    result: null,
    code: 0
}
export const builder = (data, message, code = 0, headers = {}) => {
    responseBody.result = data
    if (message !== undefined && message !== null) {
        responseBody.message = message
    }
    if (code !== undefined && code !== 0) {
        responseBody.code = code
        responseBody._status = code
    }
    if (headers !== null && typeof headers === 'object' && Object.keys(headers).length > 0) {
        responseBody._headers = headers
    }
    responseBody.timestamp = new Date().getTime()
    console.log('responseBody', responseBody)
    return responseBody
}

/request/http.js

import axios from 'axios';
import storage from 'store';
import store from '../store'
import notification from 'ant-design-vue/es/notification'
import {ACCESS_TOKEN} from "@/store/mutation-types";

const request = axios.create({
    baseURL: '',
    timeout: 5000,
})

// 异常拦截处理器
const errorHandler = (error) => {
    console.log('error',error)
    console.log('error.response',error.response)
    if (error.response) {
        const data = error.response.data
        const token = storage.get(ACCESS_TOKEN)
        // console.log('error.response.status',error.response.status)
        if (error.response.status === 403) {
            notification.error({
                message: '您没有该权限',
                description: data.message
            })
        }
        if (error.response.status === 401 && !(data.result && data.result.isLogin)) {
            notification.error({
                message: '认证错误',
                description: data.message
            })
            if (token) {
                store.dispatch('Logout').then(() => {
                    setTimeout(() => {
                        window.location.reload()
                    }, 1500)
                })
            }
        }
    }
    return Promise.reject(error)
}

//http request 拦截器
request.interceptors.request.use(config => {
    console.log('config',config)
    const token = storage.get(ACCESS_TOKEN)
    // 如果token存在,让每个请求中携带token
    if (token) {
        config.headers['Access-Token'] = token
    }
    return config
}, errorHandler)

// http response 拦截器
request.interceptors.response.use((response) => {
    console.log('response.data', response.data)
    return response.data
}, errorHandler)

export default request;

/util/auth.js

import store from "@/store";
import storage from 'store'
import {ACCESS_TOKEN,USER_ROLE} from "@/store/mutation-types";
import user from "@/store/module/user";

export function getCurrentAuthority() {
    return user.state.roles
}

export function check(authority) {
    const current = getCurrentAuthority()
    return current && current.some(item => authority.includes(item));
}

export function isLogin() {
    return storage.get(ACCESS_TOKEN) ? true : false
}

export function logout() {
    storage.set(ACCESS_TOKEN, null)
}

export function needLogin(str) {
    let whiteUrl = ['/user/login', '/user/register', '/user']
    return whiteUrl.indexOf(str) === -1 ? true : false
}

/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import NoAuth403 from '../views/403'
import NProgress from 'nprogress';
import 'nprogress/nprogress.css'
import findLast from 'lodash/findLast';
import {check, isLogin, logout, needLogin} from "@/util/auth";
import {notification} from 'ant-design-vue'

Vue.use(VueRouter)

const routes = [
    {
        path: '/user',
        hideInMemu: true,
        component: () => import(/* webpackChunkName: "layout" */ "../layouts/UserLayout"),
        children: [
            {
                path: '/user',
                redirect: '/user/login'
            },
            {
                path: '/user/login',
                name: 'login',
                component: () => import(/* webpackChunkName: "user" */ "../views/User/Login")
            },
        ]
    },
    {
        path: '/',
        component: () => import(/* webpackChunkName: "layout" */ "../layouts/BasicLayout"),
        children: [
            {
                path: '/',
                redirect: '/main/home'
            },
            {
               path: '/main/home',
               name: 'home',
               meta: {authority: ['admin', 'user']},
               component: () => import(/* webpackChunkName: "main" */ "../views/Main/Home"),
            },
            {
               path: '/main/admin_page',
               name: 'admin_page',
               meta: {authority: ['admin']},
               component: () => import(/* webpackChunkName: "main" */ "../views/Main/AdminPage"),
            },
        ]
    },
    {
        path: '/403',
        name: '403',
        component: NoAuth403
    },
]

const router = new VueRouter({
    mode: 'history',
    base: process.env.BASE_URL,
    routes
})

router.beforeEach((to, from, next) => {
    //显示加载的进度条
    if (to.path !== from.path) {
        NProgress.start()
    }
    //前往不需要登录的页面时,如果已登录过,需要先退出,清除token
    if (!needLogin(to.path)) {
        if (isLogin()) {
            logout()
        }
        next()
    } else {
        // 前往需要登陆的页面,如果用户还没登陆,跳向登陆页
        if (!isLogin()) {
            next({path: '/user/login'})
        } else {
            const record = findLast(to.matched, record => record.meta.authority)
            // 用户已经登陆但没有该页面权限
            if (record && !check(record.meta.authority)) {
                notification.error({
                    message: '403',
                    description: '您没有查看该页面的权限'
                })
                next({path: '/403'})
            }
            next()
        }
        NProgress.done()
    }
})

router.afterEach(() => {
    NProgress.done();
})

export default router

即使是代码的复制粘贴,写到这也已经很疲惫了,其实里面的坑不少,以及,更重要的是,我可能里面有部分代码写错了(因为我的项目已经找不到最初的那个版本了),但大体上是没错的,如果大伙拿去用的时候发现有问题,我们评论区再讨论。

你可能感兴趣的:(vue.js,动态路由,权限管理,用户登录验证)