ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

通用权限管理怎么做?基于Vue与Spring Boot的RBAC+JWT多终端认证实践

通用权限管理怎么做?基于Vue与Spring Boot的RBAC+JWT多终端认证实践 1. 为什么要自己做一套通用权限管理从业务痛点说起先聊个我自己的真实经历。去年接了一个外包项目对方要求后台管理系统能登录能分角色菜单按权限显示。我一看需求挺简单结果做到一半发现完全不是那么回事客户那边临时加了需求说运营人员只能在手机上看数据管理员在PC上还能审批没过两周又说要接一个小程序让用户也能登录查看自己的订单进度。这下好了原本能登录、分角色、菜单按权限显示三个简单需求直接变成了一整套多终端认证授权的架构问题。当时项目里用的还是那种老式的Session登录后端存Session前端靠Cookie带着SessionId走接口一多就乱套。更难受的是权限判断散落在各个Controller里每个方法里都写一句if(user.getRole() ! admin) return error改一次角色逻辑要动十几个文件。说实话那一刻我就下了决心与其每次接新项目都从头写一遍登录鉴权不如直接沉淀一套通用的权限管理底座下次直接拿来用改改业务就完事。这个想法其实就是我做这套通用后台权限管理系统源码的起点。它的定位很明确不绑定具体业务只解决谁可以进系统、谁可以看到什么、谁可以操作什么这三层问题同时把PC浏览器、手机Web、小程序、第三方客户端这些多终端的认证场景一并收纳进去。前端用Vue-Element后端用Spring Boot两者都是国内开发者的主流选择招人容易、资料多、踩坑了也有地方查。做这套系统的过程里我踩过的坑不算少但也正是这些坑让我把很多细节想明白了。这篇文章不是那种照着抄就行的源码解读而是把这套系统背后的设计思路、鉴权链路、模块划分和部署注意点完整拆开来讲。如果你正准备做自己的后台权限系统或者正在为一个已经堆了两年业务代码的旧系统做权限重构这篇应该能给你省下不少试错时间。2. 技术选型与整体架构Vue-Element和Spring Boot的组合逻辑2.1 前端为什么选Vue-Element后台管理的稳妥牌先说实话前端框架这几年卷得很厉害React、Vue、Svelte、SolidJS各有拥趸但你要说做后台管理系统Vue加Element UI依然是我会优先打的稳妥牌。原因很简单后台管理系统重逻辑、轻交互页面结构以表格、表单、弹窗、树形控件为主而Element UI以及它的Vue 3版本Element Plus恰好把这些组件做得非常成熟拿来即用不需要自己封装复杂的前端组件。更重要的是权限管理系统本身就有大量的动态渲染需求不同角色登录进来左侧菜单不一样、页面里的按钮不一样、甚至某个表单字段能不能编辑都不一样。Vue的响应式数据和组件化机制处理这类需求非常顺手。我在实际开发中会把权限相关的逻辑封装成Vue插件和自定义指令比如按钮权限就封装成一个v-permission指令模板里直接写v-permissionsystem:user:add没有权限的元素渲染时直接干掉干净利落。Element UI还有一个好处是它的Table组件和Form组件的表单校验逻辑很完善配合Vue的watch和computed做用户管理、角色管理这种典型的CRUD页面开发效率非常高。如果你用React做同样的事情也不是不行但往往需要额外引入antd之类组件库上手成本和心智负担都会大一些。2.2 后端为什么选Spring Boot生态整合能力是关键Spring Boot在后端的优势其实不在于快而在于全。权限管理系统的后端需要处理这样几类事情用户的认证你是谁、授权你能干什么、Token的签发与校验、接口的访问控制、操作日志的记录。这些事情Spring Boot生态都能找到现成的组件而且整合起来非常顺滑。比如Spring Security虽然它的配置和学习曲线一直被人吐槽但一旦跑通它提供的过滤器链机制、方法级权限注解PreAuthorize刚好能覆盖接口级别和方法级别的权限控制需求。再比如JWT的解析与校验可以用jjwt这类轻量库十几行代码就能完成Token的生成和校验配合Spring Security的过滤器链整个认证过程是无状态的这也为多终端认证打好了底子。我见过不少团队做权限系统直接用Shiro说实话Shiro确实比Spring Security简单不少配置也直观但Shiro在细粒度权限控制方面不如Spring Security灵活。你要做按钮级权限控制最终还是要落到接口层面Spring Security的表达式加上自己扩展的PermissionEvaluator能很优雅地实现接口匹配角色、方法匹配权限的完整链路。既然我们的目标是通用那就得选上限更高的方案。2.3 整体架构前后端分离加无状态认证这套系统的整体架构其实不复杂一句话就能说清楚前端是独立的Vue应用后端是Spring Boot提供的纯RESTful API两者之间通过JWT Token维持认证状态不依赖Session。前后端分离之后部署上就很灵活了。前端打出来的dist目录可以交给Nginx托管API部分单独部署在另一个端口或者二级域名上。好处是静态资源走CDN加速后端接口可以横向扩展加几个实例做负载均衡因为服务端不存Session所以不存在Session不同步的经典问题随便哪个后端节点处理请求都行。在接口设计层面我定了两条规则。第一所有接口统一返回ResultT结构体包含code、message、data三个字段前端根据code判断业务是否成功而不是靠HTTP状态码这样业务异常和网络异常可以清晰分开处理。第二所有需要认证的接口都要校验Token但登录接口、刷新Token接口、验证码接口这三类例外它们在Security配置里要标注为permitAll。这两条规则看起来简单但实际项目里很多混乱都是从有的接口有鉴权有的接口忘了加开始的。2.4 模块划分把通用能力和业务代码隔离开项目结构上我建议从一开始就把通用能力和业务代码分开放不然后期很容易变成一锅粥。我的做法是采用多模块Maven结构common-module公共类比如统一返回结果、异常处理、工具类、常量定义。system-module系统管理模块包含用户、角色、菜单、部门、字典这类基础管理功能这是权限系统的核心。auth-module认证模块单独处理登录、登出、Token刷新、验证码这些认证链路相关的逻辑。business-module业务模块给实际业务用的只依赖前面几个模块不反向依赖。这样分完之后有个很直观的好处业务模块的代码里你几乎看不到权限判断的逻辑因为权限控制的骨架在auth和system模块里已经搭好了业务开发只需要加注解、写接口、返回数据。后来我接新项目基本都是把前三个模块原封不动复制过去只改业务模块效率提升非常明显。3. 权限模型设计RBAC的落地细节与表结构规划3.1 为什么是RBAC最容易落地且覆盖绝大多数场景权限管理的理论模型不少有RBAC、ABAC、PBAC等等。ABAC基于属性来控制灵活度最高但规则配置复杂普通后台管理系统用起来完全是杀鸡用牛刀。RBAC也就是用户-角色-权限模型虽然听起来简单但它覆盖了绝大多数业务系统的真实需求用户归属于某些角色角色关联某些权限用户的最终权限就是所有角色权限的并集。我见过很多开发者一开始就喜欢设计精细到字段级的权限模型比如张三只能看到订单表里的金额字段但看不到客户电话。这种需求不能说没有但实际落地的时候维护成本极高每张表、每个字段都要有权限规则光配置界面就够写三周的。我的建议是做通用后台权限系统第一版老老实实用RBAC把用户、角色、菜单和接口权限管好已经能解决95%的问题了。真有字段级权限的需求再在RBAC基础上叠加一层数据规则过滤也不要推翻重来。3.2 核心表的设计五张表还是更多我们的表结构核心就是5张表外加一张字典表作为可选扩展。sys_user用户表的重点是状态字段我习惯用status字段管理账号状态0表示正常1表示锁定。这里有个细节容易被忽略删除用户不要物理删除而是打一个删除标记因为用户和很多业务数据未来可能有关联物理删除会把历史数据搞成一堆孤儿记录。sys_role角色表要有一个role_code字段它是角色的唯一编码比如admin、operator这个编码会用在后端接口的权限注解里比如hasRole(admin)。另外我加了一个data_scope字段用来做数据权限的范围控制可选值有全部数据、本部门数据、仅本人数据配合后面的数据过滤逻辑使用。sys_menu菜单表是这一套里最容易让人困惑的表因为菜单这个名词容易被理解成左侧导航栏。实际上我在这里存的不仅仅是菜单而是资源和操作的合集。菜单项、页面按钮、甚至API接口路径都可以作为一条menu记录存储。区分方式就是menu_type字段1是目录、2是菜单、3是按钮。一条用户管理菜单记录下面可以挂新增用户、编辑用户、删除用户三个按钮类型的子记录每个按钮记录绑定一个权限标识比如system:user:add这个标识就是前端v-permission指令和后端接口注解一起用的那个字符串。中间表是sys_user_role和sys_role_menu这两张表没什么好说的标准的RBAC关联表唯一需要注意的是查询的时候用批量查询避免在循环里逐条查数据库权限很大的用户角色一多循环查询很容易造成慢SQL。3.3 缓存权限数据一次登录权限信息全量加载权限校验频繁发生一个用户每次请求接口都要判断有没有权限如果每次都去数据库查角色和菜单压力很大。所以我在登录成功后会把用户的权限信息做一个全量缓存包括用户基本信息、角色编码列表、权限标识列表打包放进Rediskey设计为login:token:{token}。这里有一个关键设计缓存里存的权限标识是拍平之后的列表不存菜单树。因为接口鉴权时只需要判断这个用户的权限集合里有没有这个标识是包含关系不需要关心层级。菜单树是前端用来渲染侧边栏的后端登录接口返回一次前端自己组装。把这两件事分开之后后端接口鉴权的逻辑就是一次Redis查询加一次集合判断效率非常高。3.4 数据权限的处理一个容易被忽略的设计决策RBAC管的是能不能操作数据权限管的是能操作哪些数据。比如两个运营角色都能查看订单列表但区域A的运营只能看到华东区的订单区域B的运营只能看到华南区的订单这时候角色权限已经区分不了了就得靠数据权限处理。我在sys_role表里加了data_scope字段在业务查询里做了一层拦截。具体做法是定义一个DataScope注解标注在需要数据权限控制的Mapper方法上然后通过MyBatis拦截器自动拼接SQL条件。比如数据权限是仅本部门拦截器就自动加上WHERE dept_id #{当前用户的部门ID}如果是仅本人就加上WHERE create_by #{当前用户ID}。这个机制实现起来有点绕但做好了之后业务代码里完全不用关心数据权限只需要在Mapper方法上打一个注解通用性非常好。4. 后端认证链路从登录到JWT Token的全流程4.1 Spring Security的过滤器链理解它才能用好它Spring Security劝退过不少人核心原因就是它的过滤器链机制太抽象了。我在这里用一个通俗的比喻解释一下可以把Spring Security想象成机场安检通道一个请求过来要依次经过多个关卡——先是检查有没有带违禁品CSRF校验然后确认身份认证过滤器最后确认有没有登机牌和对应登机口授权管理器。所有关卡都通过了请求才进入Controller。我们做无状态认证的时候实际上是在这条链路上做了两件事。第一把原有的Session认证相关过滤器停用因为不用Session了。第二在合适的位置插入我们自己的JwtAuthenticationFilter它负责读取请求头里的Token解析出用户身份信息然后塞进SecurityContext里让后面的授权过滤器能够读到当前用户是谁。这个自定义过滤器是认证链路的核心它的执行逻辑大概是拿到请求头里的Authorization字段去掉Bearer前缀然后调用JwtUtils解析Token。如果解析成功根据Token里的用户ID去Redis缓存里拉取用户信息和权限列表构建一个LoginUser对象放进SecurityContext。如果解析失败不清空SecurityContext也不主动抛异常而是继续往下走让后续的放行规则决定请求能不能过。这个设计有一个好处静态资源和公开接口不需要Token也能访问而受保护接口在SecurityContext里拿不到认证信息时会被最后的ExceptionTranslationFilter统一拦截返回401状态码。4.2 JWT Token的生成与校验双Token机制Token设计我直接采用了双Token方案一个Access Token一个Refresh Token。Access Token的有效期比较短我设置的是2小时用于实际的接口访问Refresh Token有效期7天只用于调用刷新接口换取新的Access Token。为什么不能只用一个长有效期Token原因在于安全性。Token一旦泄露在有效期里任何人都能拿它访问接口。有效期设短一点泄露后的攻击窗口就小但代价是2小时就要重新登录体验太差。Refresh Token的作用就是弥补这个矛盾Access Token过期之后前端拿Refresh Token换新的Access Token用户无感续期。Refresh Token泄露的风险也有但它的使用范围仅限于刷新接口而且我可以随时在Redis里把这个刷新Token拉黑安全性可控。Token里我放的信息不多就是userId、username、tokenTypeaccess还是refresh、clientTypeweb还是app还是mini以及expireTime。不放权限列表因为权限放在Redis缓存里如果放Token里用户权限一变旧的Token依然是旧权限改起来很麻烦。4.3 多终端认证的Token区分解决同一账号多处登录互相顶掉的问题多终端认证是这套系统里比较有特色的部分。PC浏览器登录了手机APP再登录按直觉这两个应该是可以共存的但如果后端只用一个userId对应的Token就会互相顶掉。我在设计里用clientType和userId联合生成一个缓存key比如PC端登录的缓存key是login:web:{userId}APP端是login:app:{userId}小程序端是login:mini:{userId}。这样一来同一用户在不同终端可以各自独立登录互不影响。而同一终端重复登录时旧Token会被新Token覆盖达到同端互踢的效果。这个策略比较贴合真实使用场景。另外APP端和小程序端的认证载体和PC端也有差别。PC端是标准的请求头Authorization携带Token小程序环境和APP环境有时候没法自由控制请求头所以我会额外支持Token放在URL参数里传递同时要求必须配合签名参数一起使用避免Token直接暴露在日志里。这个属于特殊情况下的妥协方案不建议默认开启。4.4 登录接口的完整流程验证码、密码加密、登录日志登录接口的完整流程走一遍的话大概是这样的前端先请求验证码接口后端生成一张带4位字符的图片同时把验证码的答案存到Redis里key是captcha:{uuid}有效时间5分钟。用户输入用户名、密码、验证码之后后端先校验验证码对不对再查用户是否存在然后校验密码。密码存储方面我用的BCrypt加密。这里有一条安全原则必须强调密码绝不能明文存储也不要用MD5这种快速哈希因为MD5已经可以被彩虹表批量碰撞。BCrypt算法自带盐值相同密码每次加密的密文不同而且故意设计成计算耗时较长能有效拖慢暴力破解的速度。登录成功后后端生成Access Token和Refresh Token同时把用户信息、角色、权限列表、终端类型组装好一次性返回给前端。登录日志的记录也不能偷懒我专门建了sys_login_log表记录用户IP、操作系统、浏览器、登录时间、登录结果。写这个的时候要注意IP获取的坑如果后端前面有Nginx代理request.getRemoteAddr()拿到的是代理服务器的IP必须在Nginx配置里加上X-Forwarded-For头后端解析时取这个头的第一个IP才是真实客户端IP。5. 前端权限控制动态路由、菜单和按钮权限的联动5.1 前端用路由守卫做第一道拦截前端权限控制的第一步是路由守卫。Vue Router提供了beforeEach钩子可以在每次路由跳转前做判断。登录状态的判断逻辑不复杂如果当前路由是白名单比如登录页、注册页直接放行如果不在白名单且本地没有Token跳转到登录页如果本地有Token但还没拉取用户信息先调用接口拉取用户信息和权限再继续跳转。这里有个细节值得注意发送请求获取用户信息是一个异步操作而路由守卫里是拿不到同步结果的。所以我会在Vuex或Pinia里放一个hasInitAuth的布尔标志第一次鉴权的时候异步拉取用户信息拉取完成后再执行next()放行并且把标志置为true后续路由跳转就直接放行不再重复请求。这样既保证了第一次进入界面时权限信息一定就绪又避免了每次路由跳转都请求一次用户信息的浪费。5.2 动态路由生成后端驱动菜单而不是前端写死如果你做权限系统还在前端路由表里把页面写死那你的菜单是控制不了的。正确的做法是权限菜单由后端返回前端根据后端的菜单数据动态注册路由。登录后后端返回的菜单数据是一个树形结构每一层包含path、component、name、meta、children。前端的动态路由生成逻辑是遍历菜单树根据component字段找到对应的组件我习惯用前端一个固定的组件映射表比如system/user/index映射到import(/views/system/user/index.vue)然后调用router.addRoute动态注册。注册完所有路由之后再手动调用router.replace跳转到当前要访问的地址。做动态路由还有一个经典的坑页面一刷新动态路由就没了。因为动态路由是运行时注册的不经过本地持久化刷新浏览器后Vue实例重新创建路由表回到初始状态。解决办法是在路由守卫里判断如果本地有Token且动态路由还没注册过先走一遍上面的动态路由生成逻辑再放行。简单说就是每次刷新都重新跑一遍首次登录时的路由生成流程这样就不会出现刷新404的问题了。5.3 按钮权限自定义指令控制元素显隐页面级权限通过路由控制按钮级权限需要通过自定义指令控制。Element UI的后台管理页面里操作列经常有一排按钮新增、编辑、删除、导出。不同角色看到的按钮应该不一样。我封装了一个Vue自定义指令v-permission用法是这样的el-button v-permissionsystem:user:add typeprimary新增用户/el-button指令的实现逻辑是在mounted钩子里从Pinia里拿到当前用户已拥有的权限标识列表判断传入的字符串是否在列表里如果不在就把当前DOM元素从父节点移除。注意这里不能用display:none隐藏因为用户审查元素一样能看到按钮接口的存在直接从DOM移除更干脆。实际项目里还有另一层需求某些按钮不应当移除而应当置灰并给出提示。比如删除按钮普通运营角色能看到但没有权限点击后提示无操作权限。这种情况我另外提供一个v-permission-disabled指令不做移除而做禁用样式。两个指令互补配合Element UI的按钮状态基本覆盖了所有按钮权限的交互场景。5.4 菜单与按钮的联动菜单表里的层级关系因为后端的sys_menu表本身是父子结构目录下面挂菜单菜单下面挂按钮所以前端拿到菜单树后可以很自然地做渲染。菜单部分渲染成侧边栏导航按钮部分不会出现在导航里而是以权限标识的形式存在。这里有一个设计细节要讲清楚按钮记录虽然是挂在菜单下面的子节点但前端渲染侧边栏的时候要过滤掉menu_type3的记录否则侧边栏会混进一堆按钮节点。我的做法是后端返回菜单树之前就先把按钮节点剔除掉但按钮对应的权限标识列表依然返回放在LoginUser的permissions字段里。这样一个接口同时解决了两件事菜单树用于导航渲染权限标识列表用于按钮显隐判断。如果哪天要调整菜单和权限的映射关系只需要在后台维护树形表格前端和后端代码都不用动。6. 多终端认证的实战PC、移动端、小程序客户端的Token策略6.1 多终端认证的本质同一用户不同信任级别多终端认证这个词听起来复杂说白了其实是一个问题同一个用户在不同设备上登录系统应该怎么对待这些会话最朴素的做法是共享一个会话用户在任何一端登录其他端就都是登录状态。但实际体验很差想象一下你手机APP正在用着PC端同事借你电脑登录了一下手机上的会话直接被顶掉这在业务上完全不可接受。所以我采用了按终端维度隔离会话的策略同一个用户和不同终端类型组合各自拥有独立的Token和独立的会话状态互不干扰。信任级别也是一个必须考虑的因素。PC端后台管理系统的登录是高频、高权限场景我要求它必须经过完整的验证码加密码校验。移动端APP考虑到输入体验允许用验证码登录或者手机号密码登录但首次登录后必须绑定设备指纹比如设备唯一标识后续在安全设置里可以查看和解除绑定。小程序端因为环境特殊Token不能存储在localStorage我存到了小程序自己的storage里同时因为小程序不存在跨域问题API请求相对干净但必须在服务端校验请求来源的Referer或者自定义Header防止被其他Web环境篡改调用。6.2 Refresh Token在多终端的刷新机制前面提到双Token机制多终端场景下Refresh Token的使用有一些额外细节。Refresh Token在生成的时候同样要带上clientType并且Redis缓存key带终端标识这样终端的会话隔离和Token刷新才能对齐。比如APP端的Access Token过期了拿APP端的Refresh Token去刷新后端校验Refresh Token的时候发现clientTypeapp那就只更新APP端这个缓存里的Access Token不会动Web端和小程序端的会话。刷新流程我设计成前端拦截器自动处理不需要业务代码参与。Axios的响应拦截器里如果发现HTTP状态码是401判断错误信息是不是Token过期如果是就调用刷新接口拿新的Token然后重放刚才失败的请求。这里有一个并发问题需要处理如果同一时间有多个请求同时失败每个请求都去调刷新接口就会冗余刷新多次。我的做法是在拦截器里加一个refreshing锁标记第一个401触发的刷新请求把锁置为true后续401的请求不重新发刷新请求而是等待刷新完成后再重放。6.3 不同终端的认证方式差异化设计PC端、移动端、小程序端虽然都走JWT认证但登录方式可以差异化设计。我在auth模块里把登录入口做成了策略模式一个LoginStrategy接口不同终端实现不同的登录逻辑终端类型登录方式验证强度Token有效期Access备注PC浏览器账号密码 图片验证码中2小时支持记住我7天刷新移动端APP手机号 验证码 / 密码高4小时首次登录绑定设备微信小程序微信OAuth授权登录中2小时需要关联手机号第三方客户端API Key Secret高12小时常用于服务间调用第三方客户端的认证方式要额外说一下因为这种情况不涉及用户交互它本质上是一个服务账号。我在用户表里通过user_type字段区分是后台用户还是第三方应用。第三方应用登录时传入的是它自己的API Key和Secret后端校验通过后生成一个Access Token但这个Token的身份是应用、不是某个具体用户权限直接从应用绑定的角色里读取。这样做的好处是可以给第三方应用精细化授权比如一个报表应用只能查看数据不能修改。6.4 跨域和HTTPS的配套配置多终端认证绕不开跨域问题。PC端Web请求API前后端分属不同域名或端口必然触发CORS。我的后端配置里开启CORS允许的域名通过配置文件管理同时在Spring Security的过滤器链里CORS过滤器必须排在认证过滤器之前因为预检请求OPTIONS是拿不到认证信息的得先放行CORS预检否则实际请求永远到不了认证环节。还有一个必须强调的点生产环境下必须上HTTPS且Token不能放在URL参数里除了前面提到的小程序特殊场景。Token一旦通过明文HTTP传输在网络中就等于裸奔。前端Nginx配置好证书和强制跳转后端API服务也在内网通过HTTPS代理暴露。这些看起来是部署细节但直接决定认证系统能不能扛住真实攻击不要偷懒。7. 部署实测与踩坑记录几个文档里不会写的问题7.1 前端刷新404问题动态路由必须重新注册这个坑在5.2节提过但实测中它是出现频率最高的问题之一值得单独展开。前端刷新后浏览器直接向Nginx请求/system/user这样的路径Nginx静态服务器找不到对应的物理文件会返回404。解决办法有两层。第一层是Nginx配置把所有非静态文件请求都rewrite到index.htmllocation / { try_files $uri $uri/ /index.html; }第二层是路由守卫的重新注册逻辑。刷新之后Vue应用重新启动Pinia里没有用户信息路由表里也没有动态路由这时候路由守卫判断到有Token但没初始化权限就会重新调用用户信息接口、重新注册路由、重新跳转。两层配合好之后刷新404问题才真正根治。我见过很多项目只改了Nginx没处理路由守卫刷新页面虽然不再404了但白屏这个细节容易被忽略。7.2 Token过期的边界情况并发请求重放问题上一节提到刷新Token的并发锁这里讲一个更隐蔽的边界场景用户停留在某个页面超过2小时然后点了某个按钮这个请求带的是旧Access Token后端返回401前端拦截器自动刷新Token并重新发起请求。但如果用户此时刷新了整个页面流程变成路由守卫发现Token还在Refresh Token没过期没有跳登录页直接重新拉用户信息而用户信息接口也是需要Access Token的返回401又触发了重放逻辑。逻辑上没毛病但要注意刷新接口不能用过期的Refresh Token再去刷新如果Authenticate接口自己都挂了需要一个全局兜底刷新失败就直接跳转登录页。我实际测试中还遇到过另一个问题用户有多个浏览器标签页开着同一套系统其中一个标签页的Refresh Token过期了刷新后跳转登录页另一个标签页还在正常用。这是因为Token存在localStorage里所有标签页共享。解决办法是在标签页间的storage事件上同步登出逻辑或者更简单一点用短有效的Access Token和相对长有效的Refresh Token同时Refresh Token刷新后直接替换localStorage里的旧Token多标签页共享更新。7.3 权限变更不生效的排查Redis缓存必须主动失效用户权限的变更比如给某个用户添加了一个角色或者调整了角色下的菜单权限如果Redis缓存没有同步失效用户要等Access Token过期最长2小时才能看到新权限。排查这类问题的时候第一反应往往是查数据库数据对不对实际上数据早就对了是缓存没刷。我踩过一次很真实的坑给财务角色新增了一个导出报表按钮权限前端按钮始终不显示后端权限标识列表里也没有。查了半天发现角色菜单关联表已经更新了但登录用户信息接口返回的权限列表是从Redis缓存里读的缓存里还是旧数据。后来我的解决方案很粗暴但有效所有角色权限调整的接口里统一调用一个RedisService.delByPrefix(login:*)把这个角色下所有用户的登录缓存全部删掉。下次用户刷新页面或发任何请求缓存miss自动回源数据库重新加载权限变更即时生效。7.4 慢SQL问题先查权限列表再查业务数据权限系统本身不难但性能问题常在数据量上来之后暴露。用户表几百人还好部门几万个、角色几十个权限校验相关的查询就慢下来了。我遇到最多的问题是MyBatis的N1查询查用户列表时循环查每个用户的角色再循环查每个角色的菜单一个列表接口背后跑了几百条SQL。优化思路有两个方向。第一用户关联角色、角色关联菜单的查询都改成批量JOIN查询一次查出所有关联关系。第二权限相关数据几乎只读多、写少缓存的力度可以放大。我最后是在Redis里存了角色-菜单权限的映射表每当缓存miss时全量加载一次角色权限数据后续所有用户的权限判断都能直接命中缓存。7.5 日志记录的高频写入异步落库登录日志和操作日志是权限系统的必备功能但也是最容易忽视性能的地方。每次用户登录都同步写一条数据库记录请求量上来之后日志表成了性能瓶颈。我的方案是引入一个简单的异步线程池收到日志写入请求后直接丢进内存队列由后台线程批量落库请求响应时间和日志写入解耦。这里有个权衡要说清楚异步化意味着日志可能丢失系统崩溃时未落库的日志就没了。但登录日志和操作日志本来就是辅助审计的99.9%的落库率完全够用。如果业务对日志完整性要求极高那就应该上消息队列加消费者落库成本也会高不少。对于通用权限系统这个定位异步线程池性价比最高。写在最后的几点实操心得做完这套系统我最想分享的心得就是权限管理这个领域简单模型解决大部分问题复杂模型解决剩余问题但复杂模型的维护成本往往大得吓人。不要一开始就追求ABAC、字段级权限、多租户隔离这些概念先把RBAC跑通把认证链路理清把缓存设计好你的系统已经能扛住绝大多数真实业务场景了。另一个心得是权限系统的成败不仅在代码更在配置管理。菜单表、角色表、权限标识的命名规范必须一开始就定好比如统一用模块:实体:操作三段式命名权限标识system:user:add否则业务一多权限标识起得乱七八糟到时候想整理都是一场灾难。最后多终端认证这件事本质是会话管理的艺术。会话隔离、Token过期策略、刷新机制、跨端体验——每一块都需要结合自己的业务场景仔细权衡。建议你先跑通一个终端再逐步扩展不要一上来就想把PC、APP、小程序全部一次做好。这套系统的源码结构已经尽量把各种终端的差异封装在独立模块里但真正拿到你的项目里还是需要根据自己的情况做取舍。
返回列表