项目复盘之【用户登录】上

系列文章目录

用户登录(上)

用户登录(下)

文章目录

目录

系列文章目录

用户登录(上)

文章目录

前言

一、登录流程分析

二、涉及知识点

1.访问登录页面——路由和权限校验

1.路由权限实例

2.路由处理逻辑分析

(1)路由逻辑图

(2)路由场景分析

(3)路由逻辑源码

(4)动态路由permission/generateRoutes分析

(5)小结

2.侧边栏sidebar

1.源码位置

2.分析

3.重定向

1.登录重定向

2.重定向组件

4.面包屑导航与路由匹配

1.面包屑导航实现的逻辑

2.渲染面包屑导航


前言

用户登录是许多中后台项目必备的开发模块,看似简单,但背后涉及的知识点非常广泛,需要非常注重细节,拆分出的方法多。本篇文章会基于 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 属性
        • 判断用户的角色是否命中路由中的任意一个权限,如果命中,则保存路由
        • 如果未命中,则直接将该路由舍弃;
  • 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
  1. 第一部分是当只需要展示一个 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 为空时,整个菜单项将不会展示
  2. 第二部分是当 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应用方式、请求用户信息等内容请到用户登录-下查看

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值