Uniapp如何实现用户千人千面权限控制开发实战?
书接上篇,我们今天继续来学习一下 UniApp 如何实现一个稍微进阶一点、但每个商业项目都绕不开的需求:用户权限控制,附带了完整的设计思路和源码示例,以及常见避坑指南和开发实践。
此系列文章将带领你从移动端跨平台开发入门到精通,如果你也喜欢关注APP、小程序、公众号、H5等等应用的开发,可以持续关注后续更新,避免错过宝贵的知识分享。
致开发者的忠告: AI编程盛行的今天,我们并不是不需要学习技术,而是更应该专研技术,拥有把控全局的架构设计思维才能在AI盛行的未来有立足之地。
言归正传,咱们今天继续来干一件每个商业级项目都躲不掉、但很多新手都写不利索的事(权限控制):让不同用户看到不同的世界–千人千面。
你可能会想:“不就是根据登录状态显示不同按钮吗?我v-if一把梭不就完了?”但等你真的开始做,就会遇到:
-
用户登录了,页面还是显示游客的内容——UI不刷新!
-
会员A能看到VIP专区,会员B也能看到,可B明明等级不够!
-
明明没权限,用户通过URL直接访问也能进去!
-
退出登录再登录,权限数据还留着,串号了!
-
用户按功能付费后,如何控制对应功能的展示和使用!
今天我就带你系统梳理一遍,从设计思路到代码落地,让你彻底掌握权限控制。而且咱们用的是Pinia(Vue官方推荐的状态管理库,比Vuex更轻量、更好用),代码清晰,维护起来倍儿爽。
一、先搞清楚:我们的权限系统要管什么?
一个典型的业务系统,权限控制通常包含四个维度:
-
页面访问权限:哪些页面游客能进?哪些必须登录?哪些只有VIP会员能进?哪些功能付费后才能使用等等?
-
按钮操作权限:比如“删除订单”按钮只有管理员能看到、能点。
-
区块展示权限:比如普通会员看不到“专属客服”区块,黄金会员能看到。
-
链接跳转权限:点击某个链接,如果没权限要么隐藏,要么跳转时被拦截。
角色定义(示例):
-
游客:未登录,只能看公开内容。
-
普通会员:已登录,基础权限。
-
黄金会员:在普通会员基础上,多几个特权。
-
钻石会员:在黄金基础上再加特权。
(当然,实际业务可能更复杂,比如还有管理员、超级管理员,但原理一样。)
二、地基:权限数据存在哪?怎么存?
权限控制的核心是用户信息和权限规则。
1️⃣ 用户信息数据结构
假设后端返回的登录信息长这样:
{userId: 1001,nickname: '张三',role: 'member', // 角色:guest, member, adminlevel: 2, // 等级: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': { auth: false }, // 公开'/pages/user/user': { auth: true }, // 必须登录'/pages/vip/vip': { auth: true,level: 2, // 至少黄金会员(level >= 2)// 也可以使用 permission: 'vip:access' 来细粒度控制 },'/pages/admin/admin': {auth: true,role: 'admin'// 必须管理员角色 }}// 白名单(完全不拦截)exportconstwhiteList= ['/pages/login/login', '/pages/register/register']
步骤2:实现路由拦截器
新建utils/permission.js,编写拦截逻辑。
// utils/permission.jsimport { useUserStore } from'@/stores/user'import { pagePermissions, whiteList } 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 (!pageConfig) returntrue// 需要登录但未登录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: [String, Array],// 角色role: String,// 最低等级level: Number,// 数组匹配模式:some(任一)或 every(全部)mode: {type: String,default: 'some' }})constuserStore=useUserStore()consthasAccess=computed(() => {// 角色判断if (props.role&&userStore.userInfo.role!==props.role) returnfalse// 等级判断if (props.level!==undefined&&Number(userStore.userInfo.level) <props.level) returnfalse// 权限点判断if (props.permission) {returnuserStore.hasPermission(props.permission, props.mode) }// 如果没有传任何条件,默认显示returntrue})</script>
这个组件接收三个主要条件:permission、role、level,它们之间是“与”关系(所有条件必须满足才显示)。你可以根据需求传一个或多个。
在页面中使用
<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 { ref, computed } from'vue'exportconstuseUserStore=defineStore('user', () => {// 状态consttoken=ref('')constuserInfo=ref({userId: null,nickname: '',role: 'guest', // 默认游客level: 0,permissions: [] })// 计算属性:是否登录constisLogin=computed(() =>!!token.value)// 方法:判断是否有权限functionhasPermission(permission, mode='some') {// 如果没有传permission,默认有权限if (!permission) returntrue// 如果用户权限列表为空,则无权限if (!userInfo.value.permissions||userInfo.value.permissions.length===0) returnfalseconstuserPerms=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', level: 0, permissions: [] }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: {userId: 1001,nickname: '张三',role: 'member',level: 2, // 黄金会员permissions: ['order:view', 'order:create', 'vip:access'] } }userStore.login(userData)// 处理跳转if (redirect.value) {// 判断原页面是否是tabBar页面(简单判断路径是否在tabBar中)// 这里需要知道tabBar路径,可以从pages.json读取,但为了简化,我们使用reLaunch// 更严谨的做法:判断redirect.value是否以tabBar页面路径开头uni.reLaunch({ url: redirect.value }) } else {uni.switchTab({ url: '/pages/index/index' }) }}</script>
说明:因为原页面可能是普通页面也可能是tabBar页面,直接用uni.reLaunch可以清空页面栈并跳转,不会受跳转类型限制。
八、常见问题及解决方案(血泪经验)
💥 问题1:用户登录后,界面还是显示游客内容
现象:登录成功返回后,页面上的v-if没有重新计算,依然显示“登录/注册”按钮。原因:权限数据更新了,但组件没有重新渲染。Vue的响应式数据如果正确使用,应该会自动更新。但有些开发者把权限判断写在了created或mounted里,数据变化后不会重新执行。✅ 解决方案:
-
用计算属性或函数来判断权限,确保依赖响应式数据。
-
使用我们的
<Auth>组件(内部是computed),数据变化自动更新。
💥 问题2:路由拦截器不生效,没权限的页面还是能进
现象:用户直接输入URL或通过扫码进入无权限页面,没有被拦截。原因:
-
拦截器只拦截了
navigateTo,但小程序还支持switchTab、reLaunch,需要全部拦截。 -
或者在
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页面只能用switchTab或reLaunch打开。✅ 解决方案:在登录页判断原页面是否为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里的数据结构即可。
记住:永远不要在客户端做最终的安全决策,但要在客户端做最好的用户体验。权限控制的目的不是防黑客,而是让不该看到的人看不到,减少干扰。
好了,去你的项目里实践一下吧!如果遇到什么奇怪的问题,欢迎随时带着代码来找我。
—— 一个帮你把权限控制安排得明明白白的老鸟 🛡️
加油,未来的全栈大佬!💪如果你也对移动端跨端开发感兴趣,关注我,后续还有更多优质文章分享!


往期相关文章推荐
夜雨聆风
