乐于分享
好东西不私藏

Uniapp如何实现用户千人千面权限控制开发实战?

Uniapp如何实现用户千人千面权限控制开发实战?

书接上篇,我们今天继续来学习一下 UniApp 如何实现一个稍微进阶一点、但每个商业项目都绕不开的需求:用户权限控制,附带了完整的设计思路和源码示例,以及常见避坑指南和开发实践。

此系列文章将带领你从移动端跨平台开发入门到精通,如果你也喜欢关注APP、小程序、公众号、H5等等应用的开发,可以持续关注后续更新,避免错过宝贵的知识分享。

致开发者的忠告: AI编程盛行的今天,我们并不是不需要学习技术,而是更应该专研技术,拥有把控全局的架构设计思维才能在AI盛行的未来有立足之地。

言归正传,咱们今天继续来干一件每个商业级项目都躲不掉、但很多新手都写不利索的事(权限控制):让不同用户看到不同的世界–千人千面

你可能会想:“不就是根据登录状态显示不同按钮吗?我v-if一把梭不就完了?”但等你真的开始做,就会遇到:

  • 用户登录了,页面还是显示游客的内容——UI不刷新!

  • 会员A能看到VIP专区,会员B也能看到,可B明明等级不够!

  • 明明没权限,用户通过URL直接访问也能进去!

  • 退出登录再登录,权限数据还留着,串号了!

  • 用户按功能付费后,如何控制对应功能的展示和使用!

今天我就带你系统梳理一遍,从设计思路代码落地,让你彻底掌握权限控制。而且咱们用的是Pinia(Vue官方推荐的状态管理库,比Vuex更轻量、更好用),代码清晰,维护起来倍儿爽。


一、先搞清楚:我们的权限系统要管什么?

一个典型的业务系统,权限控制通常包含四个维度:

  1. 页面访问权限:哪些页面游客能进?哪些必须登录?哪些只有VIP会员能进?哪些功能付费后才能使用等等?

  2. 按钮操作权限:比如“删除订单”按钮只有管理员能看到、能点。

  3. 区块展示权限:比如普通会员看不到“专属客服”区块,黄金会员能看到。

  4. 链接跳转权限:点击某个链接,如果没权限要么隐藏,要么跳转时被拦截。

角色定义(示例):  

  • 游客:未登录,只能看公开内容。  

  • 普通会员:已登录,基础权限。  

  • 黄金会员:在普通会员基础上,多几个特权。  

  • 钻石会员:在黄金基础上再加特权。  

(当然,实际业务可能更复杂,比如还有管理员、超级管理员,但原理一样。)


二、地基:权限数据存在哪?怎么存?

权限控制的核心是用户信息权限规则

1️⃣ 用户信息数据结构

假设后端返回的登录信息长这样:

{userId1001,nickname'张三',role'member',       // 角色:guest, member, adminlevel2,             // 等级:1普通会员,2黄金会员,3钻石会员permissions: [        // 细粒度权限点'order:view','order:create','vip:access',       // VIP专区入口'button:delete'// 删除按钮  ]}

2️⃣ 存在哪里?

  • Pinia:响应式,UI自动更新(关键!)。

  • uni.setStorageSync:持久化,下次启动直接读,不用每次都登录。

注意:退出登录时,必须清空store和storage,否则会出现权限残留。


三、路由跳转权限:用拦截器守住每个页面

在UniApp中,所有跳转都可以通过uni.addInterceptor拦截。我们可以在跳转前检查用户是否有权限访问目标页面。

步骤1:定义页面权限映射

新建一个config/permission.js文件,定义每个页面需要的权限。

// config/permission.js// 页面权限配置exportconstpagePermissions= {// 格式:页面路径 => 权限要求'/pages/index/index': { authfalse }, // 公开'/pages/user/user': { authtrue }, // 必须登录'/pages/vip/vip': { authtrue,level2// 至少黄金会员(level >= 2)// 也可以使用 permission: 'vip:access' 来细粒度控制  },'/pages/admin/admin': {authtrue,role'admin'// 必须管理员角色  }}// 白名单(完全不拦截)exportconstwhiteList= ['/pages/login/login''/pages/register/register']

步骤2:实现路由拦截器

新建utils/permission.js,编写拦截逻辑。

// utils/permission.jsimport { useUserStore } from'@/stores/user'import { pagePermissionswhiteList } from'@/config/permission'exportfunctionsetupPermissionGuard() {constmethods= ['navigateTo''redirectTo''reLaunch''switchTab']methods.forEach(method=> {uni.addInterceptor(method, {invoke(args) {constuserStore=useUserStore()consturl=args.url.split('?')[0// 去掉参数,只保留路径// 白名单放行if (whiteList.includes(url)) returntrueconstpageConfig=pagePermissions[url]// 如果页面没有配置权限,默认放行(可按需改为拦截)if (!pageConfigreturntrue// 需要登录但未登录if (pageConfig.auth&&!userStore.isLogin) {uni.showToast({ title'请先登录'icon'none' })// 跳转到登录页,并带上回跳地址uni.navigateTo({url`/pages/login/login?redirect=${encodeURIComponent(args.url)}`          })returnfalse// 拦截原跳转        }// 角色判断if (pageConfig.role&&userStore.userInfo.role!==pageConfig.role) {uni.showToast({ title'权限不足'icon'none' })returnfalse        }// 等级判断(统一转数字比较)if (pageConfig.level!==undefined) {constuserLevel=Number(userStore.userInfo.level)if (userLevel<pageConfig.level) {uni.showToast({ title'等级不足'icon'none' })returnfalse          }        }// 细粒度权限判断if (pageConfig.permission) {if (!userStore.hasPermission(pageConfig.permission)) {uni.showToast({ title'无权限访问'icon'none' })returnfalse          }        }returntrue      },fail(err) {console.error('拦截器错误'err)      }    })  })}

关键点:  

  • invoke 阶段拦截,返回 false 可阻止跳转。  

  • 未登录时跳转到登录页,并带上 redirect 参数,登录后自动跳回。  

  • 所有权限判断都基于 响应式 store,确保数据最新。


四、按钮/链接/区块权限:用权限组件搞定

页面内的展示控制,通常有两种做法:自定义指令或封装组件。这里我推荐封装权限组件,因为它更灵活,而且能完美配合响应式数据。

封装权限组件 <Auth>

新建components/Auth.vue

<!-- components/Auth.vue --><template><viewv-if="hasAccess"><slot/></view></template><scriptsetup>import { computed } from'vue'import { useUserStore } from'@/stores/user'constprops=defineProps({// 权限点(字符串或数组)permission: [StringArray],// 角色roleString,// 最低等级levelNumber,// 数组匹配模式:some(任一)或 every(全部)mode: {typeString,default'some'  }})constuserStore=useUserStore()consthasAccess=computed(() => {// 角色判断if (props.role&&userStore.userInfo.role!==props.rolereturnfalse// 等级判断if (props.level!==undefined&&Number(userStore.userInfo.level<props.levelreturnfalse// 权限点判断if (props.permission) {returnuserStore.hasPermission(props.permissionprops.mode)  }// 如果没有传任何条件,默认显示returntrue})</script>

这个组件接收三个主要条件:permissionrolelevel,它们之间是“与”关系(所有条件必须满足才显示)。你可以根据需求传一个或多个。

在页面中使用

<template><view><!-- 只有黄金会员以上能看到 --><Auth:level="2"><viewclass="vip-section"><text>黄金会员专享优惠</text><button@click="goToVipDeal">立即领取</button></view></Auth><!-- 需要有 'order:delete' 权限 --><Auth:permission="'order:delete'"><buttontype="warn"@click="deleteOrder">删除订单</button></Auth><!-- 多个权限,任意一个满足即可 --><Auth:permission="['vip:access', 'admin:access']"mode="some"><navigatorurl="/pages/vip/vip">VIP入口</navigator></Auth></view></template><scriptsetup>importAuthfrom'@/components/Auth.vue'constgoToVipDeal= () => {uni.navigateTo({ url'/pages/vip/deal' })}constdeleteOrder= () => {// 删除逻辑}</script>

五、Pinia Store:用户状态管理

新建stores/user.js,用Pinia管理用户信息。

// stores/user.jsimport { defineStore } from'pinia'import { refcomputed } from'vue'exportconstuseUserStore=defineStore('user', () => {// 状态consttoken=ref('')constuserInfo=ref({userIdnull,nickname'',role'guest',      // 默认游客level0,permissions: []  })// 计算属性:是否登录constisLogin=computed(() =>!!token.value)// 方法:判断是否有权限functionhasPermission(permissionmode='some') {// 如果没有传permission,默认有权限if (!permissionreturntrue// 如果用户权限列表为空,则无权限if (!userInfo.value.permissions||userInfo.value.permissions.length===0returnfalseconstuserPerms=userInfo.value.permissionsif (Array.isArray(permission)) {// 数组权限:根据mode判断 some/everyreturnmode==='some'?permission.some(p=>userPerms.includes(p))        : permission.every(p=>userPerms.includes(p))    } else {// 单个权限returnuserPerms.includes(permission)    }  }// 登录(模拟)functionlogin(userData) {token.value=userData.tokenuserInfo.value=userData.userInfo// 持久化uni.setStorageSync('token'token.value)uni.setStorageSync('userInfo'userInfo.value)  }// 登出functionlogout() {token.value=''userInfo.value= { role'guest'level0permissions: [] }uni.removeStorageSync('token')uni.removeStorageSync('userInfo')  }// 初始化(从存储读取)functioninit() {constsavedToken=uni.getStorageSync('token')constsavedUserInfo=uni.getStorageSync('userInfo')if (savedToken&&savedUserInfo) {token.value=savedToken// 确保permissions字段存在userInfo.value= {permissions: [],...savedUserInfo      }    }  }return {token,userInfo,isLogin,hasPermission,login,logout,init  }})

六、在App.vue中初始化

在应用启动时,初始化store并设置路由拦截器。

<!-- App.vue --><scriptsetup>import { onLaunch } from'@dcloudio/uni-app'import { useUserStore } from'@/stores/user'import { setupPermissionGuard } from'@/utils/permission'onLaunch(() => {// 初始化用户状态(从storage读取)constuserStore=useUserStore()userStore.init()// 设置路由权限守卫setupPermissionGuard()})</script><style>/* 全局样式 */</style>

七、登录页处理(带重定向)

登录页需要接收redirect参数,登录成功后跳回原页面。

<!-- pages/login/login.vue --><template><viewclass="login-container"><button@click="mockLogin">模拟登录</button></view></template><scriptsetup>import { onLoad } from'@dcloudio/uni-app'import { ref } from'vue'import { useUserStore } from'@/stores/user'constuserStore=useUserStore()constredirect=ref(''// 存储回跳地址// 获取页面参数onLoad((options=> {if (options.redirect) {redirect.value=decodeURIComponent(options.redirect)  }})constmockLogin= () => {// 模拟登录成功,获取用户信息constuserData= {token'fake_token',userInfo: {userId1001,nickname'张三',role'member',level2// 黄金会员permissions: ['order:view''order:create''vip:access']    }  }userStore.login(userData)// 处理跳转if (redirect.value) {// 判断原页面是否是tabBar页面(简单判断路径是否在tabBar中)// 这里需要知道tabBar路径,可以从pages.json读取,但为了简化,我们使用reLaunch// 更严谨的做法:判断redirect.value是否以tabBar页面路径开头uni.reLaunch({ urlredirect.value })  } else {uni.switchTab({ url'/pages/index/index' })  }}</script>

说明:因为原页面可能是普通页面也可能是tabBar页面,直接用uni.reLaunch可以清空页面栈并跳转,不会受跳转类型限制。


八、常见问题及解决方案(血泪经验)

💥 问题1:用户登录后,界面还是显示游客内容

现象:登录成功返回后,页面上的v-if没有重新计算,依然显示“登录/注册”按钮。原因:权限数据更新了,但组件没有重新渲染。Vue的响应式数据如果正确使用,应该会自动更新。但有些开发者把权限判断写在了createdmounted里,数据变化后不会重新执行。✅ 解决方案:  

  • 计算属性函数来判断权限,确保依赖响应式数据。  

  • 使用我们的<Auth>组件(内部是computed),数据变化自动更新。

💥 问题2:路由拦截器不生效,没权限的页面还是能进

现象:用户直接输入URL或通过扫码进入无权限页面,没有被拦截。原因:  

  • 拦截器只拦截了navigateTo,但小程序还支持switchTabreLaunch,需要全部拦截。  

  • 或者在pages.json中配置了"tabBar",用户点击tab切换不会触发navigateTo,需要用switchTab拦截。✅ 解决方案:我们已经在拦截器中遍历了所有路由方法,全部添加了拦截。

💥 问题3:退出登录后,权限数据没清空,导致还能看到会员内容

现象:用户退出登录,但页面上还显示会员专属按钮。原因:store中的用户数据没有清空。✅ 解决方案:在登出方法中,重置store并清除storage(我们已经做了)。

💥 问题4:等级判断出错,比如2级会员看不到2级页面

现象:配置level: 2,用户等级为2却进不去。原因:数据类型不一致,比如后端返回的level是字符串”2″,前端比较时用了userLevel < pageConfig.level,字符串比较可能出错。✅ 解决方案:统一转换为数字再比较(我们代码中已经使用Number()转换)。

💥 问题5:权限数据更新后,用v-if控制的元素没变化

现象:用户从普通会员升级为黄金会员,之前隐藏的按钮没有出现。原因:如果使用自定义指令,指令的updated钩子里虽然做了判断,但移除元素后不会自动重新插入。✅ 解决方案:推荐使用<Auth>组件(基于v-if实现),或者用v-if绑定一个计算属性。

💥 问题6:登录成功后重定向到原页面,但原页面是tabBar页面,用redirectTo会报错

现象:登录成功后,使用uni.redirectTo({ url: redirect }),如果原页面是tabBar,会报错。原因:tabBar页面只能用switchTabreLaunch打开。✅ 解决方案:在登录页判断原页面是否为tabBar,或者统一使用uni.reLaunch(我们示例中用了reLaunch)。


九、完整项目结构参考

your-project/├── pages/│   ├── index/│   │   └── index.vue│   ├── user/│   │   └── user.vue│   ├── vip/│   │   └── vip.vue│   └── login/│       └── login.vue├── components/│   └── Auth.vue├── stores/│   └── user.js├── utils/│   └── permission.js├── config/│   └── permission.js├── App.vue├── main.js└── pages.json

别忘了在main.js中注册Pinia

// main.jsimport { createSSRApp } from'vue'importAppfrom'./App.vue'import { createPinia } from'pinia'exportfunctioncreateApp() {constapp=createSSRApp(App)constpinia=createPinia()app.use(pinia)return { app }}

十、最后的叮嘱

小老弟,权限控制其实是一个系统工程,不仅仅是写几个v-if那么简单。你需要考虑:

  • 数据一致性:用户信息更新后,所有依赖它的地方都要刷新。  

  • 覆盖所有跳转入口:除了导航,还有tab切换、重启动、直接输入URL。  

  • 前后端配合:前端控制是体验层面的,真正的安全校验必须后端完成。  

  • 权限颗粒度:可以根据业务需求灵活定义,比如按角色、按等级、按权限点。

上面这套方案,我已经在多个生产项目中验证过,稳得很。你只需要根据自己项目的具体需求,调整pagePermissions和store里的数据结构即可。

记住:永远不要在客户端做最终的安全决策,但要在客户端做最好的用户体验。权限控制的目的不是防黑客,而是让不该看到的人看不到,减少干扰。

好了,去你的项目里实践一下吧!如果遇到什么奇怪的问题,欢迎随时带着代码来找我。

—— 一个帮你把权限控制安排得明明白白的老鸟 🛡️

加油,未来的全栈大佬!💪如果你也对移动端跨端开发感兴趣,关注我,后续还有更多优质文章分享!

往期相关文章推荐

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Uniapp如何实现用户千人千面权限控制开发实战?

评论 抢沙发

5 + 8 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮