近期要实现一个公安偷窃案件管理系统的登陆功能,搜遍网上发现没有能直接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)目前对我还不是必须的,所以我都没有选择安装,整个项目的配置项如下:
在开始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"
},
先讲思路,让大伙有个大概的印象,不至于看代码云里雾里。
storage
中的token
作为用户是否登录的标志,如果当前storage
中有token
,表明当前系统已被登录Home.vue
,不需要登录的Login.vue
token
再跳转storage
中填入token
接下来从技术栈的角度补充几点:
vue-router
的beforeEach
方法中实现以上逻辑,判断前端跳转去向;mockjs2
模拟的用户信息作为拦截axios
发起的服务请求响应;Vuex.Store
做userName
,userRole
的状态管理;贴上项目目录结构图(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
即使是代码的复制粘贴,写到这也已经很疲惫了,其实里面的坑不少,以及,更重要的是,我可能里面有部分代码写错了(因为我的项目已经找不到最初的那个版本了),但大体上是没错的,如果大伙拿去用的时候发现有问题,我们评论区再讨论。