uni-app 权限管理深度解析与最佳实践

446 阅读40分钟

1. uni-id 权限管理基石

uni-app 生态系统中的权限管理主要依赖于 uni-id,这是一个功能全面的用户身份和权限管理框架。理解 uni-id 的核心概念是构建安全、可扩展应用的前提。

1.1. uni-id 中的 RBAC 模型:用户、角色与权限详解

uni-id 的权限体系构建于经典的基于角色的访问控制(Role-Based Access Control, RBAC)模型之上 。该模型通过引入“角色”作为用户和权限之间的桥梁,极大地简化了权限管理和分配的复杂性。  

  • 用户 (User) :系统的操作者,是权限的最终载体。在 uni-id 中,用户信息存储在 uni-id-users 表中。一个用户可以被赋予一个或多个角色,这意味着用户可以拥有不同角色所包含的权限集合 。  
  • 角色 (Role) :权限的集合,代表了一组特定的职责或访问级别,例如“内容编辑员”、“系统管理员”等。角色信息存储在 uni-id-roles 表中。每种角色对应一组相应的权限,当用户被分配了某个角色后,便拥有了该角色下的所有权限 。  
  • 权限 (Permission) :对特定资源或操作的许可,是权限控制的最小单元。例如,“创建文章” (article_create)、“删除用户” (user_delete) 或“查看评论” (comment_view)。权限信息存储在 uni-id-permissions 表中 。  

这种用户-角色-权限的层级关系,其核心优势在于将用户与具体的权限解耦。当需要对一批用户的权限进行调整时,仅需修改这些用户共同拥有的角色所关联的权限即可,无需逐个修改每个用户的权限设置。这种设计显著提升了系统的可管理性和可扩展性,特别是在用户基数庞大或权限体系复杂的应用中,其优势尤为突出。如果权限直接授予用户,那么当一个新权限需要赋予100个用户,或者一个现有权限需要从100个用户中撤销时,将涉及100次独立的用户数据更新操作。而在RBAC模型下,如果这100个用户共享一个或多个角色,那么权限的调整只需在相关的角色上进行一次即可,大大降低了管理成本和出错的风险。

此外,permission_id(权限标识)的设计至关重要。uni-id 建议 permission_id 使用全局唯一的、具有语义的字符串,例如 USER_DEL 代表删除用户的权限 。这种命名方式不仅增强了代码的可读性,使得权限的意图一目了然,而且这些 permission_id 也直接用于 clientDB 的 schema 权限规则中,例如 "'USER_EDIT' in auth.permission" 。如果 permission_id 缺乏语义(如仅使用数字)或不唯一,将会严重影响 schema 规则的可理解性和系统的正确性,可能导致权限冲突或错误的权限判断。因此,在项目初期就对 permission_id 进行审慎规划和统一命名是确保权限系统稳健运行的基础。  

1.2. uni-id 权限相关的核心数据库表

uni-id 的权限数据主要存储在云数据库的几张核心表中,它们共同构成了权限系统的骨架。

  • uni-id-users 用户表: 此表存储用户的基本信息,如 _id (用户ID)、usernamepassword (加密存储)等。与权限直接相关的是 role 字段,它是一个数组类型,用于存储该用户拥有的所有角色ID (即 uni-id-roles 表中的 role_id) 。此外,status 字段(表示用户状态,如正常、禁用)和 dcloud_appid 字段(数组类型,允许多个应用复用同一用户表时进行隔离)也间接影响权限判断的上下文 。  
  • uni-id-roles 角色表: 此表定义了系统中的各种角色。关键字段包括 role_id (角色唯一标识,字符串类型,建议语义化命名,如 USER_ADMIN)、role_name (角色显示名称) 以及 permission 字段。permission 字段是一个数组,存储了该角色拥有的所有权限ID (即 uni-id-permissions 表中的 permission_id) 。特别地,uni-id 预置了 admin 角色作为超级管理员,默认拥有所有数据表的所有权限 。  
  • uni-id-permissions 权限表: 此表存储了系统中所有原子化的权限。关键字段包括 permission_id (权限唯一标识,字符串类型,如 USER_ADD) 和 permission_name (权限显示名称) 。uni-id 规定权限总数不能超过500个 。  

这些表结构的设计体现了权限管理的灵活性。例如,uni-id-users.roleuni-id-roles.permission 字段均采用数组类型,这使得用户与角色、角色与权限之间可以建立灵活的多对多关系。一个用户可以同时拥有多个角色(例如,一个用户既是“编辑”也是某个“部门管理员”),一个角色也可以包含多个权限。如果这些字段设计为单值,将极大地限制权限模型的表达能力,无法适应真实的、复杂的组织结构和访问需求。

值得注意的是 uni-id-users 表中的 dcloud_appid 字段 。该字段允许存储一个 appid 列表,标识了用户有权登录的客户端应用。这为在多个 uni-app 应用间共享同一套 uni-id 用户体系提供了内置支持,同时又能实现应用级别的用户访问隔离。若无此机制,开发者可能需要为每个应用部署独立的 uni-id 实例,从而增加管理和维护的复杂度。结合角色和权限系统,dcloud_appid 使得开发者不仅能控制用户能做什么,还能控制用户在哪个应用里能做。  

1.3. uni-id-common 与基于 Token 的身份验证

uni-id-commonuni-id 体系中的一个核心云端公共模块。它非常精简,主要包含了账户体系服务端的 Token 管理和权限校验的核心逻辑,内置在每个 uniCloud 服务空间中 。当开发者需要在自己的云函数或云对象中验证前端用户的 Token 身份时,就需要引用此模块 。  

uni-id 使用 JWT (JSON Web Tokens) 来生成和管理用户 Token 。当用户成功登录后,服务端会下发一个 Token 给客户端。这个 Token 中会缓存用户的角色和权限信息 。这样做的一个显著好处是,在后续的 checkToken(Token校验)操作中,可以减少甚至消除对数据库的查询次数,从而有效节省云资源费用并缩短接口响应时间 。需要明确的是,JWT Token 的载荷部分(payload)存储的信息是明文的,uni-id 主要依赖 tokenSecret(在配置文件中设置)来校验客户端 Token 的合法性,确保其未被篡改 。  

Token 的行为,如有效期、自动续期阈值等,可以在 uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json 文件中进行配置 。关键配置项包括:  

  • tokenSecret: 用于生成和校验 Token 的密钥,务必修改为自定义的复杂字符串。
  • tokenExpiresIn: Token 的全局默认有效期(秒)。
  • tokenExpiresThreshold: Token 自动续期阈值(秒)。当 Token 剩余有效期小于此阈值但仍有效时,checkToken 会自动下发新的 Token。 此外,还支持针对不同平台(如 app、h5、各小程序平台)设置独立的 Token 有效期和续期阈值 。  

将角色和权限信息缓存在 Token 中,虽然带来了性能上的提升,但也引入了一个潜在的问题:数据一致性的延迟。当管理员在后端修改了用户的角色或某个角色的权限后,用户持有的现有 Token 仍然包含旧的权限信息,直到该 Token 过期并重新获取,或者通过 refreshToken 接口主动刷新。这意味着在 Token 刷新之前,用户的实际权限可能与 Token 中缓存的权限存在短暂的不一致。

tokenExpiresThreshold 配置项 在此背景下显得尤为重要。它不仅用于在 Token 完全过期前自动续期以提升用户体验,也间接成为了一个控制权限数据“新鲜度”的手段。当 Token 被刷新时(无论是通过 checkToken 的自动续期逻辑,还是主动调用 refreshToken API),uni-id 会重新从数据库获取用户最新的角色和权限信息并写入新的 Token 中 。因此,一个相对较短的 tokenExpiresThreshold(例如,设置为 tokenExpiresIn 的一半或更小)意味着 Token 的更新会更加频繁,从而更快地同步权限变更,但这也会相应增加 Token 刷新操作的频率。开发者需要根据应用的安全敏感度和性能要求来权衡此配置。  

表1: uni-id 权限系统核心组件

组件名称类型关键字段/用途
uni-id-users数据库表_id(用户ID)
role(角色ID数组)
status
dcloud_appid
uni-id-roles数据库表_id
role_id(唯一字符串)
role_name
permission(权限ID数组)
uni-id-permissions数据库表_id
permission_id(唯一字符串)
permission_name
uni-id-common云公共模块提供 checkToken/createToken/refreshToken
处理 Token 校验与权限缓存
uni-id-co云对象高层用户管理功能(登录/注册等)
基于 uni-id-common 构建
JWT Token数据载体缓存 uid/role/permission
用于客户端与服务端快速校验
config.json配置文件tokenSecret
tokenExpiresIn
tokenExpiresThreshold

2. uni-admin 后端权限控制

uni-admin 是一个基于 uni-id 的后台管理框架,它复用了 uni-id 的用户、角色、权限系统,并提供了可视化的管理界面 。  

2.1. 通过 uni-admin 界面管理用户、角色和权限

uni-admin 提供了一个直观的 Web 界面,使得管理员可以方便地进行权限相关的操作 :  

  • 用户管理:可以创建新用户、查看用户列表、编辑用户信息(包括为用户分配一个或多个角色)、禁用/启用用户等。
  • 角色管理:可以创建新角色、定义角色的 role_id 和名称、为角色分配具体的权限、编辑或删除现有角色。
  • 权限管理:可以创建新权限、定义权限的 permission_id 和名称、编辑或删除现有权限。

一个典型的操作流程可能如下:

  1. 在“权限管理”中新增一个权限,例如“查询用户信息”,标识为 USER_READ 。  
  2. 在“角色管理”中新增一个角色,例如“普通成员”,标识为 member,并将 USER_READ 权限分配给此角色 。  
  3. 在“用户管理”中创建一个新用户(例如“张三”),并将“普通成员” (member) 角色赋予该用户 。  

uni-admin 的可视化管理界面极大地降低了权限管理的门槛。它将底层的数据库表操作抽象为用户友好的表单和列表,使得非开发人员(如系统管理员或运营人员)也能够进行日常的权限配置和调整,而无需直接操作数据库 。这在一定程度上解放了开发者的精力,使其可以更专注于业务功能的实现。  

然而,尽管 uni-admin 简化了操作层面,但权限系统的根基——即 permission_id 的定义及其所代表的业务含义——仍然需要由开发者来主导设计。如果权限定义得不清晰(例如,权限粒度过粗、命名模糊),那么即使 uni-admin 界面再便捷,管理员也难以准确地进行权限分配。运营人员只能基于开发者预先定义好的权限进行组合和分配。因此,一个高效的 uni-admin 权限管理实践,是建立在开发者对权限进行合理规划和清晰定义的基础之上的。

2.2. 动态菜单管理:将菜单可见性 (opendb-admin-menus) 与用户权限关联

uni-admin 的一个核心特性是其动态菜单系统,即左侧导航菜单的显示内容会根据当前登录用户的权限动态生成 。  

菜单数据存储在云数据库的 opendb-admin-menus 表中 。此表中的关键字段与菜单权限控制相关的是 permission 字段。这是一个数组类型的字段,用于存储一个或多个 permission_id。该字段通常只对没有子菜单的叶子节点菜单项进行配置 。  

其工作机制是:用户登录 uni-admin 时,系统会首先根据用户的角色获取其拥有的所有权限 permission_id 列表。然后,在渲染菜单时,系统会检查每个菜单项(特别是叶子节点)的 permission 字段。如果用户拥有的权限列表中包含了该菜单项 permission 字段中指定的任何一个 permission_id,则该菜单项对用户可见;否则,该菜单项将被隐藏 。  

例如,管理员可以在“菜单管理”中找到“用户管理”这个菜单项,然后在其权限列表中勾选之前创建的 USER_READ 权限。这样配置后,只有拥有 USER_READ 权限的用户(例如,被赋予了“普通成员”角色的“张三”)才能在 uni-admin 的侧边栏看到“用户管理”这个菜单 。  

通过菜单控制权限是UI层面的一种权限实施方式。它能够有效地阻止用户看到他们未被授权访问的功能入口,从而提升用户体验,并提供第一层安全防护。但这本身并不足以保证系统的安全性。如果一个恶意用户知道了某个受限页面的直接URL,他们仍可能尝试访问。因此,菜单级别的权限控制必须与后续的API接口层面和数据层面的权限校验相结合,形成纵深防御。

opendb-admin-menus 表的设计将 permission 字段的配置限制在叶子节点菜单项上 ,这简化了权限判断逻辑。但如果一个父级目录菜单(非叶子节点)本身也需要根据权限来决定是否显示,就需要间接实现。例如,一个父菜单“系统设置”是否可见,取决于其下所有子菜单(如“用户管理”、“角色管理”)是否都因权限不足而对当前用户隐藏。如果希望对父菜单的可见性有更直接的控制,可能需要更扁平化的菜单结构,或者在前端进行一些自定义的逻辑处理。  

2.3. 在 uni-admin 页面中使用 $hasPermission$hasRole 进行 UI 控制

为了方便在 uni-admin 的 Vue 页面内部进行更细致的 UI 元素权限控制,uni-admin 框架内置了两个便捷的辅助方法:$hasPermission('PERMISSION_ID')$hasRole('ROLE_ID') 。  

这两个方法可以直接在 Vue 模板中使用,通过 v-ifv-show 等指令,根据当前登录用户的权限或角色来动态决定页面元素的显示或隐藏。例如:

  • <button v-if="$hasPermission('USER_ADD')">新增用户</button>:只有拥有 USER_ADD 权限的用户才能看到“新增用户”按钮。
  • <div v-if="$hasRole('admin')">管理员专属操作区</div>:只有拥有 admin 角色的用户才能看到此区域。

这些前端辅助方法依赖于用户登录 uni-admin 时缓存在 Token 中的权限数据。因此,它们的判断速度非常快,无需为每次检查都向后端发起网络请求。但也正因为如此,它们同样受到 Token 缓存数据可能存在的短暂“过时”问题的影响(如1.3节所述)。如果管理员A修改了用户B的权限,用户B在其当前会话中(Token 未刷新前)通过这些辅助方法判断的结果可能仍然是旧的权限状态。因此,对于 uni-admin 内部执行的、涉及高度敏感信息或关键业务流程的操作,如果要求权限变更的即时生效性,那么仅依赖这些前端UI辅助方法可能是不够的,仍需后端接口进行最终校验。

$hasPermission$hasRole 两个方法的存在提供了权限判断的灵活性。基于角色的判断 ($hasRole) 是一种较粗粒度的控制,而基于权限的判断 ($hasPermission) 则更为精细。通常,更推荐的做法是优先使用 $hasPermission 进行判断。这是因为权限代表了执行某个具体操作的能力,而角色是权限的集合。如果UI元素的显隐直接与某个操作能力(即权限)挂钩,那么当角色定义发生变化(例如,新增一个也需要该操作能力的角色,或调整现有角色的权限构成)时,UI代码无需修改。反之,如果UI代码强依赖于特定的角色名,那么在角色体系调整时,可能需要多处修改UI代码,增加了维护成本和出错的风险。这种做法也更符合RBAC的核心思想:定义操作(权限),然后将操作能力赋予角色,代码逻辑则检查是否具备操作能力。

2.4. 保障数据访问安全:为 uni-admin 操作配置 clientDB Schema 权限

虽然菜单的动态显示和页面元素的条件渲染控制了用户在 uni-admin 中的操作入口和可见界面,但最终的数据读取和写入操作(uni-admin 大量使用 clientDB 进行数据交互)必须在数据库层面得到安全保障。这需要通过配置相应数据表的 DB Schema 文件中的 permission 规则来实现 。  

例如,在2.1节的例子中,即使用户“张三”因为拥有 USER_READ 权限而看到了“用户管理”菜单,并点击进入了用户列表页面,如果 uni-id-users 表的 Schema 文件没有正确配置读权限,那么在尝试获取用户数据时,clientDB 仍会返回“权限校验未通过”的错误 (步骤7)。要允许“张三”(其角色 member 拥有 USER_READ 权限)读取用户列表,需要在 uni-id-users.schema.json 文件中,将 permission 节点下的 read 权限配置为类似 "'USER_READ' in auth.permission" 的表达式 (步骤8)。  

这清晰地展示了一个多层级的安全防护策略:uni-admin 的UI控制(菜单显隐、按钮显隐)是第一道防线,而 clientDB 的 Schema 权限则是针对数据操作的、更具权威性的第二道防线。两者必须协同工作,保持一致,才能确保系统的整体安全。

在 Schema 权限表达式中可用的 auth 对象(包含 uid, role, permission 等用户信息)是由 uniCloud 在处理 clientDB 请求时,通过验证客户端传递的 Token 后填充的。这意味着 clientDB 的 Schema 权限判断是在服务端基于一个已验证的、可信的 Token 内容进行的。即便客户端的 Token 缓存可能存在短暂的“过时”,或者客户端尝试恶意构造请求,最终的数据访问决策权仍在服务端,由 Schema 规则依据经过验证的 Token 信息来做出。这使得 clientDB Schema 权限成为一种非常可靠的服务端强制执行机制。

表2: uni-admin 菜单权限配置 (opendb-admin-menus)

字段名 (在 opendb-admin-menus 中)数据类型权限控制中的作用
menu_idString菜单项的唯一标识
nameString菜单的显示名称
urlString叶子节点菜单项对应的页面链接
parent_idString父级菜单的ID,定义菜单层级结构
permissionArray核心字段: 权限ID (permission_id) 数组。用户需拥有其中至少一个权限才能看到此菜单项。仅对叶子节点配置。
enableBoolean菜单是否启用

 

3. 前端权限策略 (小程序, H5, App)

在 uni-app 的各类前端应用(包括小程序、H5 和 App)中,权限判断主要用于提升用户体验,例如动态展示或隐藏界面元素、控制页面跳转等。

3.1. 客户端权限校验:uniIDHasRole()uniIDHasPermission()

uni-app 框架在 HBuilderX 3.1.15+ 版本后,全局提供了两个 API 用于在客户端判断当前用户的角色和权限 :  

  • uniIDHasRole('ROLE_ID'): 接收一个角色ID (role_id) 作为参数,返回布尔值,判断当前登录用户是否拥有该角色 。  
  • uniIDHasPermission('PERMISSION_ID'): 接收一个权限ID (permission_id) 作为参数,返回布尔值,判断当前登录用户是否拥有该权限 。需要特别注意的是,如果用户拥有 admin 角色,此 API 通常会直接返回 true,因为 admin 角色被认为拥有所有权限,即使其 Token 中的权限列表可能为空 。  

这两个 API 的工作机制是读取并解析客户端本地存储(通常是 localStorage 或小程序的 storage)中的 uni_id_token 。用户的角色和权限信息在登录成功后由服务端下发并缓存于此 Token 内 。因此,uniIDHasRole()uniIDHasPermission() 的执行速度非常快,因为它们不涉及网络请求 。但也正因如此,它们本身并不校验 Token 的合法性(如签名是否正确)或时效性(是否已过期);它们仅仅是读取 Token 中已缓存的数据进行判断 。  

这两个 API 可以在 Vue 模板中直接使用,例如通过 v-if="uniIDHasRole('admin')" 来控制管理员入口的显示;也可以在页面的 JavaScript 逻辑中使用,例如 if (this.uniIDHasPermission('edit_article')) {... } 。  

虽然这些客户端API非常便捷,但其便利性和速度是建立在一定假设之上的。它们操作的是可能“过时”的数据(如果后端权限已更新但Token未刷新),理论上也可能操作的是被篡改的数据(尽管在常规环境下,用户直接篡改经过签名的JWT Token并使其依然有效是非常困难的,但本地存储的安全性总归低于服务端)。因此,这些API主要应用于UI/UX层面的优化,例如根据用户权限动态调整界面显示,避免用户看到其无权操作的选项。绝对不能将它们作为关键业务操作或敏感数据访问的唯一安全屏障。 所有关键性的权限校验都必须在服务端进行。

关于 admin 角色的特殊处理,文档中提到“admin角色的用户拥有所有权限” ,并且 uni-id-commoncheckToken 在用户角色包含 admin 时返回的 permission 数组为空 。这意味着 uniIDHasPermission API 在客户端进行判断时,很可能内置了特殊逻辑:如果检测到用户拥有 admin 角色(通过 uniIDHasRole('admin')),则无论请求判断的是何种具体权限,都会返回 true。开发者应了解这一行为特性。  

3.2. 条件化UI渲染:动态显示/隐藏元素

uniIDHasRole()uniIDHasPermission() 最直接的应用场景就是在 Vue 模板中使用 v-ifv-show 指令,动态地控制页面元素的渲染和显示。

例如:

  • 隐藏“编辑”按钮,如果用户不具备 article_edit 权限:

    <button v-if="uniIDHasPermission('article_edit')">编辑文章</button>
    
  • 显示一个“后台管理”的入口链接,仅当用户角色为 admin 时:

    <navigator url="/pages/admin/index" v-if="uniIDHasRole('admin')">进入后台</navigator>
    

这种做法能够根据用户的权限级别,为用户呈现一个“干净”且相关的界面,避免了不必要的干扰,从而提升了整体的用户体验。

然而,必须清醒地认识到,这种基于客户端权限判断的UI元素显隐,本质上是一种“君子协定”,它仅仅是视觉上的隐藏。如果一个略懂技术的用户通过浏览器开发者工具分析页面结构,或者直接调用页面脚本中的方法,依然可能尝试执行那些被隐藏按钮对应的操作。因此,任何通过这些UI元素触发的、涉及数据修改或敏感信息访问的后端请求(如调用云函数),其对应的服务端逻辑必须重新进行严格的权限校验。前端的条件渲染更多是UX优化,而非安全保障。

当UI中存在基于多个权限或角色组合的复杂条件判断时,直接在模板中编写冗长的 v-if 表达式会降低代码的可读性和可维护性。例如,一个操作按钮可能需要用户同时拥有 permissionA 并且(拥有 roleX 或拥有 permissionB)。此时,推荐在 Vue 组件的 computed (计算属性) 或 methods (方法) 中封装这些复杂的判断逻辑:

// 在页面的 <script> 中
export default {
  //...
  computed: {
    canExecuteSpecialAction() {
      // 假设 uniIDHasPermission 和 uniIDHasRole 已正确混入或可用
      const hasPermA = this.uniIDHasPermission('permissionA');
      const hasRoleX = this.uniIDHasRole('roleX');
      const hasPermB = this.uniIDHasPermission('permissionB');
      return hasPermA && (hasRoleX |
| hasPermB);
    }
  }
}

然后在模板中可以简洁地使用:

<button v-if="canExecuteSpecialAction">执行特殊操作</button>

这种方式将复杂的权限判断逻辑内聚到组件的脚本部分,使得模板更清晰,逻辑也更易于测试和复用。

3.3. 页面级访问控制:实现路由/导航守卫

页面级的访问控制旨在阻止用户导航到他们未被授权访问的页面或路由。这比仅仅隐藏导航链接更为安全,因为它可以拦截用户直接通过URL访问页面的尝试。

使用 uni.addInterceptor 实现

uni-app 框架提供了 uni.addInterceptor API,可以用来拦截框架内置的路由跳转方法,如 uni.navigateTo, uni.redirectTo, uni.switchTab, uni.reLaunch 。通过为这些方法添加拦截器,可以在导航实际发生前执行权限检查逻辑。  

在拦截器的 invoke 函数内部,可以获取目标页面的路径 args.url。然后,可以定义一个页面访问白名单 (如登录页、错误提示页) 和一个需要权限控制的页面配置表。根据目标URL,结合 uni.getStorageSync('uni_id_token') (检查是否登录) 以及 uniIDHasRole() / uniIDHasPermission() (检查具体角色/权限),来决定是否允许导航,或者重定向到登录页或权限不足提示页。

以下是一个概念性的示例代码结构(具体实现需结合项目实际情况调整):

// 在 main.js 或 App.vue 的 onLaunch 中配置
// 注意:此处的 uniIDHasRole/uniIDHasPermission 需要确保在调用时已可用
// 可能是通过 Vue.prototype 挂载,或在 Vuex/Pinia store 中提供

const whiteList = ['/pages/login/login', '/pages/common/error/403']; // 路由白名单
const pagePermissions = { // 页面权限配置
  '/pages/admin/dashboard': { roles: ['admin', 'editor'], permissions: ['view_dashboard'] },
  '/pages/user/profile-edit': { requiresLogin: true }, // 简单登录检查
  '/pages/vip/content': { permissions: ['access_vip_content'] }
};

function checkAuth(url) {
  const token = uni.getStorageSync('uni_id_token');
  const pageRule = pagePermissions[url.split('?')]; // 移除query参数比较

  if (whiteList.includes(url.split('?'))) {
    return true; // 白名单页面直接放行
  }

  if (!token) { // 未登录
    if (pageRule) { // 且目标页需要权限
      uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent(url) });
      return false;
    }
    return true; // 目标页不需要权限,未登录也可访问
  }

  // 已登录,检查页面具体权限
  if (pageRule) {
    let hasAccess =!pageRule.requiresLogin; // 如果仅需登录,则已满足

    if (pageRule.roles && pageRule.roles.length > 0) {
      hasAccess = pageRule.roles.some(role => uni.uniIDHasRole(role));
    }
    if (hasAccess && pageRule.permissions && pageRule.permissions.length > 0) {
      // 如果同时配置了roles和permissions,则需同时满足(或根据业务调整为或)
      // 此处示例为:满足任一role后,再检查是否满足任一permission
      hasAccess = pageRule.permissions.some(perm => uni.uniIDHasPermission(perm));
    } else if (!pageRule.roles && pageRule.permissions && pageRule.permissions.length > 0) {
      // 仅配置permissions
      hasAccess = pageRule.permissions.some(perm => uni.uniIDHasPermission(perm));
    }


    if (!hasAccess) {
      uni.navigateTo({ url: '/pages/common/error/403' });
      return false;
    }
  }
  return true; // 默认放行或目标页无特定权限规则
}

.forEach(method => {
  uni.addInterceptor(method, {
    invoke(args) {
      if (!checkAuth(args.url)) {
        return false; // 返回false阻止原生跳转
      }
      return args; // 放行时需要返回args或true
    },
    fail(err) {
      console.error(`拦截 ${method} 失败:`, err);
    }
  });
});

( 指向 uni.addInterceptor 的文档, 详细说明了其用法)。  

使用第三方路由插件 (如 uni-simple-router)

对于更复杂的路由管理需求,可以考虑使用如 uni-simple-router 这样的第三方路由插件 。这类插件通常提供了更完善的路由配置方式和专门的导航守卫机制(如 beforeEach 全局前置守卫),使得定义路由元信息(如页面所需角色/权限)和实现全局权限校验更为结构化和便捷 。  

路由守卫相较于仅隐藏导航链接或按钮,是一种更强的客户端防护措施,因为它能在用户尝试通过任何方式(包括直接输入URL)访问受限页面时进行拦截。然而,它依然是客户端的技术,其代码和逻辑都存在于前端包中。有经验的用户仍可能通过分析前端代码或绕过客户端JavaScript来达到访问页面的目的(尽管页面内容可能因后续API调用失败而无法正确加载)。因此,路由守卫保护的页面,其内部加载的任何敏感数据或执行的任何关键操作,都必须依赖于服务端的进一步权限校验。

为了使路由权限管理更易于维护,特别是对于大型应用,建议将各路由所需的权限元数据进行集中管理,而不是将判断逻辑散布在 addInterceptorinvoke 函数中。例如,可以创建一个独立的配置文件或模块来维护路由与权限的映射关系。uni-simple-router 这类插件通常通过路由配置中的 meta 字段来支持这种元数据的定义,使得在 beforeEach 守卫中可以方便地获取和使用这些信息。一个良好组织的路由权限定义策略,与守卫机制本身同等重要。

4. uniCloud 服务端权限强制执行

客户端的权限控制(无论是UI显隐还是路由守卫)本质上都是为了提升用户体验和提供初步的访问过滤。真正的安全防线必须设在服务端。所有关键的业务逻辑和数据操作,都应在 uniCloud 的云函数或云对象中进行严格的权限校验。

4.1. 在云函数和云对象中使用 uni-id-common 校验权限

uni-id-common 公共模块及其核心方法 checkToken(token) 是服务端权限校验的基石 。  

checkToken(token) 方法的主要职责是:

  1. 接收从客户端传递过来的 uniIdToken

  2. 验证该 Token 的合法性(包括签名校验、是否过期等)。

  3. 如果 Token 有效,则解析并返回一个包含用户身份信息的对象,其中通常有 uid (用户ID)、role (用户角色ID数组)、permission (用户权限ID数组) 等字段 。  

    • 特别注意:如果用户的 role 数组中包含 admin,那么返回的 permission 数组将为空。因此,在服务端逻辑中判断权限时,如果 admin 角色意味着拥有所有权限,则需要显式地检查用户是否具有 admin 角色,而不能仅仅依赖 permission 数组的内容 。  

在云函数或云对象中的典型权限校验流程如下:

  1. 从调用事件对象 eventthis.getUniIdToken() (云对象中) 获取客户端传递的 uniIdToken

  2. 引入并实例化 uni-id-common

    const uniID = require('uni-id-common');
    // 在云函数中:
    const uniIDIns = uniID.createInstance({ context: context });
    // 在云对象的方法中 (通常在 _before 方法中初始化 this.uniID):
    this.uniID = uniID.createInstance({ clientInfo: this.getClientInfo() });
    
  3. 调用 checkToken 方法进行校验:

    // 假设 uniIdToken 已获取
    const payload = await uniIDIns.checkToken(uniIdToken); // uniIDIns 指向已创建的实例
    
  4. 检查 payload.code 是否为0。若不为0,则表示 Token 无效或发生错误,应中断操作并返回错误信息。

  5. payload.code === 0,则可以安全地使用 payload.uidpayload.rolepayload.permission 来进行后续的业务权限判断。 例如,判断用户是否有权发布文章:

    if (payload.code!== 0) {
      return { errCode: 'TOKEN_INVALID', errMsg: '用户身份验证失败,请重新登录' };
    }
    
    // 检查是否为 admin 角色,或者是否拥有 'publish_article' 权限
    const isAdmin = payload.role && payload.role.includes('admin');
    const hasPublishPermission = payload.permission && payload.permission.includes('publish_article');
    
    if (!isAdmin &&!hasPublishPermission) {
      return { errCode: 'PERMISSION_DENIED', errMsg: '抱歉,您没有发布文章的权限' };
    }
    
    // 用户权限校验通过,继续执行发布文章的业务逻辑...
    //...
    

服务端的 checkToken 校验是权限控制的最终权威。它能够有效抵御来自客户端的各种潜在风险,例如使用过期的 Token、尝试绕过客户端校验逻辑等。因为 checkToken 会严格依据 tokenSecret 验证 Token 签名,并基于 Token 内(或刷新时从数据库获取的)角色和权限信息进行判断。

然而,需要理解的是,即便是服务端的 checkToken,它在验证通过后返回的 rolepermission 信息也是源自 Token 被签发那一刻的状态。如果在此之后、Token 刷新或过期之前,管理员在后台更改了用户的权限配置,那么 checkToken 返回的权限信息相对于数据库中的最新状态而言,可能也是“过时”的。对于绝大多数应用场景,这种短暂的不一致是可以接受的,因为 Token 本身是经过验证且可信的。但对于那些对权限变更即时性要求极高的、极度敏感的操作,开发者可以考虑在 checkToken 成功之后,额外从数据库中查询一次用户当前最新的角色和权限信息,以确保万无一失。或者,更积极地管理 Token 的生命周期,使用较短的有效期和续期阈值。uni-idrefreshToken API 在刷新 Token 时,会重新从数据库加载用户的角色权限信息,这本身就是一种获取最新权限的机制 。  

表3: 关键权限检查API和方法 (汇总)

API/方法上下文 (环境)主要用途关键输入关键输出 (若适用)是否依赖Token缓存?服务端验证?参考资料
$hasPermission(permId)uni-admin (Vue 页面)uni-admin 后台的条件化UI渲染permission_id (String)Boolean否 (纯客户端)
$hasRole(roleId)uni-admin (Vue 页面)uni-admin 后台的条件化UI渲染role_id (String)Boolean否 (纯客户端)
uniIDHasPermission(pId)uni-app 前端 (Vue/JS)条件化UI, 客户端逻辑permission_id (String)Boolean否 (纯客户端)
uniIDHasRole(roleId)uni-app 前端 (Vue/JS)条件化UI, 客户端逻辑role_id (String)Boolean否 (纯客户端)
uniCloud.getCurrentUserInfo()uni-app 前端 (JS)客户端获取基本用户信息、角色、权限-UserInfo 对象 (uid, roles 等)否 (纯客户端)
uniIDIns.checkToken(token)uniCloud (云函数/对象)服务端Token验证与权限信息获取uniIdToken (String)Payload (uid, role, permission)读取Token内容是 (Token本身)
DB Schema permissionuniCloud (clientDB评估)服务端数据访问控制auth 对象 (源自Token)访问被授予/拒绝读取Token内容是 (经由uniCloud)

 

5. 使用 clientDB Schema 实现高级数据访问控制

当应用选择使用 clientDB 允许前端直接操作数据库时,DB Schema 文件中的 permission 规则就成为保障数据安全的生命线 。  

5.1. 在 DB Schema 中制定 permission 规则以实现细粒度控制

每个数据集合(表)都可以拥有一个对应的 .schema.json 文件,例如 uni-id-users.schema.json。这个文件不仅定义了表的字段结构和数据校验规则,更重要的是,它包含了 permission 节点,用于规定对该表进行 create (新增)、read (读取)、update (更新)、delete (删除) 操作的权限条件 。  

这些权限条件是以 JavaScript 布尔表达式的形式书写的。在这些表达式中,可以使用以下预置变量:

  • auth.uid: 当前发起请求的、经过身份验证的用户的唯一ID。
  • auth.role: 一个数组,包含了当前用户所拥有的所有角色ID (role_id)。
  • auth.permission: 一个数组,包含了当前用户所拥有的所有权限ID (permission_id)。
  • doc: 在 read, update, delete 操作的权限表达式中,doc 代表数据库中正在被访问或修改的当前文档(记录)对象。
  • newData: 在 createupdate 操作的权限表达式中,newData 代表客户端提交的、将要写入数据库的新数据对象。

clientDB 收到一个来自前端的数据操作请求时,uniCloud 服务端会首先验证用户 Token,然后根据 Token 信息填充 auth 对象,并执行 Schema 中对应操作的 permission 表达式。只有当表达式的计算结果为 true 时,该数据库操作才会被允许执行;否则,操作将被拒绝,并向前端返回权限错误 。  

DB Schema 权限提供了一种声明式的、在服务端强制执行的数据层安全机制。其强大之处在于,这种安全策略独立于数据操作的发起方式。无论是来自 uni-admin 后台的规范操作,还是来自定制化前端的正常请求,甚至是某个怀有恶意的用户尝试通过技术手段直接调用 clientDB 接口,都必须经过服务端 Schema 权限规则的严格审查。

虽然使用 JavaScript 表达式赋予了 Schema 权限极大的灵活性,可以实现非常复杂和精细的控制逻辑,但也需要注意避免过度复杂化。过于复杂的权限表达式(例如,包含大量字符串操作、循环或正则表达式匹配,尽管通常不推荐这样使用)可能会在每次数据访问时都带来额外的计算开销,从而影响数据库性能。因此,在设计 Schema 权限规则时,应力求简洁高效,优先使用直接的字段比较和数组包含判断。如果遇到确实需要复杂逻辑判断的权限场景(例如,涉及跨表查询或大量计算才能确定权限的),将其封装到专门的云函数或云对象中处理,可能比在 Schema 表达式中实现更为合适和高效。

5.2. DB Schema 权限的常用模式

以下是一些在 clientDB Schema permission 规则中常用的设计模式:

  • 基于所有者的访问控制 (Owner-based access)

    • 读取 (read): "doc.user_id == auth.uid" (用户只能读取自己创建的文档,假设文档中用 user_id 字段记录创建者ID)。
    • 更新/删除 (update/delete): "doc.user_id == auth.uid" (用户只能更新/删除自己的文档)。 ( 提及了 auth.uid 的可用性)。  
  • 基于角色的访问控制 (Role-based access)

    • 读取 (read): "auth.role.includes('editor') | | auth.role.includes('admin')" (拥有 'editor' 或 'admin' 角色的用户可以读取)。
    • 创建 (create): "auth.role.includes('publisher')" (只有 'publisher' 角色的用户可以创建)。
  • 基于权限的访问控制 (Permission-based access) - 最灵活的方式

    • 读取 (read): "'view_articles' in auth.permission" (用户必须拥有 view_articles 权限才能读取)。( 中有类似示例:"'USER_EDIT' in auth.permission")。  
    • 更新 (update): "'edit_article' in auth.permission && doc.user_id == auth.uid" (用户需要 edit_article 权限,并且只能编辑自己的文章)。
    • 创建 (create): "'create_article' in auth.permission"
  • 公开访问 (只读)

    • 读取 (read): "true" (任何人都可以读取)。
    • 创建/更新/删除 (create/update/delete): "false" (禁止通过 clientDB直接写入,所有写操作必须经由特定的云函数/云对象进行,这些云函数/对象内部可能会使用 uniCloud.setContext({નારole: ['admin']}) 或类似方式临时提升权限以绕过 Schema 进行写操作)。( 提及了将权限设为 false 并通过云函数处理的场景)。  
  • 禁止客户端访问 (仅限服务端操作)

    • 所有操作 (create, read, update, delete): "false"

通过组合这些模式,可以实现非常复杂的访问控制逻辑。例如,可以定义一个 articles 表的更新权限,使得管理员可以编辑任何文章,内容编辑员也可以编辑任何文章,而普通注册用户则只能编辑他们自己发布的文章。

在编写 update 规则时,理解 docnewData 变量的区别至关重要。doc 指的是数据库中记录的当前状态,而 newData 是客户端提交的、意图更新的数据。这对于防止用户在更新时非法修改某些关键字段(如文档的 user_id 所有者字段)非常重要。例如,一个允许用户编辑自己文章的规则 "'edit_article' in auth.permission && doc.user_id == auth.uid",本身并不能阻止用户在提交的 newData 中将 user_id 修改为其他人的ID。要防止这种情况,可以进一步在表达式中约束 newData.user_id == doc.user_id,或者更推荐的做法是,在 Schema 的字段级别 properties 定义中,将 user_id 字段的写权限设置为仅在创建时允许 ("permission": { "write": "doc.user_id == null" }),从而使其在创建后不可更改。这说明了健壮的 Schema 设计需要同时考虑文档级权限和字段级权限/校验规则。

表4: 常用 clientDB Schema 权限表达式示例

场景操作表达式示例说明参考资料
用户只能读/改/删自己的文档R, U, D"doc.user_id == auth.uid"检查文档中的 user_id 字段是否与当前认证用户的ID匹配。(auth.uid)
用户必须拥有 'editor' 角色才能创建C"auth.role.includes('editor')"检查用户的角色数组中是否包含 'editor'。(auth.role)
用户需要 'view_item' 权限才能读取R"'view_item' in auth.permission"检查 'view_item' 字符串是否存在于用户的权限数组中。(auth.permission)
'admin' 角色拥有完全访问权,其他用户禁止C, R, U, D"auth.role.includes('admin')"如果用户拥有 'admin' 角色则授予访问权限。若无其他规则允许,则隐式拒绝其他用户。(auth.role)
公开可读,但仅特定角色可写R / C, U, D读取: "true", 写入: "auth.role.includes('writer')"任何人都可以读取。只有 'writer' 角色的用户可以创建、更新或删除。(auth.role)
创建后禁止修改所有者字段U (字段级别)user_id 字段的 permission 中设置: { "write": "doc.user_id == null" } (在 properties 内)user_id 字段仅在文档创建时(即 doc.user_id 尚不存在时)可写。- (概念性)
仅当状态为 'draft' 时允许更新U"doc.status == 'draft' && doc.user_id == auth.uid"用户只能更新其自己的、且当前状态为 'draft' 的文档。(auth.uid)

 

6. uni-app 权限架构最佳实践

构建一个健壮且易于维护的权限系统,需要遵循一系列设计原则和实践经验。

6.1. 设计有效的角色和权限:粒度、命名与范围

  • 权限粒度 (Granularity) :权限的设计应尽可能地原子化,即一个权限对应一个具体的操作,例如 article:create (创建文章)、article:edit:own (编辑自己的文章)、article:publish (发布文章)、user:list (查看用户列表) 。避免设计过于宽泛的权限(如 manage_everything),因为这违反了最小权限原则;同时也要避免权限划分过细导致权限数量爆炸式增长,难以管理(uni-id 权限上限为500个 )。找到合适的平衡点是关键。  
  • 命名约定 (Naming Conventions) :为角色ID (role_id) 和权限ID (permission_id) 采用清晰、一致且具有语义的命名规范至关重要 。例如,可以使用 资源:操作 (如 article:publish) 或 动词-名词 (如 publish-article) 的格式。良好的命名能极大提升代码和配置的可读性与可维护性。  
  • 角色设计 (Role Design) :角色应根据用户的实际工作职能或逻辑上的职责分组来创建 。例如,“内容管理员”、“财务审计员”、“普通会员”等。为角色分配权限时,应确保权限集合与该角色的日常工作任务相匹配 。  
  • 作用范围 (Scope) :考虑权限和角色是全局性的,还是具有特定上下文范围。uni-id 本身除了 dcloud_appid 提供的应用级隔离外,并未内置更细致的上下文范围(如项目内权限、部门内权限)模型。如果需要此类控制,可能要通过巧妙的角色/权限命名约定(例如 projectA:article:edit)或在业务逻辑中进行额外的自定义判断来实现。

在代码中进行权限判断时,应优先检查用户是否拥有执行某个操作所需的特定“权限”,而不是检查用户是否属于某个特定的“角色” 。这种做法更为灵活和面向未来。角色的构成和用户的角色分配可能会随着业务发展而调整,但执行某个核心操作(如“删除用户”)所需的根本能力(即“删除用户”这个权限本身)通常是相对稳定的。如果代码逻辑强依赖于角色名,当需要将某个操作能力赋予新角色或从旧角色中移除时,就不得不修改多处代码。反之,如果代码检查的是权限,那么只需要调整角色与权限的映射关系,业务代码无需变动,从而实现更好的解耦和可维护性。  

虽然 uni-id 的权限列表是扁平的 ,但在权限数量较多时,管理起来可能会显得杂乱。可以考虑通过命名约定来模拟一种逻辑上的层级或分组,例如 模块:子模块:操作 (如 finance:report:view, finance:transaction:create)。这虽然不是 uni-id 强制的层级结构,但有助于人工理解和组织权限,尤其是在 uni-admin 界面进行分配时。不过,仍需注意 uni-id 的500个权限总数限制 ,避免因过度细化或不当的“层级模拟”而过早达到上限。  

6.2. 管理 Token 生命周期并确保权限的时效性

鉴于权限信息缓存于 Token 中可能引发的“过时”问题,对 Token 生命周期的妥善管理显得尤为重要。

  • uni-id/config.json 中审慎配置 tokenExpiresIn (Token 有效期) 和 tokenExpiresThreshold (Token 自动续期阈值) 。需要在安全性(权限信息更新更及时)与用户体验/性能(减少 Token 刷新次数和重新登录的频率)之间找到平衡。  
  • 对于涉及高度敏感操作或特权角色的场景,可以考虑为其设置更短的 Token 有效期。
  • 应告知用户,权限变更可能不会立即生效,可能需要重新登录或等待一小段时间(直到 Token 自动刷新)。
  • uni-idrefreshToken API 在被调用时,会重新从数据库获取用户最新的角色和权限信息并更新到新 Token 中,这本身就是一种保证权限数据相对较新的机制 。  

不存在一个适用于所有应用的“最佳”Token 有效期。例如,一个金融交易类应用可能会采用极短的 Token 有效期(如几分钟或几十分钟),而一个内容浏览型应用则可能允许 Token 有效期长达数天或数周。开发者应根据应用的具体业务特性、数据敏感程度以及用户活跃模式来做出明智的配置决策,而不是简单沿用默认值。

此外,尽管 uni-id 提供了在 Token 临近过期时自动续期的机制 ,但一个健壮的应用还应妥善处理 Token 续期失败的场景(例如,由于网络中断,或者用户账户在后端已被禁用)。通常的处理方式是,当客户端检测到无法修复的 Token 错误时(例如,Token 确实已过期且所有续期尝试均失败),应主动清除本地的用户会话信息(包括存储的 Token),并将用户引导至登录页面。这意味着,围绕 Token 管理的全面错误处理机制,也是一个安全的权限体系不可或缺的一环。  

6.3. 遵循最小权限原则 (PoLP)

最小权限原则 (Principle of Least Privilege, PoLP) 是信息安全领域的一项基本准则,其核心思想是:任何用户、程序或进程,都只应被授予执行其预定任务所必需的最少权限 。  

  • 避免不必要地为用户或角色分配过于宽泛的权限,尤其是像 admin 这样的超级管理员角色。应尽可能创建更具体的、权限范围受限的角色。
  • 定期对现有用户的角色分配和角色的权限构成进行审查和审计,及时移除那些不再需要的、过度的权限。
  • 在功能设计和权限分配时,如果只读权限就能满足需求,就不要授予写入或修改权限 。  

严格遵循 PoLP,能够显著降低因账户被盗用、内部人员误操作或恶意行为所可能造成的损害范围。如果一个账户只拥有有限的权限,那么即使该账户出现问题,其所能执行的未授权操作或能接触到的敏感数据也是有限的。

有效实施 PoLP 通常不仅仅是一个技术决策,它往往需要开发者与业务方(如产品经理、部门主管)进行深入沟通,以准确理解不同用户群体在应用中的实际工作流程和职责需求。例如,一个“市场实习生”角色是否应该拥有直接发布文章的权限,还是只能起草文章等待审核?这类问题涉及到业务流程的定义,其答案直接影响权限的设计。PoLP 不是一次性的配置工作,而是一个随着应用功能迭代和组织结构演变而需要持续进行调整和优化的动态过程。

6.4. 构建可维护、可扩展的权限逻辑代码结构

  • 后端集中校验:将核心的权限校验逻辑集中在服务端的云函数或云对象中,而不是将其分散在多个不同的业务逻辑点。
  • 前端辅助封装:对于前端的权限判断,除了使用路由守卫进行页面级访问控制外,对于组件内部复杂的UI显隐逻辑,可以考虑使用 Vue 的计算属性或辅助方法进行封装,以保持模板的整洁和逻辑的内聚性。
  • clientDB Schema 简洁高效:保持 clientDB Schema 中的权限表达式清晰、易懂且执行高效。
  • 逻辑解耦:尽可能将授权逻辑(判断用户是否拥有权限)与核心业务逻辑(执行具体操作)分离开来。例如,在云函数中可以考虑使用类似中间件或装饰器的模式来预处理权限校验。uni-cloud-router 插件为云函数请求处理引入的中间件概念,就体现了这种解耦思想。  

结构混乱的权限代码(例如,在多处重复进行相同的权限判断,或者使用深度嵌套的条件语句来处理权限逻辑)不仅难以维护和调试,其本身也可能成为安全隐患的来源。遵循代码的 DRY (Don't Repeat Yourself) 原则,将通用的权限检查逻辑抽象为可复用的函数或服务,是至关重要的。

在现代前端开发实践中,尤其是在使用 Vue 3 的组合式 API (Composition API) 的项目中(尽管 uni-app 的许多老项目和示例可能仍以选项式 API 为主),可以考虑使用自定义 Hook (或称为 Composable 函数) 来封装可复用的前端权限相关逻辑。例如,一个常见的UI需求是:如果用户缺乏某权限,则禁用某个按钮并在鼠标悬停时显示提示信息。这个行为涉及到权限检查、按钮 disabled 状态的响应式管理以及提示信息的动态生成。与其在每个需要此功能的组件中重复实现这套逻辑,不如创建一个 usePermissionControl('some_permission') 这样的自定义 Hook,它返回一组响应式的数据(如 { isDisabled, tooltipText }),供组件直接使用。这种方式能够有效地封装权限驱动的UI行为,使组件代码更简洁,权限逻辑也更易于独立测试和维护。

6.5. 权限更新与缓存失效策略

这是一个相对高级的主题,主要围绕如何处理因 Token 缓存导致的权限信息“过时”问题,特别是在需要权限变更即时生效的场景下。

  • 即时性要求极高的情况

    1. 缩短 Token 有效期:这是最直接的方法,但会增加服务端处理 Token 刷新请求的负载,并可能略微影响用户体验。
    2. 服务端主动通知或客户端轮询:可以设计一种机制,当管理员在后台修改了权限后,服务端能以某种方式(如通过 WebSocket 推送,如果架构支持)通知相关的活动客户端会话,或者客户端可以设置一个较短的定时器,周期性地向服务端请求一个“权限版本号”或简化的权限摘要,与本地 Token 中的信息比对,若不一致则主动刷新 Token 或重新获取完整权限配置。uni-id 本身并未在提供的资料中直接提供“广播权限更新”或“强制指定用户下线/刷新Token”的API。
    3. 关键操作前强制刷新校验:对于那些极度敏感的操作,对应的后端云函数可以在执行核心业务逻辑之前,除了进行常规的 checkToken 外,额外再从数据库中查询一次用户当前最新的、实时的角色和权限信息,确保万无一失。
  • uni-id 的文档中曾提及可以使用 Redis 等外部缓存来存储用户权限,这可能比仅仅依赖 Token 缓存提供更灵活的缓存控制和失效策略 ,但这方面的具体实践并未在当前资料中详细展开。  

真正意义上的、跨所有分布式客户端的权限实时失效是一个复杂的技术挑战。对于大多数基于 uni-appuni-id 的应用而言,依赖 Token 本身的过期和刷新机制,通常是在可接受的延迟与系统复杂度之间取得的一个务实平衡。

一种更精细化的处理“过时”权限的思路是引入“权限版本”的概念。具体做法可以是:

  1. uni-id-users 表或一个专门的权限版本关联表中,为每个用户或每个角色的权限配置维护一个版本号或最后更新时间戳。
  2. 当管理员修改了某个用户或角色的权限时,同步更新这个版本号/时间戳。
  3. uni-id 在签发 Token 时,将当前用户权限对应的版本号/时间戳也包含在 Token 的载荷中。
  4. 在服务端进行 checkToken 之后,或者在执行关键业务操作之前,可以从 Token 中提取出这个权限版本号/时间戳,并与数据库中存储的该用户当前最新的权限版本号/时间戳进行比对。
  5. 如果两者不一致,则说明 Token 中缓存的权限信息已“过时”,此时服务端可以拒绝该操作,或强制客户端刷新 Token,或直接使用从数据库查询到的最新权限进行判断。 这种方法相比单纯依赖时间过期,能更准确地识别出权限的“过时”状态,但无疑也增加了系统的复杂度和数据库的读取操作。这属于对 uni-id 核心逻辑的一种自定义扩展。

7. uni-app 权限管理综合:总结与建议

构建一个完善的 uni-app 权限系统,是一个涉及前端、后端乃至数据库层面协同设计的系统工程。

7.1. 核心策略回顾

一个健壮的 uni-app 权限管理体系应采用多层次的防护策略:

  • uni-admin 后台管理:作为权限数据的配置中心,提供用户、角色、权限的可视化管理界面,并通过动态菜单控制后台操作入口的可见性。
  • 前端应用 (小程序/H5/App) :利用 uniIDHasRole / uniIDHasPermission API 和路由守卫等机制,实现客户端的UI元素条件渲染和页面访问控制,主要目的是提升用户体验和进行初步的访问过滤。
  • 后端云函数/云对象:通过 uni-id-commoncheckToken 方法,对所有来自客户端的API请求进行严格的身份验证和权限校验,这是保障业务逻辑安全的核心防线。
  • clientDB 数据库 Schema:通过在 Schema 文件中定义精细的 permission 规则,对所有直接的数据库操作(尤其是通过 clientDB 从前端发起的)进行最终的、权威性的数据级访问控制。

7.2. 构建安全、高效、用户友好的权限系统之最终建议

  1. 始于精心设计:在项目初期就投入足够精力,清晰地规划和设计角色体系与权限粒度。遵循语义化命名,明确每个权限的业务含义。
  2. 服务端校验为王:始终将服务端的权限校验(云函数/对象中的 checkToken,以及 clientDB Schema 中的 permission 规则)作为安全保障的最后屏障和最高权威。绝不能仅依赖客户端的判断。
  3. 客户端体验优化:合理运用客户端的权限判断能力(uniIDHasRole/Permission、路由守卫)来优化用户界面和交互流程,但要清楚其局限性,不将其视为安全机制。
  4. 深思熟虑Token策略:根据应用的具体需求,审慎配置 Token 的有效期 (tokenExpiresIn) 和自动续期阈值 (tokenExpiresThreshold),在安全性和用户便利性之间取得平衡。
  5. 恪守最小权限:严格遵循最小权限原则,确保每个用户和角色只拥有其完成本职工作所必需的最少权限。定期进行权限审计。
  6. 文档化与规范化:将权限模型、角色定义、关键权限ID及其含义等重要信息进行文档化,并建立团队内部的开发规范,确保权限系统的一致性和可维护性。
  7. 全面彻底测试:对权限系统的各个层面进行充分测试,包括正常的权限分配场景、权限不足的拒绝场景、以及各种尝试绕过权限限制的边缘案例和安全攻击模拟。

这是这二年使用 uni-admin 和 uni-id 的权限的一些心得,整个过程,对于一个商业系统权限控制,前端 接口,后台与数据库权限有详细的介绍,并且 unidcloud 完整的支持这些,比想象的做得还多, uni-id 体系直接兼容多应用与多个 Login 的合集,是一个不错的设计。大家有相关的问题欢迎交流。