Vue和React权限控制的那些事
自我介绍
看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。
前言
无论是后台管理系统,还是面向C端的产品,权限控制都是日常工作中常见的需求。在此梳理一下权限控制的那些逻辑,以及在Vue/React框架下是有什么样的解决方案。
什么是权限控制?
现在基本上都是基于RBAC权限模型
来做权限控制
一般来说权限控制就是三种
页面权限:说白了部分页面是具备权限的,没权限的无法访问
操作权限:增删改查的操作会有权限的控制
数据权限:不同用户看到的、数据是不一样的,比如一个
列表
,不同权限的查看这部分数据,可能有些字段是**
脱敏的,有些条目无法查看详情
,甚至部分条目是无法查看
那么对应到前端的维度,常见的就4种
权限失效(无效)(token过期/尚未登录)
页面路由控制,以路由为控制单位
页面上的操作按钮、组件等的权限控制,以组件/按钮为最小控制单位
动态权限控制,比如1个列表,部分数据可以编辑,部分部分不可编辑
⚠️注意: 本文一些方案 基于 React18
React-Router V6
以及 Vue3
Vue-Router V4
⚠️Umi Max
这种具备权限控制系统的框架暂时不在讨论范围内~~
前置条件
由于市面上各家实现细节不一样,这里只讨论核心逻辑思路,不考虑细节实现
无论框架如何,后端根据RABC
角色权限这套逻辑下来的,会有如下类似的权限标识
信息,可以通过专门的接口获取,或者跟登录接口放在一起。
然后根据这些数据,去跟路由,按钮/组件等,比对产生真正的权限
像这种权限标识一般都存在内存
当中(即便存在本地存储也需要加密,不过其实真正的权限控制还是需要后端来控),一般都是全局维护的状态,配合全局状态管理库使用。
权限失效(无效)
这种场景一般是在发送某些请求,返回过期
状态
或者跟后端约定一个过期时间(这种比较不靠谱)
通常是在 全局请求拦截 下做,整理一下逻辑
路由级别权限控制
通常前端配好的路由可以分为 2 种:
一种是静态路由
:即无论什么权限都会有的,比如登录页、404页这些
另一种是动态路由
:虽然叫动态路由,其实也是在前端当中定义好了的。说它是动态的原因是根据后端的权限列表,要去做动态控制的
vue实现
在vue体系下,可以通过路由守卫
以及动态添加路由
来实现
动态路由
先配置静态路由表 , 不在路由表内的路由重定向到指定页(比如404)
在异步获取到权限列表之后,对动态部分的路由进行过滤之后得到有权限的那部分路由,再通过router.addRoute()
添加到路由实例当中。
流程为:
(初始化时) 添加静态路由 --> 校验登录态(比如是否有token之类的) --> 获取权限列表(存到vuex / pinia) --> 动态添加路由(在路由守卫处添加)
rightsRoutesList // 来自后端的当前用户的权限列表,可以考虑存在全局状态库
dynamicRoutes // 动态部分路由,在前端已经定义好, 直接引入
// 对动态路由进行过滤,这里仅用path来比较
// 目的是添加有权限的那部分路由,具体实现方案自定。
const generateRoute = (rightsRoutesList)=>{
//ps: 这里需要注意下(如果有)嵌套路由的处理
return dynamicRoutes.filter(i=>
rightsRoutesList.some(path=>path === i.path)
)
}
// 拿到后端返回的权限列表
const getRightsRoutesList = ()=>{
return new Promise(resolve=>{
const store = userStore()
if(store.rightsRoutesList){
resolve(store.rightsRoutesList)
}else{
// 这里用 pinia 封装的函数去获取 后端权限列表
const rightsRoutesList = await store.fetchRightsRoutesList()
resolve(rightsRoutesList)
}
}
}
let hasAddedDynamicRoute = false
router.beforeEach(async (to, from) => {
if(hasAddedDynamicRoute){
// 获取
const rightsRoutesList = await getRightsRoutesList()
// 添加到路由示例当中
const routes = generateRoute(rightsRoutesList)
routes.forEach(route=>router.addRoute(route))
// 对于部分嵌套路由的子路由才是动态路由的,可以
router.addRoute('fatherName',route)
hasAddedDynamicRoute = true
}
// 其他逻辑。。。略
next({...to})
}
踩坑
通过动态addRoute
去添加的路由,如果你F5刷新进入这部分路由,会有白屏现象。
因为刷新进入的过程经历了 异步获取权限列表 -->
addRoute注册 的过程,此时跳转的目标路由就和你新增的路由相匹配了,需要去手动导航。
因此你需要在路由守卫那边next放行,等下次再进去匹配到当前路由
你可以这么写
router.beforeEach( (to,from,next) => {
// ...其他逻辑
// 关键代码
next({...to})
})
路由守卫
一次性添加所有的路由,包括静态和动态。每次导航的时候,去对那些即将进入的路由,如果即将进入的路由是在动态路由里,进行权限匹配。
可以利用全局的路由守卫
router.beforeEach( (to,from,next) => {
// 没有访问权限,则重定向到404
if(!hasAuthorization(to)){
// 重定向
return '/404'
}
})
也可以使用路由独享守卫,给 权限路由 添加
// 路由表
const routes = [
//其他路由。。。略
// 权限路由
{
path: '/users/:id',
component: UserDetails,
// 定义独享路由守卫
beforeEnter: (to, from) => {
// 如果没有许可,则
if(!hasAuthorization(to)){
// 重定向到其他位置
return '/404'
}
},
},
]
react实现
在react当中,一般先将所有路由添加好,再通过路由守卫来做权限校验
局部守卫loader
React-router 当中没有路由守卫功能,可以利用v6版本的新特性loader来做,给权限路由都加上对应的控制loader
import { redirect, createBrowserRouter, RouterProvider } from 'react-router-dom'
const router = createBrowserRouter([
{
// it renders this element
element: <Team />,
// when the URL matches this segment
path: "teams/:teamId",
// with this data loaded before rendering
loader: async ({ request, params }) => {
// 拿到权限
const permission = await getPermission("teams/:teamId")
// 没有权限则跳到404
if(!permission){
return redirect('/404')
}
return null
},
// and renders this element in case something went wrong
errorElement: <ErrorBoundary />,
},
]);
// 使用
function RouterView (){
return (
<RouterProvider router={router}/>
)
}
包装路由(相当于路由守卫)
配置路由组件的时候,先渲染包装的路由组件
在包装的组件里做权限判断
function RouteElementWrapper({children, path, ...props }: any) {
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(()=>{
// 判断登录态之类的逻辑
// 如果要获取权限,则需要setIsLoading,保持加载状态
// 这里判断权限
if(!hasAccess(path)){
navigate('/404')
}
},[])
// 渲染routes里定义好的路由
return isLoading ? <Locading/> : children
}
按钮(组件)级别权限控制
组件级别的权限控制,核心思路就是 将判断权限的逻辑抽离出来,方便复用。
vue 实现思路
在vue当中可以利用指令系统,以及hook来实现
自定义指令
指令可以这么去使用
<template>
<button v-auth='/site/config.btn'> 编辑 </button>
</template>
指令内部可以操作该组件dom和vNode,因此可以控制显隐、样式等。
hook
同样的利用hook 配合v-if 等指令 也可以实现组件级颗粒度的权限控制
<template>
<button v-if='editAuth'> 权限编辑 </button>
<div v-else>
无权限时做些什么
</div>
<button v-if='saveAuth'> 权限保存 </button>
<button v-if='removeAuth'> 权限删除 </button>
</template>
<script setup>
import useAuth from '@/hooks/useAuth'
// 传入权限
const [editAuth,saveAuth,removeAuth] = useAuth(['edit','save','remove'])
</script>
hook里的实现思路: 从pinia获取权限列表,hook里监听这个列表,并且匹配对应的权限,同时修改响应式数据。
react 实现思路
在React当中可以用高阶组件和hook的方式来实现
hook
定义一个useAuth的hook
主要逻辑是: 取出权限,然后通过关联响应式,暴露出以及authKeys ,hasAuth函数
export function useAuth(){
// 取出权限 ps: 这里从redux当中取
const authData = useSelector((state:any)=>state.login)
// 取出权限keys
const authKeys = useMemo(()=>authData.auth.components ?? [],[authData])
// 是否拥有权限
const hasAuth = useCallback(
(auths:string[]|string)=>(
turnIntoList(auths).every(auth=>authKeys.includes(auth))
),
[authKeys]
)
const ret:[typeof authKeys,typeof hasAuth] = [authKeys,hasAuth]
return ret
}
使用
const ProductList: React.FC = () => {
// 引入
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth("edit"), [hasAuth]);
// ...略
return (
<>
{ authorized ? <button> 编辑按钮(权限)</button> : null}
</>
)
};
权限包裹组件
可以跟进一步,依据这个权限hook,封装一层包裹组件
const AuthWrapper:React.FC<{auth:string|string[],children:JSX.Element}> = ({auth, children})=>{
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth(auth), [hasAuth]);
// 控制显隐
return authorized ? children : null
}
使用
<AuthWrapper auth='edit'>
<button> 编辑按钮(AuthWrapper) </button>
</AuthWrapper>
还可以利用renderProps特性
const AuthWrapper:React.FC<{auth:string|string[],children:JSX.Element}> = ({auth, children})=>{
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth(auth), [hasAuth]);
+ if(typeof children === 'function'){
+ return children(authorized)
+ }
// 控制显隐
return authorized ? children : null
}
<AuthWrapper auth='edit'>
{
(authorized:boolean)=> authorized ? <button> 编辑按钮(rederProps) </button> : null
}
</AuthWrapper>
动态权限控制
这种主要是通过动态获取到的权限标识,来控制显隐、样式等。可以根据特定场景做特定的封装优化。主要逻辑其实是在后端处理。
结尾
可以看到在两大框架下实现权限控制时,思路和细节上还是稍稍有点不一样的,React给人的感觉是手上的积木更加零碎的一点,有些功能需要自己搭起来。相反Vue给人的感觉是面面俱到,用起来下限会更高。
最后
如果大家有什么想法和思考,欢迎在评论区留言~~。
另外:本人经验有限,
来源:juejin.cn/post/7242677017034915899