一、理解前后端权限的区别
后端:后端权限可以控制某个用户是否能够查询数据,是否能够修改数据等操作
前端:前端仅视图层的展示权限的核心是在于服务器中的数据变化
所以后端才是权限的关键
简单了解后端吧
1.后端如何知道该请求是哪个用户发过来的
cookie
session
token
2.后端的权限设计使用RBAC
用户
角色
权限工
二、前端的权限的意义
1.降低非法操作的可能性
2.尽可能排除不必要请求减轻服务器压力
3.提高用户体验
三、前端的权限控制常见的分类
菜单的控制
界面的控制
按钮的控制
请求和响应的控制
四、vue的权限控制实现前的准备工作
推荐下载vue-admin-template模板 打开文件,如下图所示:
目录结构说明
├── build # 构建相关
├── mock # 项目mock 模拟数据
├── plop-templates # 基本模板
├── public # 静态资源
│ │── favicon.ico # favicon图标
│ └── index.html # html模板
├── src # 源代码
│ ├── api # 所有请求
│ ├── assets # 主题 字体等静态资源
│ ├── components # 全局公用组件
│ ├── directive # 全局指令
│ ├── filters # 全局 filter
│ ├── icons # 项目所有 svg icons
│ ├── lang # 国际化 language
│ ├── layout # 全局 layout
│ ├── router # 路由
│ ├── store # 全局 store管理
│ ├── styles # 全局样式
│ ├── utils # 全局公用方法
│ ├── vendor # 公用vendor
│ ├── views # views 所有页面
│ ├── App.vue # 入口页面
│ ├── main.js # 入口文件 加载组件 初始化等
│ └── permission.js # 权限管理
├── tests # 测试
├── .env.xxx # 环境变量配置
├── .eslintrc.js # eslint 配置项
├── .babelrc # babel-loader 配置
├── .travis.yml # 自动化CI配置
├── vue.config.js # vue-cli 配置
├── postcss.config.js # postcss 配置
└── package.json # package.json
跨域配置在vue.config.js
中配置,如图:
说明:默认的模板中,使用的是mockjs,进一步了解mockjs官方对mockjs的解释有以下4点:
1.前后端分离
2.不需要修改既有代码,就可以拦截 Ajax 请求,返回模拟的响应数据
3.数据类型丰富
4.通过随机数据,模拟各种场景
看一个项目首先要从入口文件开始看,即:main.js和App.vue
main.js程序入口文件 初始化vue实例 并引入使用需要的插件和各种公共组件
[ new Vue()是新创建的实例 el是为实例提供挂载元素 ]
App.vue项目的主组件/页面入口文件 ,所有页面都在App.vue下进行切换,app.vue负责构建定义及页面组件归集。
这里就不做过多解释了
五、理解permission.js文件
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
const hasGetUserInfo = store.getters.user
if (hasGetUserInfo) {
next()
} else {
try {
// get user infob
await store.dispatch('user/getInfo').then(res => { //触发获取用户信息到接口
store.dispatch('permission/generateRoutes', res).then(() => { //触发获取路由表的接口
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
})
next()
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken') // 触发vuex中 resetToken => src/store/user.js 的 resetToken函数 清除token
Message.error(error.message || 'Has Error') // 弹出异常
next(`/login?redirect=${to.path}`)
// 然后就执行这里 跳转到 login redirect把从哪个页面出错的 做重定向的 => view/login/index.js的handleLogin函数
// =>src/store/user.js 的login函数
NProgress.done()
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
//其他没有访问权限的页面将重定向到登录页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
六、理解request.js文件,理解axios的拦截请求/响应原理
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // 跨域请求时发送cookie
timeout: 5000 // 请求超时
})
// 请求拦截器
service.interceptors.request.use(
config => {
// do something before request is sent
console.log(config)
if (store.getters.token) {
// 让每个请求都带有token
// ['AdminToken'] is a custom headers key
// 根据实际情况自行修改
config.headers['AdminToken'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* 根据HTTP状态码来判断code
*/
response => {
const res = response.data
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 1) {
Message({
message: res.msg || 'Error',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code == 1101) {
// to re-login
MessageBox.confirm('登录已失效,请重新登录!', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
//Location.reload() 方法用来刷新当前页面。该方法只有一个参数,当值为 true 时,将强制浏览器从服务器加载页面资源,
//当值为 false 或者未传参时,浏览器则可能从缓存中读取页面。
})
})
}
return Promise.reject(new Error(res.msg || 'Error'))
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
七、理解并编辑router/index.js的文件 ,动态路由渲染侧边栏
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
//布局
import Layout from '@/layout'
export const constantRouterMap = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '控制面板', icon: 'dashboard' }
}
]
}
]
export const asyncRouterMap = [
{
path: '/permission',
component: Layout,
name: 'permission',
meta: {
title: '权限管理',
permissions: ['roleRead', 'roleEdit', 'adminUserRead', 'adminUserEdit'],
icon: 'el-icon-s-order'
},
children: [
{
path: 'role',
component: () => import('@/views/pemission/role'),
name: 'role',
meta: {
title: '角色管理',
permissions: ['roleRead', 'roleEdit'],
icon: 'el-icon-coordinate'
}
},
{
path: 'index',
component: () => import('@/views/pemission/admin'),
name: 'admin',
meta: {
title: '管理员管理',
permissions: ['adminUserRead', 'adminUserEdit'],
icon: 'el-icon-s-custom'
}
}
]
},
{
path: '/user',
component: Layout,
name: 'user',
meta: {
title: '用户管理',
permissions: ['userRead', 'userEdit'],
icon: 'el-icon-user-solid'
},
children: [
{
path: 'index',
component: () => import('@/views/user/user.vue'),
name: 'userIndex',
meta: {
title: '用户管理',
permissions: ['userRead', 'userEdit'],
icon: 'el-icon-user'
}
}
]
},
/*动态路由渲染的侧边栏菜单*/
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
const createRouter = () =>
new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // 重置路由
}
export default router
八、新建文件,编辑api/admin.js文件
import request from '@/utils/request'
const prefix = '/admin'
// 登陆
export function login(data) {
return request({
url: prefix + '/login',
method: 'post',
data
})
}
// 获取登录用户信息
export function getInfo() {
return request({
url: prefix + '/getUserInfo',
method: 'get'
})
}
// 退出登录
export function logout() {
return request({
url: prefix + '/logout',
method: 'post'
})
}
// 获取用户列表
export function getList(params) {
return request({
url: prefix + '/getUserList',
method: 'get',
params
})
}
// 获取角色列表
export function getRoleList(params) {
return request({
url: prefix + '/getRoleList',
method: 'get',
params
})
}
// 增加角色
export function addRoles(params) {
return request({
url: prefix + '/addRole',
method: 'post',
params
})
}
// 编辑角色
export function editRoles(params) {
return request({
url: prefix + '/editRole',
method: 'post',
params
})
}
// 删除角色
export function deleteRoles(params) {
return request({
url: prefix + '/deleteRole',
method: 'post',
params
})
}
// 获取权限列表
export function getPermissionList(params) {
return request({
url: prefix + '/getAuthList',
method: 'get',
params
})
}
// 新增用户
export function adminUserCreate(data) {
return request({
url: prefix + '/userCreate',
method: 'post',
data
})
}
// 重置用户密码
export function resetAdminPassword(data) {
return request({
url: prefix + '/resetUserPassword',
method: 'post',
data
})
}
// 编辑用户
export function editUser(data) {
return request({
url: prefix + '/editUser',
method: 'post',
data
})
}
// 编辑用户角色
export function editAdminRoles(data) {
return request({
url: prefix + '/editUserRole',
method: 'post',
data
})
}
九、编辑view的vue文件,此处示例 :permission/admin.vue
<template>
<div class="app-container">
<div class="search-container">
<div class="search-form">
<el-form :inline="true" :model="searchForm">
<el-form-item label="账号">
<el-input v-model="searchForm.account" placeholder="账号" />
</el-form-item>
<el-form-item>
<el-button type="warning" @click="fetchData">查询</el-button>
<el-button
type="primary"
@click="addDialogVisible = true"
>新增</el-button>
</el-form-item>
</el-form>
</div>
</div>
<el-table
v-loading="listLoading"
:data="list"
element-loading-text="Loading"
border
fit
highlight-current-row
>
<el-table-column align="center" label="ID" width="95">
<template slot-scope="scope">
{{ scope.row.id }}
</template>
</el-table-column>
<el-table-column label="用户名" width="270" align="center">
<template slot-scope="scope">
{{ scope.row.account }}
</template>
</el-table-column>
<el-table-column label="角色" align="center">
<template slot-scope="scope">
<span>{{
scope.row.is_admin == 1
? '超级管理员'
: scope.row.role.map(i => i.name).join()
}}</span>
</template>
</el-table-column>
<el-table-column label="登录IP" width="250" align="center">
<template slot-scope="scope">
{{ scope.row.login_ip }}
</template>
</el-table-column>
<el-table-column
align="center"
prop="login_time"
label="登录时间"
width="200"
>
<template slot-scope="scope">
<i class="el-icon-time" />
<span>{{ scope.row.login_time }}</span>
</template>
</el-table-column>
<el-table-column
class-name="status-col"
label="状态"
width="110"
align="center"
>
<template slot-scope="scope">
<el-tag :type="scope.row.status | statusFilter">{{
scope.row.status | statusFilter3
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="350" align="center">
<template slot-scope="scope">
<el-button
type="primary"
@click="editRole(scope.row)"
>编辑角色</el-button>
<el-button
type="primary"
@click="resetPassword(scope.row)"
>重置密码</el-button>
<el-button
:type="scope.row.status | statusFilter2"
:disabled="scope.row.state == 1"
@click="toggleStatus(scope.row)"
>{{ scope.row.status | statusFilter4 }}</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
:current-page="pagination.currentPage"
:page-sizes="[10, 20, 30, 40, 50, 100]"
:page-size="pagination.limit"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<el-dialog title="新增账号" :visible.sync="addDialogVisible" width="500px">
<div class="dialog-container">
<el-form ref="addForm" :model="addForm" :rules="addFormRules">
<el-form-item prop="account">
<el-input v-model="addForm.account" placeholder="请输入账号名称" />
</el-form-item>
<el-form-item>
<el-select
v-model="addForm.roles"
multiple
clearable
placeholder="请选择角色"
style="width:100%;"
>
<el-option
v-for="item in roleList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addAdmin">确 定</el-button>
</div>
</el-dialog>
<el-dialog title="编辑角色" :visible.sync="editDialogVisible" width="500px">
<div class="dialog-container">
<el-form ref="editForm" :model="editForm">
<el-form-item>
<el-select
v-model="editForm.roles"
multiple
clearable
placeholder="请选择角色"
style="width:100%;"
>
<el-option
v-for="item in roleList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="editAdmin">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
getRoleList,
getList,
adminUserCreate,
editAdminRoles,
resetAdminPassword,
editUser
} from '@/api/admin'
import { filterObjectEmpty } from '@/utils'
export default {
name: 'Admin',
filters: {
statusFilter(status) {
const statusMap = [, 'success', 'danger']
return statusMap[status]
},
statusFilter2(status) {
const statusMap = [, 'danger', 'success']
return statusMap[status]
},
statusFilter3(status) {
const statusMap = [, '启用', '禁用']
return statusMap[status]
},
statusFilter4(status) {
const statusMap = [, '禁用', '启用']
return statusMap[status]
}
},
data() {
return {
searchForm: {
account: ''
},
pagination: {
page: 1,
limit: 10
},
list: null,
total: 0,
listLoading: true,
addDialogVisible: false,
addForm: {
account: '',
roles: []
},
addFormRules: {
account: [
{ required: true, message: '请输入账号名称', trigger: 'blur' },
{ min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
]
},
roleList: [],
editDialogVisible: false,
editForm: {
id: '',
roles: []
}
}
},
created() {
this.fetchData()
this.fetchRoleList()
},
methods: {
fetchRoleList() {
getRoleList({ limit: 100 }).then(res => {
this.roleList = res.data.data
})
},
fetchData() {
this.listLoading = true
const params = {
...filterObjectEmpty(this.searchForm),
...this.pagination
}
getList(params).then(response => {
const { data, current_page: currentPage, total } = response.data
this.list = data
this.total = total
this.pagination.page = currentPage
this.listLoading = false
})
},
handleSizeChange(size) {
if (size != this.pagination.limit) {
this.pagination.limit = size
this.pagination.page = 1
this.fetchData()
}
},
handleCurrentChange(page) {
if (page != this.pagination.page) {
this.pagination.page = page
this.fetchData()
}
},
addAdmin() {
this.$refs.addForm.validate(valid => {
if (valid) {
adminUserCreate(this.addForm).then(res => {
this.addDialogVisible = false
this.fetchData()
})
}
})
},
toggleStatus(user) {
let statu = '启用'
let status = 1
if (user.status == 1) {
statu = '禁用'
status = 2
}
this.$confirm(`是否确认设置为${statu}状态?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
editUser({ user_id: user.id, status }).then(res => {
this.fetchData()
})
})
},
editRole(user) {
this.editForm.id = user.id
this.editForm.roles = user.role.map(role => role.id)
this.editDialogVisible = true
},
editAdmin() {
const data = {
user_id: this.editForm.id,
role_ids: this.editForm.roles
}
editAdminRoles(data).then(res => {
this.fetchData()
this.editDialogVisible = false
})
},
resetPassword(user) {
this.$confirm('确定重置密码?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
resetAdminPassword({ user_id: user.id }).then(res => {
this.$message({
message: '重置密码成功!',
type: 'success'
})
})
})
}
}
}
</script>
<style lang="scss" scoped></style>
十、页面展示
总结
同理,结合ES6和Element-UI可以完成其它管理菜单
1.首先是理解vue_admin_template集成模板自带的主要文件
2.编辑登录页面的权限配置
3.生成侧边栏菜单管理
4.实现菜单管理页面的各个功能,常见的有增/删/改/除/查、禁用状态、上传图片等
5.新页面的实现:router[新增侧边栏菜单]
+api[配置接口]
+view[编辑菜单管理的功能]