前端权限开发——设计到实践

1.权限控制的方案选择。 做后台项目区别于做其它的项目,权限验证与安全性是非常重要的,可以说是一个后台项目一开始就必须考虑和搭建的基础核心功能。在后台管理系统中,实现权限控制可以采用...

1.权限控制的方案选择。

做后台项目区别于做其它的项目,权限验证与安全性是非常重要的,可以说是一个后台项目一开始就必须考虑和搭建的基础核心功能。在后台管理系统中,实现权限控制可以采用多种方案:

attachments-2025-03-AoaxJvky67ea4eaab55f7,png

这里我选择了基于角色的访问控制(Role-Based Access Control,RBAC) 这是因为RBAC提供了一种灵活且易于管理的方式来控制用户对系统功能和资源的访问,也是目前最主流的前端权限方案选择。

在RBAC中,系统中的功能和资源被组织成角色,而用户则被分配到不同的角色。每个角色都有一组权限,定义了该角色可以执行的操作和访问的资源。通过给用户分配适当的角色,可以实现对用户的权限控制。

RBAC的好处之一是它简化了权限管理的复杂性。管理员只需管理角色和分配角色给用户,而不需要为每个用户单独定义权限。当需要对用户的权限进行修改时,只需调整其角色的权限即可。

此外,RBAC还支持灵活的权限组合,允许创建具有不同权限组合的角色,以适应不同用户的需求。它也便于扩展,可以随着系统的发展和需求的变化而调整和添加角色。

2.RBAC下的权限字段设计与管理模型

1.用户权限授权

是对用户身份认证的细化。可简单理解为访问控制,在用户身份认证通过后,系统对用户访问菜单或按钮进行控制。也就是说,该用户有身份进入系统了,但他不一定能访问系统里的所有菜单或按钮,而他只能访问管理员给他分配的权限菜单或按钮。 主要包括:

  • Permission(权限标识、权限字符串):针对系统访问资源的权限标识,如:用户添加、用户修改、用户删除。
  • Role (角色):可以理解为权限组,也就是说角色下可以访问和点击哪些菜单、访问哪些权限标识。

权限标识或权限字符串校验规则:

  • 权限字符串:指定权限串必须和菜单中的权限标识匹配才可访问
  • 权限字符串命名规范为:模块:功能:操作,例如:sys:user:edit
  • 使用冒号分隔,对授权资源进行分类,如 sys:user:edit 代表 系统模块:用户功能:编辑操作
  • 设定的功能指定的权限字符串与当前用户的权限字符串进行匹配,若匹配成功说明当前用户有该功能权限
  • 还可以使用简单的通配符,如 sys:user:*,建议省略为 sys:user(分离前端不能使用星号写法)
  • 举例1 sys:user 将于 sys:user 或 sys:user: 开头的所有权限字符串匹配成功
  • 举例2 sys 将于 sys 或 sys: 开头的所有权限字符串匹配成功 这种命名格式的好处有:
  1. 可读性和可理解性:使用模块、功能和操作的格式可以直观地表达权限的含义。每个部分都有明确的作用,模块表示特定的模块或子系统,功能表示模块内的某个功能或页面,操作表示对功能进行的具体操作。通过这种格式,权限名称可以更容易地被开发人员、管理员和其他人员理解和解释。
  2. 可扩展性和灵活性: 通过使用模块、功能和操作的格式,可以轻松地扩展和管理权限。每个模块、功能和操作都可以被单独定义和控制。当系统需要增加新的功能或操作时,可以根据需要添加新的权限字符串,而不需要修改现有的权限规则和代码。
  3. 细粒度的权限控制: 这种格式支持细粒度的权限控制,可以针对特定的功能和操作进行权限管理。通过将权限名称拆分为模块、功能和操作,可以精确地定义哪些用户或角色具有访问或操作特定功能的权限。
  4. 避免权限冲突: 使用模块、功能和操作的格式可以避免权限之间的冲突。不同模块、功能和操作的权限名称是唯一的,这样可以避免同名权限之间的混淆和冲突。

2.权限管理模型

关键数据模型如下:

  • 用户:登录账号、密码、角色
  • 角色:角色名称、角色权限字符、对应菜单、对应菜单下的权限
  • 菜单:菜单名称、菜单URL、菜单类型
  • 用户角色关系:用户编码、角色编码
  • 角色菜单关系:角色编码、菜单编码

关系图如下:

【用户】  <---多对多--->  【角色】  <---多对多--->  【菜单/权限】

3.实现思路与步骤

前端权限一般分为路由级权限和按钮级权限,这里我们先实现页面路由级的权限功能,按钮级的会在后面讲到。大致的思路如下图:

attachments-2025-03-kLsMPJOv67ea5803bf93d,jpg

上图是用户从登录-->路由守卫-->权限验证-->构建路由表-->跳转目标页面的一个简单的正向流程,可以看到,权限验证构建路由表这两步是发生在路由守卫这一步里,而实际上在开发设计阶段,我们还要做的准备有:

  1. 与后端确认权限字段及类型
  2. 确定路由表信息是由前端还是后端生成

为了方便理解,我们就按照这个正向流程来逐步实现,期间需要用到或者提前考虑设计到的内容,包括以上需要准备的两点,我会补充在步骤当中。

1.登录

登录成功后,获取到token,将token存到本地的sessionStorage里。

async login({ commit }, userInfo) {
    const { data } = await login(userInfo)
    const accessToken = 'Bearer ' + data.access_token
    if (accessToken) {
      sessionStorage.getItem(tokenTableName)
    } else {
      message.error(`登录接口异常,未正确返回${tokenName}...`)
    }
  }

接着,进行路由跳转到首页

import { useRouter } from 'vue-router';
const router = useRouter(); 
router.push('/');

如果考虑到页面token失效后,重新登陆后返回原先的路由地址,则需要在路由守卫中添加redirect字段用来存储当前的路由地址

 next({ path: '/login', query: { redirect: to.fullPath }, replace: true })

然后在登录页中,监听路由对象中的这个值,如果有值,那么在刚刚路由跳转时,就跳转到该路由地址,而非/首页,另外,还需要考虑到403,404页面。所以,添加了这些逻辑后,部分代码如下:

//login.vue
<script setup>
import { ref, watch, useRouter } from'vue';
import { useRouter } from'vue-router';
const redirect = ref('/');
const router = useRouter();

watch(
() => router.currentRoute.value.query.redirect,
  (redirectValue) => {
    redirect.value = redirectValue || '/';
  },
  { immediatetrue }
);

function handleRoute({
        return redirect.value === '/404' || redirect.value === '/403'
          ? '/'
          : redirect.value
      },
      
async handleSubmit() {
        loading.value = true
        try {
          await login(form.value)
          loading.value = false
          router.push(handleRoute());
        } catch {
         loading.value = false
        }
      },      
</script>

2.路由守卫与权限校验

当进行路由跳转时,会进入到路由守卫中,在路由守卫中:

  1. 路由守卫会首先判断有没有token,如果没有,先判断要去的路由地址是否包含在路由白名单内,如果要去的路由地址也不在路由白名单内,就会让用户跳转到登录页重新登陆。
  2. 如果有token,会先判断跳转的目标地址是否是登录页,如果是,则重新跳转到默认首页
  3. 此时,我们就需要对用户权限进行校验,首先,判断当前用户是否拥有角色信息,如果没有,就要获取用户的角色信息。

3.获取角色信息与权限信息

调取用户信息接口获取用户角色信息和权限信息,代码如下:

//store/user
 // 获取用户信息
  GetInfo({ commit, dispatch }) {
    returnnewPromise((resolve, reject) => {
      getUserInfo()
        .then(async (response) => {
          const result = response.data
          if (result.roles && result.roles.length > 0) {
            // 验证返回的roles是否是一个非空数组
            commit('SET_ROLES', result.roles)
            //permissions就是对应的用户权限信息
            commit('SET_PERMISSION', result.permissions)
          } else {
            // 如果当前用户没有角色,则赋值一个默认角色
            commit('SET_ROLES', ['ROLE_DEFAULT']) 
          }
          resolve(result)
          // GetInfo一旦失败就说明这个token不是过期就是丢失了,直接走catch并让调用方跳转路由
          if (!response.success) {
            reject(response.errorMsg)
          }
        })
        .catch((error) => {
          reject(error)
        })
    })
  },

根据之前上文提到的权限管理模型,从之间的关系是多对多,因此,role(角色)permssions(权限)的类型应为数组,其中的权限permssion这个字段的格式规范也在上文提及。在与后端约定好后,后端返回的信息如图:

attachments-2025-03-mor5eol967ea5aa545e0d,jpg

attachments-2025-03-08tM4k6x67ea5aad0287f,jpg

4.谁来生成动态路由表?

在成功获取到用户信息,拿到用户角色和对应权限后,此时,就需要根据权限生成对应的路由表了,到这里,我们需要思考一个问题:路由表是由后端提供还是由前端提供?A:前端根据权限生成路由表 B:后端生成路由表给前端

没错!答案是C:路由表可以由后端提供或由前端提供。 

两者的优劣分别是:

1、后端提供路由表:

优点

安全性高:后端负责验证和控制权限,可以确保只有授权的用户能够访问特定的路由和功能。

隐藏敏感信息:后端可以根据用户的角色和权限隐藏不应该被访问的敏感路由和数据。

适用于复杂的权限规则:后端可以使用更复杂的逻辑和规则来处理权限控制,例如基于用户角色、用户组、权限等的复杂控制逻辑。

缺点:

前后端耦合度高:由于路由表由后端提供,前端开发人员可能需要与后端开发人员密切协作,增加了协调和沟通的成本。

前端依赖后端:前端应用程序可能需要等待后端提供路由表后才能进行开发和测试,增加了开发的时间和依赖性。

2、前端提供路由表:

优点:

前后端解耦:前端可以独立开发和维护路由表,减少了与后端的依赖性和协调成本。更好的用户体验:

前端可以根据用户的角色和权限动态展示路由和功能,提供更灵活和个性化的用户体验。

缺点:

安全性较低:前端提供的路由表容易受到篡改和绕过,安全性相对较低。隐藏敏感信息的难度增加:

前端无法直接隐藏敏感路由和数据,需要依赖后端接口的授权验证来保护敏感信息。

最终,考虑到后台管理系统的安全性优先级最高,我选择了由后端存储,并生成返回路由表信息。 确定好了之后,你就会遇到一个坑:后端提供的路由表无法直接在前端路由中添加使用 这是因为,后端存储的路由表的结构只一个JSON对象。怎么解决,我们放到后面讲。

5.公共路由和动态路由

我们现在把整个路由分为两个部分,分别是公共路由动态路由(私有路由)

公共路由

公共路由顾名思义就是无论当前用户是什么角色,都会出现的路由部分。一般这样的路由分别有:首页驾驶舱、登录注册页、403页、404页。  这部分路由是在前端项目中提前写死的。以我的项目为例,我在src/config下创建一个publicRouter.js文件,然后里面放入基础路由信息:

//基础公共路由
exportconst constantRouterMap = [
{
path'/',
name'',
component() =>import('@/layout'),
redirect'/homePage',
children: [
   {
     path'/homePage',
     name'homePage',
     component() =>import('@/views/homePage/index.vue'),
   },
 ],
},
{
path'/login',
component() =>import('@/views/login'),
},
{
path'/403',
name'403',
component() =>import('@/views/403'),
},
{
path'/:pathMatch(.*)*',
component() =>import('@/views/404'),
}]

这里面都是路由懒加载的写法,这个没啥好说的。需要注意的是在vue3中,vue-router的版本是4.x以上,在跳转404页面时,如果你的写法是

{
 path"/404",
 name: "notFound",
 component:  () => import('@/views/404')
}

这样写会提示报错,这是因为vue-router4.x的版本官方推荐引入了这种新写法,配置一个通配符路由,匹配所有未被其他具体路由匹配的路径。其中 :pathMatch 是参数名,而 (.*)* 是参数的匹配模式,它使用正则表达式来匹配任意路径。

动态路由(私有路由)

上文提到的后端返回给前端当前用户的路由表信息,其实就是私有路由,这部分的路由是动态的。那么我们只需要把公共路由+动态路由=当前角色用户完整路由表。然鹅,后端返回给前端的动态路由是无法直接添加到当前页面路由中的,这是因为在浏览器的前后端请求当中,信息的返回体都是以JSON字符串的数据交换格式进行传输,而JSON字符串是不支持函数的。为什么要提到函数形式呢?因为当我们在路由表使用懒加载的写法时,component的值是一个异步函数。也只有异步函数才能实现对路由的异步懒加载。import()会返回一个promise,这个 promise 最终会加载对应的组件模块。在格式上表现为一个func函数,因此,我们无法将compnent的值传给后端存储但是办法总比困难多,我们可以将对应的路径字符串传给后端存储,然后再通过后端返回的路径字符串转换成这种箭头函数的写法。 除了这个需要转换以外,我们还要考虑一些问题:

  1. 比如,转换后的路由结构要符合router中的路由格式,否则会在调用router.addRoutes时报错,添加失败。
  2. 添加一些其他自定义属性(例如添加导航栏菜单图标、某个路由的显隐、路由缓存、跳转外链接、...),以满足特定需求。 以我项目中的的路由示例,直接上代码看:
{
    "path""/",  //path,路由的路径,即访问该路由时的 URL 路径。
    "name""homePage",//name,路由的名称,用于在代码中标识路由。通常用于编程式导航。
    "hidden"false,//hidden,是否隐藏路由,在Vue Router 4.0 中被废弃。
    "component""Layout"//路由对应的组件,可以是通过懒加载方式导入的异步组件,或直接引入的同步组件。
    "meta": {//meta,路由元信息,可以用于存储一些额外的信息,比如页面标题、权限等
        "title""首页",   //路由标题
        "icon""icon-Home"//路由图标
        "target"""//是否跳转新页签
        "permission""homePage",//权限
        "keepAlive"false//是否缓存
    },
    "redirect""/homePage",//重定向
    "fullPath""/",//完整的url路径,会带上?后面的参数
    "children": [//子路由
        {
            "path""/homePage",
            "name""homePage",
            "meta": {
                "title""首页",
                "icon""",
                "target""",
                "permission""homePage",
                "keepAlive"false
            },
            "fullPath""/homePage"
        }
    ]
}

考虑完这些之后,我们就开始拿着后端返回的路由表信息动手转换吧! 先看看后端返回给前端的路由表格式内容


  • 发表于 2025-03-31 16:13
  • 阅读 ( 61 )

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
shitian
shitian

662 篇文章

作家榜 »

  1. shitian 662 文章
  2. 石天 437 文章
  3. 每天惠23 33 文章
  4. 小A 29 文章