企业招聘系统的权限管理,看着是个老生常谈的话题,但真到自己从零搭建一套,才发现坑远比想象的多。尤其招聘系统这种角色多、数据敏感的业务——HR、部门主管、面试官、求职者、管理员,每个角色能看什么、能改什么、能审批什么,稍微没控住就是越权泄露或者误操作事故。这个项目折腾下来,我最大的感受是:权限管理不是加个拦截器判断一下角色名那么简单,它是一套从数据模型、认证方式到接口鉴权、前端渲染层层配合的体系。这篇就把我实际落地的RBAC权限模型、Spring Security + JWT认证授权,以及针对招聘场景做的安全加固方案整个拆开来讲,附上能直接跑的源码思路,给正在做同类系统的同学一个参考。
先说下这个系统的背景:技术栈是 Spring Boot 2.7 + Vue 3 + MyBatis Plus + MySQL 8,典型的单体应用加前后端分离。招聘系统的核心业务线有三条:职位发布与管理、简历投递与筛选、面试安排与Offer审批。对应过来,角色至少需要五类:平台管理员(超管)、HR(招聘专员)、部门面试官、普通员工(内推)、求职者。业务上还要求部分权限能细分到数据范围,比如HR只能看自己负责的职位下的简历,部门主管只能审批自己部门的Offer。这决定了纯用角色字符串判断是撑不住的,必须上RBAC,而且是带数据权限的RBAC。
1. 权限模型选型与设计思路
1.1 为什么是RBAC而不是ACL或其他模型
做权限设计前,我先对比了ACL(访问控制列表)和RBAC(基于角色的访问控制)两种主流方案。ACL的思路是把权限直接挂在用户身上,像文件系统的权限一样,"谁对什么资源有什么操作"。在小系统里这很简单直接,但一旦用户量超过几十个、角色分工开始细化,ACL就变成灾难——每加一个用户就得重新配置一大串权限,而且权限调整时要挨个用户去改,极易漏改和改错。
RBAC的核心是引入"角色"这一层中间概念:用户关联角色,角色关联权限。招聘系统这种场景恰好多角色、权限重复度高,RBAC能把权限管理收敛到"给角色配权限"这一件事上,用户的变化只影响用户和角色的绑定关系,不影响底下权限的配置结构。
RBAC本身有几种变体:RBAC0是最基础的用户-角色-权限三层;RBAC1加了角色继承(比如HR经理继承HR的全部权限再加审批权);RBAC2加了职责分离约束(比如同一个角色不能同时拥有创建职位和审核职位的权限);RBAC3就是继承加约束的组合。我在项目里用的是RBAC0加轻量级的职责分离:基础权限全部靠菜单-按钮粒度控制,涉及敏感操作(如批量删除简历、调整Offer薪资)单独加权限标识校验。这里没有上角色继承,是因为招聘系统的角色层级很扁平,硬套继承反而会让权限判断变得隐晦难排查。
另外要说一下"多租户"的问题。很多企业招聘系统会做成SaaS平台供多家公司使用,那就要在RBAC之上再叠加租户隔离。我这个项目是单企业自用,数据隔离靠部门ID和创建者ID做行级数据权限就够了,租户那套暂不上,否则初期复杂度会高很多。如果你的项目是多公司入驻的,一定在表设计阶段就要预留tenant_id字段,不然后期改造数据隔离会非常痛苦。
1.2 数据库表设计与权限数据流转
RBAC的标准表结构至少包含五张表:用户表、角色表、权限表、用户-角色关联表、角色-权限关联表。实际项目里我加了两张辅助表:部门表和菜单表,因为招聘系统需要"用户属于哪个部门"的数据,而权限落到前端表现为菜单和按钮,需要有菜单表来驱动动态路由和页面按钮显示。
表结构大致是这样:
-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), department_id BIGINT, status TINYINT DEFAULT 1 COMMENT '1启用 0禁用', create_time DATETIME ); -- 角色表 CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(50) NOT NULL UNIQUE, role_name VARCHAR(50) NOT NULL, data_scope TINYINT DEFAULT 2 COMMENT '1全部 2本部门 3本人', create_time DATETIME ); -- 权限表(菜单权限) CREATE TABLE sys_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT 0, perm_name VARCHAR(50), perm_code VARCHAR(100) COMMENT '如 hr:resume:export', perm_type TINYINT COMMENT '1目录 2菜单 3按钮', path VARCHAR(200), component VARCHAR(200), icon VARCHAR(50), sort_order INT ); -- 用户角色关联表 CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) ); -- 角色权限关联表 CREATE TABLE sys_role_permission ( role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL, PRIMARY KEY (role_id, permission_id) ); -- 部门表 CREATE TABLE sys_department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT 0, dept_name VARCHAR(50) );初次看到这套表结构的人容易有个疑问:data_scope字段是干嘛的?这就是招聘系统里很关键的数据权限设计。比如HR经理和普通HR都有简历管理权限,但HR经理能看到全部简历,普通HR只能看自己名下职位收到的简历,这时候单靠"有没有简历管理这个权限标识"无法区分,必须在角色上定义数据范围。我用的是1全部、2本部门、3本人三档,查询时动态拼接SQL条件,后面讲数据权限实现会细说。
权限数据流的完整路径是这样的:用户登录成功之后,服务端根据用户ID查出所有角色编码,再根据角色查出所有权限标识(permission_code),把两者存进Redis缓存。每次请求接口时,Spring Security的过滤器会从请求头取出JWT,解析出用户ID,再从Redis拿权限集合,判断当前请求所需的权限标识是否在集合中。前端则是登录后调用"获取用户信息"接口,拿到权限标识列表和菜单树,动态生成侧边栏路由和按钮的显隐。整条链路下来,核心就是**认证(你是谁)→ 授权(你能做什么)→ 数据过滤(你能看哪些数据)**三层,业务代码里完全不散落权限判断逻辑,都在统一的地方处理。
2. 认证与授权核心环节实现
2.1 认证流程设计:从登录到Token颁发
招聘系统的登录场景分两种:内部人员(管理端)和求职者(门户端)。两者我用了同一个认证服务,但在Token里加了userType字段区分,避免求职者Token跑到管理端接口去。这里有个很容易忽略的点:管理端的JWT密钥和门户端的必须分开,或者至少在Token中带角色类型标识并在网关/过滤器中校验,否则求职者可以自己构造一个Token试出管理员的密钥,直接横向越权。
登录接口的逻辑流程我梳理一下:
- 接收用户名和密码,先做前置校验:用户是否存在、状态是否启用、账号是否锁定。
- 密码校验不直接比对明文,而是用BCrypt算法验证。BCrypt每次生成的哈希都带随机盐,就算两个用户密码相同,存储的哈希值也不一样,能有效对抗彩虹表攻击。
- 校验通过后,加载用户的角色和权限,生成JWT。JWT的payload部分我存了这些字段:用户ID、用户名、userType、角色编码列表、过期时间。权限字段不放进JWT,而是放Redis,原因是要支持权限变更后实时生效,JWT一旦签发没法主动作废,但Redis可以随时删掉让用户重新登录。
生成Token的工具类核心代码大致这样:
public String createToken(LoginUser loginUser) { Map<String, Object> claims = new HashMap<>(); claims.put("userId", loginUser.getUserId()); claims.put("username", loginUser.getUsername()); claims.put("userType", loginUser.getUserType()); claims.put("roles", loginUser.getRoles()); return Jwts.builder() .setClaims(claims) .setSubject(loginUser.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expireTime * 1000)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); }关注几个参数:密钥我这里用的是HS256对称加密,密钥长度至少32字节,直接写在配置文件里,通过@ConfigurationProperties注入。过期时间设置的是管理端8小时、门户端24小时,这个在项目的application.yml里通过不同配置项区分。注意JWT里不要放敏感信息,比如手机号、身份证号这类,因为JWT的payload只是Base64编码,不是加密的,能轻易解码看到内容。我自己见过有人把用户手机号放JWT里,前端拿到Token一解Base64全暴露了,这是低级错误。
2.2 授权环节实现:SecurityFilterChain与动态鉴权
Spring Security的授权核心是过滤器链和SecurityFilterChain。我在配置类里定义了规则:放行登录接口、获取验证码接口、投递简历接口(因为求职者需要先注册),其他所有接口都需要认证。
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests() .antMatchers("/api/auth/login", "/api/auth/register", "/api/captcha").permitAll() .antMatchers("/api/portal/**").hasAnyRole("CANDIDATE", "ANONYMOUS") .anyRequest().authenticated() .and() .exceptionHandling() .authenticationEntryPoint(restAuthenticationEntryPoint) .accessDeniedHandler(restAccessDeniedHandler); http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }这段配置里有两个容易踩坑的细节:
一是csrf().disable()。前后端分离用Token认证,CSRF防护本身就是冗余的,不关掉的话POST请求会因为拿不到CSRF Token全部403。但如果你的系统有浏览器直接渲染页面、依赖Cookie的场景,那就不能关,要区分场景。
二是SessionCreationPolicy.STATELESS。Spring Security默认会创建Session,前后端分离必须改成无状态,否则即使你用了JWT,Security上下文还是会在Session里存一份,导致服务端有状态残留,集群部署时Session同步问题会找上门。
但是配置了anyRequest().authenticated()只能保证请求来自一个登录用户,没法区分这个用户到底有没有权限访问某个接口。比如普通求职者也能调管理端的"删除简历"接口,如果只是authenticated状态,请求能直达Controller。所以我在每个需要鉴权的接口上用**@PreAuthorize**注解做细粒度控制:
@PreAuthorize("hasAuthority('hr:resume:delete')") @DeleteMapping("/resume/{id}") public R<Void> deleteResume(@PathVariable Long id) { resumeService.deleteResume(id); return R.ok(); }hasAuthority的字符串来自sys_permission表里配的perm_code,加载到Redis里后,Spring Security每次请求会把这些权限放进SecurityContext的Authentication对象。这里要特别提醒,角色和权限是两个维度:hasRole('ADMIN')检查的是ROLE_ADMIN前缀,hasAuthority('hr:resume:delete')检查的是精确的权限标识。之前有个同事把两者混用,权限标识配了ROLE_前缀结果一直403,排查半天,其实是对Spring Security的校验逻辑理解有偏差。
2.3 前端权限控制与菜单动态渲染
服务端权限只解决接口层面的控制,前端如果不配合,用户体验会很怪:界面上所有按钮都显示,点一下弹出"无权限"。所以前端也要做一套和RBAC联动控制。
Vue 3项目里我用的是动态路由方案:登录后调用**/api/auth/userinfo接口,返回两个数据——菜单树和权限标识数组。菜单树用来动态注册路由(router.addRoute),权限标识数组存进Pinia的store里,写一个自定义指令v-perm**控制按钮显隐:
// permission指令 const permission = { mounted(el, binding) { const { value } = binding; const store = useUserStore(); if (value && !store.permissions.includes(value)) { el.parentNode && el.parentNode.removeChild(el); } } };<el-button v-perm="'hr:resume:export'" type="primary">导出简历</el-button>按钮指令本身有个问题:如果用户权限是从缓存里异步加载的,指令执行时permissions可能还是空数组,按钮会被误删。解决方法是改成v-if + store判断,或者用函数式写法在渲染前确保permissions已加载。我一开始直接上指令版本,刷新页面后一堆按钮消失,后来改成路由守卫里先await userinfo接口再挂载路由和渲染页面,才稳定下来。
菜单这里的处理也要说一句:动态路由的注册时机必须放在路由守卫里,不能放在App启动时同步执行,因为菜单数据是异步的。典型写法是在beforeEach里判断store里没有路由时就调接口、生成路由表、addRoute,然后next({ ...to, replace: true })重新进入一次路由。
3. 安全加固与攻防实测
3.1 密码存储与登录防爆破
招聘系统里用户的密码安全保障我认为是最基础也最不能出问题的一环。项目里用了BCrypt来做密码哈希,实际在生产上线的过程中,我还对登录接口做了几个加固措施。
BCrypt的正确打开方式:注册时每次生成新的盐做哈希,登录时把用户提交的明文密码和库里的哈希值一起交给BCrypt去校验。注意BCrypt的强度因子strength,默认是10。当初我把这个参数调到12,结果压测时登录接口的吞吐量掉了将近四成,因为BCrypt是刻意设计成计算慢的算法,强度每加一档计算时间翻倍。建议在安全性和性能之间取平衡,10足够日常防护,如果对安全性有更高要求而且登录接口有独立网关限流,可以调到11。
登录防爆破这一块,我用了双重策略:短期失败锁定和图形验证码。连续输错5次账号锁定30分钟,用户表里加lock_time字段,锁定期间直接拒绝登录请求。这里有个优化点是锁定提示语别直接说"账号已锁定",而是统一提示"用户名或密码错误",防止攻击者通过提示差异遍历出系统里有哪些有效账号。验证码我用的是开源的Hutool工具类生成,服务端把验证码答案存Redis、有效期5分钟,前端拿一个captchaId来取图。要注意校验验证码时要先比较、成功后就删除,防止同一个验证码被重放。
我还为登录增加了一个简单的来源IP白名单功能:管理端的登录接口只对内部办公网络IP开放,求职端登录接口不受限制。如果你的系统部署在云上,这个可以通过安全组或Nginx层限制,不必写死在代码里,但可以做一层兜底。
3.2 数据传输与越权漏洞防护
越权漏洞是招聘系统最容易出现的高危问题,因为业务天然按"谁创建的职位、谁负责的简历"来组织数据。我总结下来,越权分两种:水平越权和垂直越权。垂直越权用上一节说的@PreAuthorize就能挡住,水平越权才是真正要业务代码里花心思的。
水平越权的典型场景:一个普通HR登录后,把请求里的简历ID改成别人的,删除了另一个HR负责的简历,因为后端没有校验"这条简历的负责人是不是当前登录用户"。解决方式是在Service层统一加数据归属校验。我在简历表加了owner_id(负责人ID)和department_id字段,删除或更新简历时强制校验:
@Override public void deleteResume(Long id, LoginUser loginUser) { Resume resume = resumeMapper.selectById(id); if (resume == null) { throw new BizException("简历不存在"); } // 数据权限校验:超管可直接操作,其他角色必须匹配归属 if (!loginUser.isAdmin() && !loginUser.getUserId().equals(resume.getOwnerId())) { throw new BizException("无权操作该简历"); } resumeMapper.deleteById(id); }这套逻辑看起来简单,但实现时要留意两个坑:第一,不能把校验逻辑散落在Controller里,每个接口写一遍很容易漏,建议抽象成数据权限切面或者抽一个BaseService统一处理;第二,前端隐藏按钮不等于后端安全,攻击者完全可以绕过页面直接构造HTTP请求,所以校验必须无条件放在服务端执行。
数据在传输过程中的安全我也做了两手:一是全站启用HTTPS,这个不多说,现代应用是底线,Nginx层配置证书即可;二是敏感接口的响应数据做了脱敏。进入招聘系统的简历包含手机号、邮箱、学历信息,不能让所有角色都看到完整信息。比如面试官看简历时,手机号中间四位应该打码,只有HR能看完整号码。我在查询简历列表的SQL里直接处理了字段,或者用一个脱敏工具类在序列化时根据当前用户角色动态替换。这个点容易忽略,等接到求职者投诉说面试官把他的手机号泄露给陌生人才想起来要改。
3.3 接口安全与日志审计
接口层安全主要防两类问题:接口滥用和请求篡改。招聘系统的简历投递接口天然会被脚本刷,有人写个Python脚本批量投简历,直接把数据库搞瘫。我这边在网关上做了一层简单限流:基于IP和用户ID双重维度,同一个IP每分钟最多调用投递接口20次,超过直接返回429。实现用Redis的INCR加过期时间,伪代码如下:
String key = "rate:deliver:" + userId + ":" + DateUtil.format(now, "yyyyMMddHHmm"); Long count = redisTemplate.opsForValue().increment(key); if (count == 1) { redisTemplate.expire(key, 1, TimeUnit.MINUTES); } if (count > 20) { throw new BizException("操作过于频繁,请稍后再试"); }登录接口和短信接口也需要类似的限流,只是阈值要更低,比如登录接口同样IP每分钟最多5次。个人觉得这是性价比极高的安全措施,代码量不大,但能挡住绝大多数脚本攻击。
日志审计这块,招聘系统涉及简历查看、下载、导出、Offer审批等敏感操作,必须留痕。我封装了一个**@AuditLog注解**加AOP切面,标注了注解的接口会自动记录:操作人ID、操作时间、操作类型、请求参数(脱敏后)、IP地址、操作结果。查日志的规则是"只记录敏感操作,不记录查询类高频操作",否则日志表一天涨几十万条。数据保留90天,到期自动清理任务删一波。
日志里还要特别注意不记录明文密码和完整手机号,否则日志一旦泄露比业务数据泄露更麻烦。AOP切面在做参数记录时统一过一遍过滤器,把password、phone这类字段替换成星号。
4. 常见问题与排查技巧实录
4.1 高频问题实录与解决方案
这个系统从开发到联调到上线,前前后后修了不少权限相关的Bug,我挑几个典型的放在这里,基本覆盖了权限系统开发中最容易踩的坑。
问题一:权限修改后用户不生效。权限数据存在Redis里,角色权限在后台调整后,用户还在旧权限缓存中。最初的处理是每次调整权限就把涉及角色的缓存全删掉,让用户下次请求时重新加载。后来发现瞬时的高并发场景下会导致缓存穿透,于是改成双版本号方案:Redis里存权限时带一个roleVersion值,每次权限变更自增,JWT过滤器中读取用户信息时拿当前roleVersion和缓存对比,不一致就重新加载。这里依据实际并发规模决定用哪种,普通系统直接清缓存也行。
问题二:跨域配置导致OPTIONS请求401。前后端分离肯定要配CORS,但我一开始只写了一个简单的跨域过滤器,结果前端调试时发现所有POST请求都先弹401。原因在于预检请求OPTIONS不带Authorization头,被JWT过滤器拦截了。解决办法是在JWT过滤器中判断请求方法是OPTIONS就直接放行,不需要认证。
if (request.getMethod().equalsIgnoreCase("OPTIONS")) { filterChain.doFilter(request, response); return; }问题三:自定义权限注解不生效。我在一个接口上加了@PreAuthorize,结果发现没加权限的普通用户也能调用。排查后发现问题出在启动类上没加**@EnableGlobalMethodSecurity(prePostEnabled = true)**。Spring Security默认不开启方法级注解,必须在配置类或启动类显式开启,这个开关藏得比较深,网上很多教程默认你已经开了。
问题四:前端路由刷新后404。动态路由加载后,用户刷新页面时Vue Router发现当前路径没有匹配的路由,直接跳到404。这是因为路由是异步注册的,刷新后Store里的菜单数据丢失,路由表还是初始状态。解决思路是持久化菜单数据到localStorage或Pinia配合持久化插件,刷新时先从本地恢复路由再匹配。我用的是pinia-plugin-persistedstate把用户store持久化到localStorage,刷新时路由守卫里检查store里有没有菜单数据,有就直接恢复,没有再重新调接口拉取。
问题五:数据权限拼接SQL注入风险。数据权限需要根据data_scope动态拼接查询条件,比如"本部门"就拼上department_id = ?。这里严禁用字符串拼接方式构造SQL,因为部门ID一旦来自前端请求参数就有注入风险。正确方式是MyBatis Plus的QueryWrapper条件构造器或者自定义XML里用#{}参数占位,不要用${}。
4.2 权限模块性能优化实践
权限系统刚上线时,每个请求都查数据库拿用户权限,高峰期数据库连接池直接被占满。优化分了三步:
第一步是Redis缓存权限数据。上面已经提到,登录后把角色和权限集合写入Redis,expireTime设为4小时。请求过程中JWT过滤器从Redis取权限,不再查库。Redis的数据结构我用了String存JSON数组,因为权限集合就一个列表,没有复杂查询需求。但要注意缓存版本的更新机制,权限变更时旧缓存要主动删除或等过期,公司内部系统可以接受最长4小时的延迟。
第二步是缓存权限校验结果。招聘系统里很多页面需要同时加载多个接口,每个接口都做一遍权限集合遍历,虽然都是内存操作,但频繁大数组遍历也有损耗。我把"用户ID + 接口权限标识"作为key,把校验布尔结果缓存到本地Caffeine中,过期时间设为30秒。这样同样一个接口在短时间内被多次调用,权限校验只会真正执行一次。注意本地缓存在多实例部署时各有各的缓存,但权限校验结果是幂等的,影响不大。
第三步是SQL层面优化。查询用户列表时要关联用户表、部门表、角色表,我一开始用的是嵌套子查询,单表数据量过万之后执行时间从几十毫秒慢慢涨到几百毫秒。后来改成MyBatis Plus的分页插件加一个专门的VO查询,把关联查询的字段精简到真正需要的范围,并在user表的department_id上建了联合索引。查询从平均180ms降到40ms,效果明显。
权限这块还有一个被忽略但实际上非常重要的性能点:JWT过期时间的续期策略。系统里很多用户登录后一直操作8小时以上,Token一旦过期就强制退出,体验很差。我实现了一种类似"滑动过期"的逻辑:JWT过期时间前端在请求拦截器里提前判断,如果Token剩余有效期小于半小时,前端先调用刷新接口换新Token。刷新接口校验旧Token的签名和有效期,如果已过期就要求重新登录,如果还在有效期内就颁发新Token。这个机制我自己用了很久,比写一个无限期Token再不停踢人优雅很多,也比Lua脚本做黑名单要简单可靠。
5. 源码使用与二次开发建议
5.1 项目结构与核心代码位置
这个项目的源码我按模块化结构组织,拿到手先看几个核心包:
src/main/java/com/company/recruit/ ├── common/ # 通用响应体、异常处理、工具类 ├── config/ # Security配置、Redis配置、跨域配置 ├── security/ # JWT过滤器、登录用户对象、认证入口 ├── system/ # 用户、角色、权限、部门管理模块 ├── recruit/ # 职位、简历、面试、Offer业务模块 └── aspect/ # 操作日志切面、数据权限切面新手拿到源码最容易迷路的地方是Security的过滤器链没法Debug看到过程,我建议把断点打在JwtAuthenticationFilter的doFilterInternal方法上,一路跟一遍就清楚请求从入口到Controller之间的完整路径了。权限相关的SQL初始化脚本在sql/init.sql里,里面预置了管理员、HR、面试官、求职者四种角色和对应的权限数据,跑起来之后可以直接拿这些账号登录测试。
数据库脚本里塞了示例账号:admin / Admin@123456,以及test_hr / Hr@123456。上线前务必删掉这些默认密码的账号,实战中很多事就是栽在默认密码上。我见过某公司系统上线半年后被扫出来后台管理员的密码还是admin123,整库数据都被拖走了。第一次启动项目时,记得修改application.yml里的数据库连接信息和Redis地址。
5.2 面向你自身场景的裁切方案
这套权限设计不是银弹,有些模块是可以用"轻量替代"的。如果你的招聘系统是给小型团队用的,比如只有3个HR、10个面试官,不需要部门数据权限这一层,可以简化去掉sys_department表和data_scope字段,权限判断全部落到粗粒度菜单级即可。这种情况下表结构能少两张,代码量会减少四分之一左右。
另一个常见变体是简历系统与OA系统对接。企业现有组织架构在OA里,招聘系统不应该再维护一套用户体系,而是通过企业微信或钉钉扫码登录,拉取组织架构和员工信息。我建议把sys_user表扩展为对接数据(open_id、source_type),登录逻辑改成OAuth2或企业微信扫码登录,本地User表只保留业务扩展字段。这个改造涉及认证方式的替换,JWT认证的核心代码不用动,只改登录入口即可衔接。
如果做的是多租户SaaS招聘平台,就要在RBAC之上再增加一层租户隔离。需要改的核心点:所有业务表增加tenant_id字段,登录认证时候选租户,数据查询时强制拼上租户条件,缓存key也要增加租户维度。这是一套更大的工程,我这次的方案更偏单企业内网或者中小规模部署,多租户的可以在RBAC基础上继续延伸。
这里也提醒一下:源码里的密码加密方式BCrypt、请求限流、越权校验这三点,就算你不想用整套权限框架,也应该单独提出来用到其他项目里。尤其越权校验,我见过太多系统接口裸奔的案例,能在Service层统一加归属校验,能挡掉一大半安全漏洞。
最后再分享一个实际运维中的小技巧:权限系统上线前,把角色权限矩阵整理成表格并让业务方确认一遍再执行初始化脚本。我遇到过业务方说"这个总监应该有全部审批权限",但权限表里漏配了Offer审批按钮,上线后才被业务发现,灰溜溜回滚。提前把角色和权限的映射关系跟业务方对齐,比上线后反复改权限数据靠谱得多。权限管理这事的性质就是这样,设计再严密,配置错了照样出事,功劳不在设计多漂亮,而在每一步都经得住业务检验。