1. 楔子
我开始接触 Web 开发那会,正是 MVC 框架大行其道的时期。怎么做前端鉴权呢?大致有两种思路:
1、页面在服务端生成,模版引擎拿到当前用户的角色/授权信息(来自 session 或其他渠道)配合权限规则生成最终的 HTML 代码返回给浏览器。此时用户看到的就是已被授权的内容。
2、网页从后端获取用户的角色/授权信息,在本地判断元素的显隐。
今时今日如火如荼的前后端分离做鉴权,大多采用的是类似第二种的做法,也是今天我要聊的话题。
2. 鉴权方案
前端鉴权主要分权限(角色/授权信息)获取、权限判断这两块,简单来说就是拿到规则跟应用规则。
2.1 获取用户权限
我们通常会选择在应用初始化、首次需要鉴权这两个时间点获取用户的权限信息,这里采用的是前者。由于从远程获取数据是个存在延迟的动作,所以要注意同步控制(只有拿到权限数据才进行后续的作业,否则可能出现越权),还有就是存在白屏的风险,一旦出现可以通过显示 loading 动画缓解。
拿到权限数据后,如何存放呢?
这里采用的方案是直接写入到 window 全局对象(不可修改,见下方代码),纯粹是因为这样读写方便、代码简单 😄。
// 假设 remoteData 为 {roles:["INPUT"], id: "001", name:"集成显卡"}
let account = Object.assign({ roles: [] }, remoteData)
//锁定用户对象,不支持修改
Object.keys(account).forEach(k => {
Object.defineProperty(account, k, { value: account[k], writable: false, enumerable: true, configurable: true })
})
window.User = account
如果是将数据放入状态管理库(如 Pinia),可增加隐蔽性(无法通过控制台 window.User 查看)、支持响应式,相对地代码也更复杂。
2.2 权限判断
用户权限信息已经拿到手了,如何使用呢?
首先我们定义一个通用方法 checkRole,用来判断当前登录用户是否具备指定权限
/**
* 所在文件 Auth.js
*
* @param {*} requireRole 角色名称或者函数(自行实现判断逻辑)
* @returns
*/
export function checkRole(requireRole) {
let roles = window.User ? (User.roles || []) : []
return typeof (requireRole) === 'string' ? roles.includes(requireRole) : requireRole(roles)
}
通常会在下面的场景需要判断权限。
2.2.1 页面级
路由跳转时进行鉴权,若权限不匹配则重定向到指定页面
借助 vue-router 的钩子函数进行拦截:
router.beforeEach((to, from, next) => {
/*
判断权限
注意:meta 是路由定义被保留的属性
*/
if (to.meta.role && !checkRole(to.meta.role)) {
console.error(`☹ ${to.name} (${to.fullPath}) 需要权限 ${to.meta.role},请联系管理员授权 ☹`)
return next({ name: P403 })
}
next()
})
2.2.2 组件级
页面内某个组件(如按钮、菜单)只有具备相应权限才显示
- 组件内判断
<!--需要赋值 isAdmin,如 let isAdmin = window.User.roles.includes("ADMIN")-->
<div v-if="isAdmin"></div>
- 封装为组件
<template>
<template v-if="show"> <slot></slot> </template>
</template>
<script setup>
import { checkRole } from "@S/Auth"
const props = defineProps({
need: { type: [String, Function], default: "" }
})
let show = checkRole(props.need)
</script>
<!--如何使用-->
<WithRole need="ADMIN"> <AdminMenu /> </WithRole>
- 自定义指令
/**
* 权限判断指令,如组件标记了 v-role="ADMIN" 需要 ADMIN 权限方可显示
*/
export const Role = {
mounted(el, binding) {
const { value } = binding
/**
* 由于自定义指令无法作用于自定义组件上(即智能用于 div、span 等标准元素)
* 所以移除 dom 元素时,直接将父元素移除
*/
if (!checkRole(value)) el.parentNode && el.parentNode.remove()
}
}
使用自定义指令(注意:自定义指令仅能用于原生 dom 元素,如 div、span 等)
<div v-role="'ADMIN'"><AdminMenu /></div>
上述几种方式都能实现效果,可根据实际情况或者个人口味选择食用。我的话,偏向于自定义指令(显得逼格更高 😎),但是在同一组件内多次鉴权就会用方式一,可以节约判断的次数,绿色计算,为实现碳中和实现一份绵力哈哈。
3. 结尾
随着前端体系跟计算量日渐增大,权限控制的重要性会愈加突出,方式也会更多样,甚是期待 😁
以上是对于前端鉴权的个人理解,如果有不对或者更合适的方案可留言哈。
我把相关代码放到仓库里:https://github.com/0604hx/vue3-naive-starter