系列文章目录
用户登录(上)
用户登录(下)
文章目录
目录
(4)动态路由permission/generateRoutes分析
前言
用户登录是许多中后台项目必备的开发模块,看似简单,但背后涉及的知识点非常广泛,需要非常注重细节,拆分出的方法多。本篇文章会基于 vue-element-admin的通用后台框架详细的整理用户登录业务的流程及原理,分为上下章,可根据索引查看,共勉~上:整体登录流程、路由权限实例、侧边栏 、重定向 、面包屑导航与路由匹配;
下:后端鉴权——MySQL应用方式、JWT应用方式、请求用户信息,后端进行token校验——axios拦截器。
一、登录流程分析
1.用户在【前端】登录页面输入用户名和密码,用户【点击】登录后会将用户名、密码传到【后端】,【后端】在MySQL数据库中验证用户名与密码是否正确(用户鉴权);
2.验证正确后,通过jwt生成token——登录的令牌,传递给前端,保存在本地。之后前端需要请求服务器时,将token附带在http header中,并向后端发起token校验;
3.后端解析token并查询用户信息,返回给前端,前端将用户信息做路由认证,重定向到根路径(dashboard),根据用户权限生成菜单。
二、涉及知识点
1.访问登录页面——路由和权限校验
1.路由权限实例
配置子路由信息,加入roles: ['admin']后,非管理员登录会进入error页面
meta属性:
- 定义:Vue Router中,meta属性是一个对象,用于存储与路由相关的元数据。
- 作用:方便地管理路由的元数据,其可以包括路由的标题、描述、图标等信息,也包括需要进行权限验证的信息。
- 获取:在组件中通过$route.meta来获取上述信息。使用meta属性可以,提高代码的可维护性和可读性。
export const asyncRoutes = [
{
path: '/book',//1.配置路由路径
component: Layout,//2.配置路由对应的组件
name:'book'//3.名字,可省略
redirect: '/book/create',
children: [
{
path: '/book/create',
component: () => import('@/views/book/create'),
name: 'book',
meta: { title: '添加图书', icon: 'edit', roles: ['admin'] }
}
]
},
// ...
]
2.路由处理逻辑分析
(1)路由逻辑图
(2)路由场景分析
中后台路由常见-场景分析
场景 情况分析 已获取 Token 1.访问 /login:重定向到 / 2.访问 /login?redirect=/xxx:重定向到 /xxx 3.访问 /login 以外的路由:直接访问 /xxx 未获取 Token 1.访问 /login:直接访问 /login 2.访问 /login 以外的路由:如访问 /dashboard,实际访问路径为 /login?redirect=%2Fdashboard,登录后会直接重定向 /dashboard
(3)路由逻辑源码
1.全局入口文件main.js 中加载了全局路由守卫
import './permission' // permission control
2.permission 定义了全局路由守卫
路由与生命钩子的分析:Vue的钩子函数[路由导航守卫、keep-alive、生命周期钩子] - 掘金
路由守卫文档:导航守卫 | Vue Router
浏览器title设置:http://t.csdn.cn/F9JRe
//校验和路由跳转——全局路由守卫
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', '/auth-redirect'] // no redirect whitelist
router.beforeEach(async(to, from, next) => {
// 启动进度条
NProgress.start()
// 修改页面标题
document.title = getPageTitle(to.meta.title)
// 从Cookie中获取Token
const hasToken = getToken()
if (hasToken) {
//以获取token
if (to.path === '/login') {
//访问 /login:重定向到 /
next({ path: '/' })
NProgress.done()
} else {
//访问 /login 以外的路由:直接访问 /xxx
// 判断用户的角色是否存在
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
//用户角色已经存在,动态路由表已生成,按照动态路由放行
next()
} else {
try {
// 用户角色不存在:异步获取用户的角色
const { roles } = await store.dispatch('user/getInfo')
// 根据用户角色,动态生成路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 调用 router.addRoutes 动态添加路由
router.addRoutes(accessRoutes)
//已获取token,场景3:访问 /login 以外的路由:直接访问 /xxx
// 使用 replace 访问路由,不会在 history 中留下记录
next({ ...to, replace: true })
} catch (error) {
// 异常处理:移除 Token 数据
await store.dispatch('user/resetToken')
// 显示错误提示
Message.error(error || 'Has Error')
// 重定向至登录页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* 未获取token*/
if (whiteList.indexOf(to.path) !== -1) {
// 如果访问的路径在白名单中,则直接访问
next()
} else {
// 如果访问的 URL 不在白名单中,则直接重定向到登录页面,并将访问的 URL 添加到 redirect 参数中
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// 停止进度条
NProgress.done()
})
(4)动态路由permission/generateRoutes分析
代码逻辑图:
动态路由模块位置:src\store\modules\permission.js
核心模块调用关系:generateRoutes->filterAsyncRoutes->hasPermission
step1:src\store\modules\permission.js中的action中定义了生成动态路由的函数generateRoutes,该函数调用 filterAsyncRoutes函数,根据用户身份,对路由进行过滤。
const actions = {
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes || []
} else {
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
step2:filterAsyncRoutes函数中的routes即是异步组件路由asyncRoutes,这里提供一个查看调用的小技巧,在函数体内将鼠标放到形参使用的地方,注意悬浮显示的信息中会提示params参数是谁,只有清楚函数内部正在处理的数据是谁?数据类型是对象还是数组?才能理清代码逻辑:
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
//对routes进行遍历,对每一项进行浅拷贝
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
//权限判断通过
if (tmp.children) {
//当前路由有子路由,迭代调用自身进行判断并更新children
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
//权限判断未通过,返回空数组
return res
}
step3:hasPermission函数检查用户权限
/*检查用户权限*/
function hasPermission(roles, route) {
//举例:
//roles=['editor','cc','dd','admin']
//route.meta.roles=['admin']
// 检查路由是否包含 meta 和 meta.roles 属性
if (route.meta && route.meta.roles) {
// 判断 route.meta.roles 中是否包含用户角色 roles 中的任何一个权限,如果包含则返回 true,否则为 false
return roles.some(role => route.meta.roles.includes(role))
} else {
// 如果路由没有 meta 或 meta.roles 属性,则视为该路由不需要进行权限控制,所有用户对该路由都具有访问权限
return true
}
}
(5)小结
1.路由处理
访问路由时会从 Cookie 中获取 Token,判断 Token 是否存在:
- 如果 Token 存在,将根据用户角色(role)生成动态路由(generateRoutes),然后访问路由,生成对应的页面组件。这里有一个特例,即用户访问
/login
时会重定向至/
路由; - 如果 Token 不存在,则会判断路由是否在白名单中(whiteList),如果在白名单中将直接访问,否则说明该路由需要登录才能访问,此时会将路由生成一个 redirect 参数传入 login 组件,实际访问的路由为:
/login?redirect=/xxx
2.
动态路由和权限校验
- vue-element-admin 将路由分为:constantRoutes 和 asyncRoutes
- 用户登录系统时,会动态生成路由,其中 constantRoutes 必然包含,asyncRoutes 进行过滤
- asyncRoutes 过滤的逻辑
- 看路由下是否包含 meta 和 meta.roles 属性
- 如果没有该属性,说明这是一个通用路由,不需要进行权限校验;
- 如果包含 roles 属性
- 判断用户的角色是否命中路由中的任意一个权限,如果命中,则保存路由
- 如果未命中,则直接将该路由舍弃;
- 看路由下是否包含 meta 和 meta.roles 属性
- asyncRoutes 处理完毕后,会和 constantRoutes 合并为一个新的路由对象,并保存到 vuex 的 permission/routes 中;
- 用户登录系统后,侧边栏会从 vuex 中获取
state.permission.routes
,根据该路由动态渲染用户菜单。
2.侧边栏sidebar
本部分:分析侧边栏如何根据路由动态渲染出侧边栏的项目
1.源码位置
- layout 组件引用sidebar ,layout 组件位于
src/layout/index.vue
- sidebar 组件源码位于
src/layout/components/Sidebar/index.vue
2.分析
- sidebar:sidebar 主要包含 el-menu 容器组件,el-menu 中遍历 vuex 中的 routes,生成 sidebar-item 组件。
- :default-active="activeMenu"实现默认高亮的项目分析
- 在开发者工具中查看vuex中的routes
sidebar组件源码-结构:
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
- sidebar 主要配置项如下:
- activeMenu:根据当前路由的 meta.activeMenu 属性控制侧边栏中高亮菜单
- isCollapse:根据 Cookie 的 sidebarStatus 控制侧边栏是否折叠
- variables:通过
@/styles/variables.scss
填充 el-menu 的基本样式
sidebar组件源码-逻辑:
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'permission_routes',
'sidebar'
]),
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>
- sidebar-item
- 第一部分是当只需要展示一个 children 或者没有 children 时进行展示,展示的组件包括:
- app-link:动态组件,path 为链接时,显示为 a 标签,path 为路由时,显示为 router-link 组件;
- el-menu-item:菜单项,当 sidebar-item 为非 nest 组件时,el-menu-item 会增加 submenu-title-noDropdown 的 class;
- item:el-menu-item 里的内容,主要是 icon 和 title,当 title 为空时,整个菜单项将不会展示
- 第二部分是当 children 超过两项时进行展示,展示的组件包括:
- el-submenu:子菜单组件容器,用于嵌套子菜单组件;
- sidebar-item:el-submenu 迭代嵌套了 sidebar-item 组件,在 sidebar-item 组件中有两点变化:
- 设置 is-nest 属性为 true;
- 根据 child.path 生成了 base-path 属性传入 sidebar-item 组件
3.重定向
1.登录重定向
login.vue 中对 $route 进行监听:
this.getOtherQuery(query)
的用途是获取除 redirect 外的其他查询条件
watch: {
$route: {
handler: function(route) {
const query = route.query
if (query) {
this.redirect = query.redirect
this.otherQuery = this.getOtherQuery(query)
}
},
immediate: true
}
}
登录成功后:完成重定向
this.$store.dispatch('user/login', this.loginForm)
.then(() => {
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
this.loading = false
})
.catch(() => {
this.loading = false
})
2.重定向组件
vue-element-admin 提供了专门的重定向组件,源码如下:
<script>
export default {
created() {
const { params, query } = this.$route
const { path } = params
this.$router.replace({ path: '/' + path, query })
},
render: function(h) {
return h() // avoid warning message
}
}
</script>
重定向组件配置了动态路由:
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path*',
component: () => import('@/views/redirect/index')
}
]
}
注意细节:表示匹配零个或多个路由,比如路由为 /redirect
时,仍然能匹配到 redirect 组件。如果将路由改为path: '/redirect/:path'将只能匹配到 Layout 组件,而无法匹配到 redirect 组件
path: '/redirect/:path*'
4.面包屑导航与路由匹配
1.面包屑导航实现的逻辑
面包屑导航实现的逻辑如下:
- 获取
this.$route.matched
,并过滤其中不包含item.meta.title
的项,生成新的面包屑导航数组matched
- 判断
matched
第一项是否为 dashboard,如果不是,则添加 dashboard 为面包屑导航第一项 - 再次过滤
matched
中item.meta.title
为空的项和item.meta.breadcrumb
为 false 的项
这里的关键是
this.$route.matched
属性,它是一个数组,记录了路由的匹配过程,这就是面包屑导航实现的基础
2.渲染面包屑导航
面包屑导航模板源码:
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
el-breadcrumb-item
内做了一个判断,如果是最后一个元素或者路由的 redirect
属性指定为 noRedirect
则不会生成链接,否则将使用 a
标签生成链接,但是这里使用 @click.prevent
阻止了默认 a
标签事件触发,而使用自定义的 handleLink
方法处理路由跳转,handleLink
方法源码如下:pathCompile
用于解决动态路由的匹配问题
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.pathCompile(path))
}
后端鉴权——MySQL应用方式、JWT应用方式、请求用户信息等内容请到用户登录-下查看