简介一套服务于品达通权限管理系统首日开发的配套资料聚焦环境搭建、基础结构与核心编码面向具备Java基础并正接触微服务与权限控制的中级开发者可作为第一天从零实现可运行程序的参考。资料含1991个文件核心为源码与配置包括Java类文件、XML与YML配置文件另有属性配置、容器编排文件、数据库脚本、结构图等支撑从工程导入、配置调整到部署启动的完整流程其中Java源码体现业务逻辑XML负责持久层映射YML用于微服务各组件配置有助于看清不同文件在项目中的分工压缩包约245.54MB。已有628人学习浏览。内容覆盖接口文档集成、身份认证测试、用户实体与传输对象设计、控制层接口等关键模块能够帮助使用者在第一天搭建可运行的项目骨架理解服务组件协作方式与权限校验过程包内目录按模块组织便于按图索骥、定位问题无论自学还是作为教学参考均较合适。1. 第一天学什么先把权限系统的全景图装进脑子里做后端开发的兄弟应该都有这种感觉权限管理系统这五个字听起来平平无奇但真要动手写一套能落地的牵扯的东西比想象中多得多。登录、令牌、角色、菜单、按钮级权限、数据范围哪一环没想清楚后期都是改不完的坑。我整理这套“品达通”权限管理系统的第一天资料和源码时思路很明确第一天不追求功能有多全而是先让一套完整可跑的代码带你转一圈把权限系统的骨架立起来。这套资料的定位就是给刚接触企业级项目、想搞懂权限到底怎么设计的同学准备的也适合手头项目需要快速接入权限模块的开发者拿来参考。第一天真正要做的不是背概念而是“先跑起来再拆开看”。源码里我刻意控制了依赖数量没有引入一堆花里胡哨的微服务组件就用最常见的Spring Boot加MyBatis Plus加Redis把登录认证、令牌签发、角色权限校验这一条主线走通。你跟着跑一遍再对着代码看一遍很多以前似懂非懂的问题——比如为什么要有Refresh Token、为什么权限要用注解切面做、为什么菜单要动态生成——都会有具体的答案。很多人第一次接触权限系统总喜欢先去啃Spring Security的源码或者去研究OAuth2.0的协议细节结果看了两天还在配置类里打转连一个能用的登录接口都没写出来。我的建议是反过来先用一个简单方案把流程跑通知道每一步在干什么再回头看那些框架的源码你会发现它们解决的就是你刚刚手动实现的那堆问题。这也是我把第一天的资料设计成“轻量实现为主、框架对比为辅”的原因。第一天资料的预期收获说白了就三件事第一把RBAC模型是怎么落到数据库表里的搞清楚第二把一次登录请求从Controller到数据库再到返回Token的完整链路跑通第三知道一个受保护的接口是如何通过拦截器加注解完成权限校验的。这三件事搞定权限系统的大门就算踏进去了。至于Redis缓存策略、动态菜单、多租户隔离这些进阶玩法后面资料里会慢慢铺开。2. 核心模型拆解RBAC、认证与授权一次说透2.1 RBAC模型为什么权限系统都离不开五张表RBAC基于角色的访问控制不是什么高深的理论它就是把“给用户直接分配权限”这件事拆成“给用户分配角色再给角色分配权限”两步。为什么要多绕这一下因为现实里一个企业可能有几百上千个用户但岗位角色就那么几个。你要是挨个给用户配权限来一个人事变动就得改一遍维护成本直接爆炸。有了角色这一层新员工入职只需要给他挂一个“运营”角色所有权限自动就有了。这套模型的落地点在于数据库表设计。我给的源码里建了五张核心表用户表、角色表、权限表、用户角色关联表、角色权限关联表。有人会问为什么用户和角色之间、角色和权限之间都需要一张关联表而不是直接把角色ID写在用户表里原因很简单现实中的关系是多对多的。一个用户可以同时是管理员和审核员一个角色也要拥有多个菜单和按钮的权限。如果只靠外键字段去存遇到多对多只能拼字符串或者存数组查询和更新都会变得很痛苦。关联表虽然看起来多写了两个实体但换来的是SQL查询的清晰和后续扩展的灵活这笔账是划算的。权限表的设计也需要好好琢磨。我见过不少项目把权限表只存成菜单名称结果做按钮级控制的时候傻眼了还得再建一张按钮表。我的做法是在权限表里加一个permission_type字段用数字区分目录、菜单、按钮三类权限。这样菜单树和按钮权限就能统一管理前端生成路由的时候可以过滤出目录和菜单后端校验的时候可以把按钮权限码拿出来比对一张表干了两张表的活。2.2 认证逻辑登录不只是查一次数据库那么简单认证Authentication解决的是“你是谁”的问题。第一天的源码里登录接口的逻辑是这样的前端把用户名和密码传过来后端先根据用户名查用户拿到用户后用BCrypt去校验密码原文和密文是否匹配。这里有个细节值得多说一句密码一定不要用MD5去存。MD5本身是摘要算法虽然不可逆但彩虹表攻击实在太成熟了现在随便一个在线网站都能查常见密码的MD5值。BCrypt的好处是每次加密都会混入随机盐同一个密码两次加密的结果都不一样攻击者就算拿到数据库也没法直接反推原文。密码校验通过之后接下来要做的就是签发令牌。第一版资料里我用的方案是JWT加Redis双重配合JWT负责承载用户基本信息Redis负责存token的状态和过期时间。JWT的好处是服务端无状态验签就能拿到用户信息不用每次去数据库查会话但它有个致命的弱点——签发之后无法主动失效。如果用户的密码被改了或者管理员想把某个设备踢下线纯JWT方案是做不到的。所以我在签发JWT的同时会把token的jti存到Redis里设置和JWT相同的过期时间。每次请求进来先看Redis里有没有这个jti没有就直接拒绝。这样既享受了JWT无状态验签的便利又解决了主动失效的问题。2.3 授权逻辑注解切面和拦截器各管一段授权Authorization解决的是“你能干什么”的问题。第一天源码里我采用的是“拦截器加注解加AOP”三层配合的方案。首先拦截器负责最基础的一层检查请求头里有没有带tokentoken是不是合法有效的。这一层不关心用户是什么角色只关心你是不是一个“登录了的合法用户”。用拦截器做这层校验好处是路径匹配灵活比如 /api/login 和 /api/captcha 这些接口可以直接放行其他接口统一拦截。接下来是注解加AOP的细粒度控制。我在写受保护的接口时会在方法上标注注解比如RequiresPermission(system:user:add)。这个注解本身只是个标记真正干活的是用AOP写的切面。请求进入Controller之前切面会拦截到带这个注解的方法然后从当前登录用户的权限码集合里去找有没有对应的权限码有就放行没有就抛异常。为什么授权这层要用AOP而不是在拦截器里统一判断因为拦截器能拿到的只是URL想在拦截器里做权限判断就得维护一套URL和权限码的映射关系而且一个URL可能对应多个权限写起来非常别扭。AOP直接作用在方法上权限码就放在方法旁边的注解里代码即文档维护起来清爽得多。三层各管一段之后整个授权链路就变成拦截器判断是否登录切面判断是否有权限业务代码只关心自己的业务逻辑互不干扰。3. 数据库设计与核心表结构直接照着抄就行3.1 五张表的字段设计思路数据库设计是权限系统最见功底的地方也最容易踩坑。第一天的源码里我给每张表都加了create_time、update_time、deleted这三个公共字段deleted是逻辑删除标志。为什么用逻辑删除而不是物理删除因为权限数据关联关系复杂一张角色表被删掉可能会连带一大堆用户失去权限到时候想恢复都找不到数据。逻辑删除虽然会在查询时多带一个条件但换来的审计和恢复能力是值得的。用户表除了常规的username和password我还加了nickname、avatar、status三个字段。status特别关键它用来做账号的启用和禁用。很多初学者会把禁用账号和删除账号混为一谈一旦要封禁某个用户就把记录删了这是非常危险的操作——用户的操作日志、关联的业务数据全跟着没了。角色表和权限表相对简单角色表有role_name和role_code权限表有parent_id和permission_type。这里要单独说一下role_code的设计。很多项目角色表就一个中文名称字段但代码里判断角色的时候总不能拿中文去比硬编码字符串又容易出错。所以我在源码里设置了一个规则role_code用大写字母加下划线表示比如SYSTEM_ADMIN、OPERATOR同时要求全局唯一。代码里判断角色时都用这个code显示给用户看的时候再关联查询出role_name。这样既保证了代码的健壮性又兼顾了界面的可读性。3.2 一次登录请求的完整SQL链为了帮第一天接触这套系统的同学快速建立“SQL和数据表”的联系我把一次登录认证涉及到的SQL链路拆出来说一下。当用户输入用户名密码点登录后台实际发生的查询是这样的首先通过username去用户表查出用户的全部信息拿到用户的id和加密后的密码然后通过用户id去用户角色关联表查出所有的角色id接着用这些角色id去角色权限关联表查出所有的权限id最后用权限id去权限表查出具体的权限码和菜单路由。这一连串的关联查询看起来多但实际执行起来并不慢因为每一层都是通过主键或者唯一索引去查的。我也准备了另一种方案在用户表里冗余一个permissions字段登录后一次性把权限码拼成字符串放进去查询的时候就不用跨越四张表了。但冗余带来的问题是权限变更后缓存同步变得非常麻烦。第一天资料里我用的是标准的多表关联方案等后面讲Redis缓存的时候再给大家上“权限码预热”的优化思路。先把一条干净的链路跑通永远比一开始就优化来得重要。3.3 初始化数据要注意的细节第一次启动项目你会发现数据库里如果没有初始化数据登录接口会直接报用户不存在。所以我在源码的resources目录下放了一份schema.sql和data.sql前者是建表语句后者是初始化数据。初始化数据里内置了一个超级管理员账号和一个普通运营账号密码我都统一初始化为Admin123456第一次登录后强烈建议改掉。初始化数据的角色和权限映射我建议不要偷懒一次性全塞进去而是要按真实业务场景去配置。比如超级管理员拥有所有权限运营角色只拥有用户查询和内容审核的权限。这样在测试接口权限的时候你只需要切换不同的账号登录就能直观地感受到注解权限校验在起作用比用同一个账号反复测试有意义得多。4. 核心源码解析从登录接口到权限注解逐行拆开看4.1 登录接口的代码实现先看登录接口的入口代码我把它放在AuthController里面路径是/api/auth/login。请求进来之后首先校验验证码。这里的验证码我用的方案是Redis存储生成一个UUID作为key验证码的字符串作为value同时把验证码图片的base64和UUID一起返回给前端。前端提交登录请求时要带上UUID和用户输入的验证码后端从Redis里取出value去比对。你可能会问为什么不能把验证码直接存在Session里因为Session在多实例部署的情况下会出现同步问题而且前后端分离的项目里Session本身就不太友好Redis天然是共享存储换谁处理都一样。PostMapping(/login) public ResultLoginResponse login(RequestBody LoginRequest request) { // 1. 校验验证码 String code redisTemplate.opsForValue().get(captcha: request.getUuid()); if (code null || !code.equalsIgnoreCase(request.getCaptcha())) { throw new BizException(验证码错误或已过期); } // 2. 查询用户 SysUser user userService.getUserByUsername(request.getUsername()); if (user null || !BCrypt.checkpw(request.getPassword(), user.getPassword())) { throw new BizException(用户名或密码错误); } // 3. 生成JWT并写入Redis String jwtId UUID.randomUUID().toString(); String token jwtUtil.createToken(user.getId(), user.getUsername(), jwtId); redisTemplate.opsForValue().set(token: jwtId, user.getId().toString(), jwtUtil.getExpireTime(), TimeUnit.SECONDS); // 4. 组装权限码和菜单返回前端 SetString permissions permissionService.getUserPermissionCodes(user.getId()); return Result.success(buildLoginResponse(token, permissions)); }这段代码里有两个容易出错的点我得提醒你。第一个是BCrypt的校验方式很多人会写成BCrypt.hashpw(password, user.getPassword()).equals(user.getPassword())虽然也能跑通但BCrypt提供现成的checkpw方法底层已经帮你处理了盐值的提取直接用就行。第二个是JWT签发之后Redis里的过期时间一定要和JWT的过期时间保持一致不然会出现JWT还没过期、Redis里的记录已经被清了或者反过来Redis还在、JWT已经过期了的尴尬状态。4.2 权限校验注解和AOP切面的实现我自定义了一个注解叫RequiresPermission里面只有一个value属性用来接收权限码。这个注解用Target(ElementType.METHOD)标注说明它只能用在方法上。然后用AOP写了一个PermissionAspect切面用Around注解包住带RequiresPermission的方法。这里有个关键问题切面怎么拿到当前用户的权限码集合我的方案是封装了一个SecurityUtils工具类内部使用ThreadLocal保存当前登录用户的信息。拦截器在验证token通过之后就会把从token里解析出来的用户信息丢进ThreadLocal切面和业务代码都可以随时从ThreadLocal里取。Aspect Component public class PermissionAspect { Around(annotation(requiresPermission)) public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresPermission requiresPermission) throws Throwable { String requiredPermission requiresPermission.value(); SysUser currentUser SecurityUtils.getCurrentUser(); SetString permissions currentUser.getPermissions(); if (!permissions.contains(requiredPermission)) { throw new ForbiddenException(无访问权限); } return joinPoint.proceed(); } }AOP切面这块有个细节值得琢磨为什么用Around而不用Before因为Before只能做前置判断如果你在Before里抛了异常那请求就断了业务方法不会执行这本身没问题。但Around更灵活的地方在于你可以在放行之后做后置处理比如记录操作日志、更新最后登录时间。第一天我虽然只做了前置校验但用Around写好后面要扩展就只需要在proceed()之后加逻辑就行了不用改切面的声明方式。4.3 网关和拦截器的配合白名单配置与Token刷新拦截器这块我选择了继承HandlerInterceptorAdapter覆写preHandle方法。preHandle里做的三件事分别是从请求头Authorization里取出token字符串调用JwtUtil验证token的签名和过期时间是否合法然后把用户信息放进SecurityUtils。这里要特别提一下请求头取token的习惯很多项目用的key是token但业界更通用的写法是Authorization: Bearer 。这样做的目的一方面是习惯问题另一方面是以后如果要接OAuth2.0或者Spring Security它们的过滤器默认就是从Authorization头里解析Bearer令牌的。拦截器注册的时候要注意白名单的配置。我在WebMvcConfig里通过addPathPatterns拦截所有接口再通过excludePathPatterns放行登录、验证码、Swagger文档这些无需认证的路径。第一次跑项目的时候我发现很多人会在这里踩坑Swagger文档没放行导致接口文档页面打不开还以为是Swagger配置有问题。这里提供我项目的白名单清单可以参考一下。接口路径说明/api/auth/login登录接口/api/auth/captcha验证码接口/doc.htmlSwagger文档首页/webjars/**Swagger静态资源/v3/api-docs/**OpenAPI接口定义/api/test/public公开测试接口除了白名单配置第一天我还预留了一个“静默续期”的接口设计。思路是前端在拦截器返回token有效期不足信号时自动调用一个/refresh接口用旧token的jti去换一个新token。这个逻辑我建议第二天再深入展开第一天先把主链路跑通。如果你感兴趣可以看一下我源码里JwtUtil的refresh方法里面有一个简单的实现思路。5. 前端要点简析动态路由与按钮权限的配合第一天的资料虽然以Java后端源码为主但权限系统从来不是后端单方面的事。前端怎么根据权限动态生成菜单、怎么控制页面里的按钮显示这些同样是权限系统能否落地的关键。我在资料里附带了一个简化版的前端权限控制说明用的方案是Vue3加Vue Router。登录成功后后端返回权限码集合和菜单树数据前端把这些数据存在Pinia里然后通过router.addRoute方法动态注册路由。这里的核心是菜单数据里每一条都包含component字段前端根据这个字段去映射对应的Vue组件文件路径实现路由和组件的关联。按钮权限这块最朴素的实现方式就是v-if配合权限码判断。我封装了一个自定义指令v-permission用法是v-permissionsystem:user:add指令内部会从Pinia里取出用户权限码集合判断当前元素是否渲染。用指令的好处是模板里不需要写一长串的判断逻辑看起来干净以后权限逻辑变了也只需要改指令内部一处。const permission { mounted(el, binding) { const required binding.value const userPermissions useUserStore().permissions if (!userPermissions.includes(required)) { el.parentNode el.parentNode.removeChild(el) } } } export default permission前端动态路由这块有个很常见的坑刷新页面后路由会被清空因为Pinia的数据是存在内存里的。解决方法是刷新的时候重新调用一次获取用户信息的接口把权限码和菜单再拉一遍然后再addRoute。我见过很多初学者在这个问题上卡了一下午明明登录好好的一刷新就白屏就是因为没有处理“恢复用户状态”这个逻辑。第一天资料里我在前端工程的main.js里写了一个beforeEach钩子每次路由跳转前都检查一下Pinia里有没有用户信息没有就先调用接口恢复状态这就是一个还算标准的解决方案。6. 第一天实操中的高频报错与解决办法6.1 登录接口报401但明明账号密码没问题这个情况90%是验证码的问题。因为验证码是存在Redis里的如果你没有启动Redis或者Redis的key过期时间设得太短验证码会一直校验不过。排查方法很简单先看后端有没有连接Redis成功的日志再看Redis里有没有captcha开头的key。另外一个容易忽略的点是UUID在传输过程中可能大小写被改变了Redis的key是区分大小写的所以前端提交UUID时千万不要做toLowerCase操作。6.2 AOP切面拦不到自定义注解如果切面没有生效先检查几个地方第一确认启动类上加了EnableAspectJAutoProxy注解Spring Boot默认开启但如果你改过配置可能会被关闭。第二确认切面类被Spring扫描到了组件扫描路径是不是覆盖了切面所在的包。第三确认数据库里当前登录用户没有对应的权限码。很多时候不是你代码写错了而是用户压根没配这个权限接口当然就“无访问权限”了。这种情况的排查思路是登录后把返回的权限码集合打印出来看看里面有没有你在接口上标注的权限码。6.3 Redis和数据库连接报Connection refused这个问题基本就是环境没启动。Windows环境下的同学建议直接下载Redis的Windows发行版解压后运行redis-server.exe就能起来。macOS环境下用brew install redis安装后需要手动运行redis-server命令。数据库这里我用的MySQL 8.0连接串里可以设置allowPublicKeyRetrievaltrue和useSSLfalse避免8.0版本的公钥检索报错。你还需要确认时区参数serverTimezoneAsia/Shanghai不然日期字段会出现时区偏移。6.4 前端页面刷新后白屏或菜单全丢刚才动态路由那节提过这个问题。核心原因就是页面刷新后Pinia里的用户状态被清空动态添加的路由也跟着没了。解决方案我在前端的router.beforeEach钩子里做了处理每次路由跳转前先检查用户状态如果为空则调用获取用户信息接口再重新addRoute最后再执行next({...to, replace: true})把这次路由跳转重放一遍。这段逻辑看起来绕但你只要理解了它是在模拟“刷新后重新登录”的过程就明白为什么需要写这个重放了。6.5 首次启动数据库时提示表不存在很多人会直接把schema.sql往Navicat里一拖就执行但如果你的MySQL版本和我的有些出入可能会有报错。我的建议是先用source命令在命令行里执行schema.sql再执行data.sql这样如果某条SQL报错你能很清楚地看到报错位置。源码里所有的建表语句我都加了IF NOT EXISTS判断所以重复执行不会报错这一点第一次跑项目的时候非常友好。7. 第一天的收尾这套源码还能往哪些方向扩展第一天的源码和资料本质上就是一套精简但五脏俱全的权限系统骨架。你跑通它、读懂了它接下来就可以根据自己的项目需求去加东西了。我梳理了三个我认为优先级最高的扩展方向给你在第二天的学习里做一个参考。第一个方向是引入Spring Security框架用框架机制替代手写的拦截器加AOP。虽然当前方案能跑但Spring Security内置了Session管理、CSRF防护、OAuth2客户端等一堆企业级能力尤其是老项目可能要对接第三方登录或者单点登录手写一套成本完全不划算。但我不建议第一天就去搞先把当前方案的拦截器注解逻辑彻底吃透再去看Spring Security的FilterChain你会发现一切都是熟悉的配方只是换了一套更规范的包装。第二个方向是完善动态菜单和前端权限体系。第一天的前端权限说明只是一个最简版本真正的企业项目还要考虑页面级缓存、Tab页签联动、目录展开状态保存这些体验优化。还有按钮级权限的缓存策略如果用户权限变化了前端要通过WebSocket或者轮询及时刷新不然会出现用户权限已经撤回但前端界面按钮还在的尴尬情况。第三个方向是引入更细粒度的数据权限。目前这套系统只做到“功能权限”也就是能不能访问某个接口。但实际业务里经常需要的是“数据权限”比如销售只能看自己的客户数据部门主管能看整个部门的客户数据。数据权限的实现一般是在SQL层面做数据范围的过滤常见的做法是在权限表里加一个data_scope字段然后用MyBatis的拦截器在SQL执行前动态拼接查询条件。这个方向比较复杂也是目前市面上面试题里问得最多的高频考点。对我来说第一天的资料最大的价值不是让你照着抄代码而是帮你建立起“权限系统到底由哪几块拼成”的全局认知。你能说清楚认证和授权的区别能画出用户角色权限的关系图能知道一次请求从头到尾经过了哪些校验环节这套资料就算真正学到手了。后面几天的内容会围绕分布式会话、权限缓存、网关统一鉴权这些更进阶的主题展开但基石就是今天这些东西。建议你把源码仓库保留好跟着后面的内容继续往下走。本文还有配套的精品资源点击获取