注册
web

Vue和React权限控制的那些事

自我介绍


看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。


前言


无论是后台管理系统,还是面向C端的产品,权限控制都是日常工作中常见的需求。在此梳理一下权限控制的那些逻辑,以及在Vue/React框架下是有什么样的解决方案。


什么是权限控制?


现在基本上都是基于RBAC权限模型来做权限控制


一般来说权限控制就是三种




  • 页面权限:说白了部分页面是具备权限的,没权限的无法访问




  • 操作权限:增删改查的操作会有权限的控制




  • 数据权限:不同用户看到的、数据是不一样的,比如一个列表,不同权限的查看这部分数据,可能有些字段是**脱敏的,有些条目无法查看详情,甚至部分条目是无法查看




那么对应到前端的维度,常见的就4种




  • 权限失效(无效)(token过期/尚未登录)




  • 页面路由控制,以路由为控制单位




  • 页面上的操作按钮、组件等的权限控制,以组件/按钮为最小控制单位




  • 动态权限控制,比如1个列表,部分数据可以编辑,部分部分不可编辑




image.png


⚠️注意: 本文一些方案 基于 React18 React-Router V6 以及 Vue3 Vue-Router V4


⚠️Umi Max 这种具备权限控制系统的框架暂时不在讨论范围内~~


前置条件


由于市面上各家实现细节不一样,这里只讨论核心逻辑思路,不考虑细节实现


无论框架如何,后端根据RABC角色权限这套逻辑下来的,会有如下类似的权限标识信息,可以通过专门的接口获取,或者跟登录接口放在一起。


image.png


然后根据这些数据,去跟路由,按钮/组件等,比对产生真正的权限


像这种权限标识一般都存在内存当中(即便存在本地存储也需要加密,不过其实真正的权限控制还是需要后端来控),一般都是全局维护的状态,配合全局状态管理库使用。


权限失效(无效)


image.png


这种场景一般是在发送某些请求,返回过期状态


或者跟后端约定一个过期时间(这种比较不靠谱)


通常是在 全局请求拦截 下做,整理一下逻辑


路由级别权限控制


通常前端配好的路由可以分为 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刷新进入这部分路由,会有白屏现象。


image.png


因为刷新进入的过程经历了 异步获取权限列表 --> 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}/>
)
}



包装路由(相当于路由守卫)


配置路由组件的时候,先渲染包装的路由组件


image.png


在包装的组件里做权限判断


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给人的感觉是面面俱到,用起来下限会更高。


最后


如果大家有什么想法和思考,欢迎在评论区留言~~。


另外:本人经验有限,

作者:JetTsang
来源:juejin.cn/post/7242677017034915899
如果有错误欢迎指正。

0 个评论

要回复文章请先登录注册