首页/新闻资讯/正文详情

多角色业务系统权限设计与安全优化:RBAC、JWT与防越权实战

发布时间:2026/9/26 5:09:30 来源:云帆数科 栏目:资讯中心
多角色业务系统权限设计与安全优化:RBAC、JWT与防越权实战
我接手的这个企业招聘系统最初看需求并不复杂发布职位、投递简历、安排面试、录入评价标准的Spring Boot CRUD项目。但需求方跑通一轮完整流程后连问了几个问题面试官能不能只看到分配给自己的候选人HR下载简历能不能留操作日志普通员工账号把面试评分改掉了怎么追溯这几个问题全部指向同一个核心权限管理。招聘系统表面上是简历流转工具骨子里却是一个多角色、多数据域、多敏感等级的业务系统。用户角色决定了菜单和按钮的可见性数据流则精确到某条候选人记录谁能看、谁能改。再叠加SQL注入、弱口令、越权访问这些攻击面系统的安全性完全取决于你有没有把权限隔离和接口防护做到位。这篇文章不打算泛泛而谈而是把项目里真正落地、实测有效的一套权限管理机制与安全优化方案整理出来包含RBAC权限模型设计、五张核心表、Spring Security与JWT集成、防爆破与防越权等完整思路并附上关键源码片段。技术栈以Spring Boot MyBatis-Plus MySQL Redis为主招聘系统能用换成OA、教务管理、简单电商后台也能直接套。适合正在做Java课程设计案例源码、毕业设计或者刚接触企业级权限设计的朋友参考。1. 需求拆解招聘系统里的权限到底在管什么1.1 四种角色与三个权限颗粒度一个都不能少招聘系统至少要划分四种角色求职者、HR、面试官、系统管理员。求职者投递简历、查看进度、维护个人信息HR负责发布岗位、筛选简历、安排面试、发送offer面试官只能看分配给自己的候选人与面试日程填写评价和打分管理员负责账号开通、角色分配、日志审计。很多课程设计做到“求职者/HR/管理员”三张角色就草草收场把面试官合并进HR结果答辩时老师一问“面试官凭什么都换成你为什么不能只看到自己负责的人”立刻就露馅。正确的做法是围绕真实业务画角色矩阵每个角色对应一组操作集合再落到权限点。权限管理拆开来看是三个颗粒度菜单权限、按钮权限、数据权限。菜单权限决定登录后左侧导航有哪些模块按钮权限决定同一个页面上哪些操作能点击比如HR能“下载简历”面试官只能“预览摘要”数据权限最容易被忽略——面试官只能看到分配给自己的候选人记录HR能看到全部候选人管理员原则上不能直接翻看业务数据。这三个颗粒度在实际代码里对应三层校验前端路由和按钮显隐、后端接口的权限注解、Service层的数据归属判断。前端隐藏按钮只是用户体验安全边界必须由后端守住。1.2 敏感数据的安全边界把数据分级才能定权限权限设计的第一步不是建表而是先给数据分级。招聘系统里的数据可以分成三个等级公开数据是岗位JD、公司介绍登录可见半公开数据是候选人脱敏后的简历摘要比如姓名、学历、工作年限面试官可以看高敏数据是手机号、邮箱、身份证号、简历附件、面试评价访问必须严格限制。明确分级之后每个角色能接触哪个等级就一目了然面试官可以看半公开摘要但不能看手机号和简历附件HR可以看全部候选人详情和高敏字段但所有下载行为要留操作日志系统管理员只维护账号和权限配置连业务数据都不要开放查询接口。这样设计出来的权限边界才是清晰的而不是“谁登录了都能查候选人的手机号”。顺便说一句很多开发者在简历接口上只做了登录校验没做角色区分结果所有登录用户都能调取候选人手机号。这种漏洞在招聘系统里属于致命的后面第三部分会专门说怎么堵住。2. RBAC权限模型设计让权限从写死变成可配置2.1 RBAC三层映射用户、角色、权限为什么要拆开网上关于RBAC权限管理设计的教程很多核心思想就一句话用户不直接关联权限中间隔一个角色。用门禁卡来类比员工是用户门禁卡是角色能进哪些楼层对应权限分配门禁卡时不需要关心楼层的每一个门锁只需要决定把哪张卡发给谁。在招聘系统里如果用代码写死条件判断比如if (HR.equals(role))来控制所有接口新增一个“实习HR”角色时就要满项目改代码维护成本极高。RBAC把权限判断从逻辑判断变成数据比对角色对应的权限存在数据库里系统运行时从数据库加载权限集合判断当前用户是否拥有某个权限码。新增角色、调整权限分配都只是数据操作不改代码、不重新部署。这在多人协作的项目里尤其重要。我自己见过太多“毕业设计级”的项目权限判断散落在几十个Controller方法里if-else嵌套三层后来换人接手完全不敢动。RBAC的意义不是炫技而是让权限体系具备可扩展性。2.2 五张核心表与初始化数据字段都帮你定好了落地RBAC需要五张核心表用户表、角色表、权限表、用户角色关联表、角色权限关联表。用户表存基础账号信息密码字段长度直接给128位因为BCrypt哈希输出固定60个字符32位是MD5时代的习惯。角色表里建议加一个data_scope字段标记这个角色的数据范围是“全部”“本部门”还是“仅本人”虽然实现数据权限不只是这个字段但它能帮你把意图记录在配置里。权限表最常见的设计是菜单按钮树parent_id做父子层级permission_code用冒号分隔的字符串比如resume:download、interview:evaluate、job:publish。这类编码风格和Spring Security的hasAuthority天然契合也方便前端用字符串匹配来控制按钮显隐。类型字段type区分菜单1和按钮2按钮权限挂在某个菜单下面形成完整的权限树。初始化数据时要注意千万不要只给角色表插几行干巴巴的记录权限表至少要预置岗位管理、简历管理、面试管理、用户管理四个模块的菜单和按钮权限码再给管理员角色配上全部权限。否则后续测试时管理员连自己的页面都进不去还要去数据库里手补数据。2.3 一次接口请求的鉴权全程从登录到放行把RBAC落地到代码里一次接口请求的鉴权链路是固定的五步。第一步登录账号密码加验证码校验通过后生成JWT返回前端Token里只放用户ID和角色编码列表不要放密码、身份证号这类敏感信息。第二步前端每次请求在Header带Authorization: Bearer xxxx。第三步后端的JWT过滤器解析Token验签、查过期时间然后把用户信息封装成LoginUser对象塞进Spring Security的上下文。第四步请求进入Controller之前AOP切面读取方法上的权限注解比如RequirePermission(resume:download)比对当前用户是否持有这个权限码没有就抛403异常。第五步如果接口还涉及数据级权限Controller层放行后Service层还要做归属校验比如查询候选人详情前判断该候选人是否分配给了当前面试官。这套链路里最容易出错的是第四步和第五步的职责划分。有些人喜欢把所有校验都写在Controller里方法一多代码全是重复逻辑有些人则只做接口注解校验忘了Service层的数据归属水平越权就出现了。正确的做法是注解校验管垂直权限Service层管水平权限两层缺一不可。3. 安全优化最容易出事也最容易被忽视的五个入口3.1 SQL注入一个${}就可能让你整个库被拖走招聘系统的搜索场景非常多候选人搜索、岗位列表分页、筛选简历这些场景一旦处理不好就会成为SQL注入的口子。MyBatis的参数绑定需要分清#{}和${}#{}走预编译传进去的值只当字符串处理没有注入风险${}是直接拼接SQL片段一旦内容来自前端参数就有被注入的风险。最典型的错误写法是排序字段直接用前端传参比如orderBy id desc然后写进order by ${orderBy}。攻击者把参数改成id desc; drop table sys_user; --运气好一点整个表就没了。我在实际开发中处理这类需求只有一个原则所有${}的位置必须做白名单校验比如定义一个允许排序的字段枚举前端只能传枚举里的值其他一律拒绝。另一个容易被忽视的点是模糊搜索。比如like %${keyword}%这种写法看起来没什么问题实际上keyword里含一个单引号就可能让SQL报错甚至绕过条件。用concat(%, #{keyword}, %)就能安全实现模糊查询。写完XML后养成习惯全局搜一遍项目里有没有${逐个确认每个位置是不是真的需要动态SQL。3.2 密码存储与TokenBCrypt加盐与JWT过期策略密码存储的底线是绝对不能用MD5。MD5属于快速哈希一张彩虹表破解无盐MD5也就是分钟级的事即使加了固定盐一旦盐值泄露攻击者可以针对性构建彩虹表。正确做法是BCrypt它自带随机盐而且哈希计算刻意设计得很慢通常在100毫秒级别攻击者跑字典的成本会大幅上升。Spring Security自带的BCryptPasswordEncoder可以直接用加密就是encode(plainPassword)校验就是matches(rawPassword, encodedPassword)。这里有个细节BCrypt生成的字符串本身就包含盐值所以不需要单独建一列存盐同一密码每次加密结果都不同是正常现象不要以为代码写错了。会话安全方面JWT一定要设置合理的过期时间。招聘系统建议access token设30分钟到2小时refresh token设7天避免Token被盗后长时间有效。Token里不要放敏感字段因为JWT的payload只是Base64编码等于明文。退出登录时必须把旧Token加入Redis黑名单否则“退出”只是前端删掉Token服务端依然认它。分布式部署时JWT的无状态优势很明显但也需要配合Redis保存用户状态一旦账号被禁用、角色被变更旧Token要立即失效。3.3 水平越权与垂直越权简历被陌生人翻走的两种姿势越权攻击分两种。水平越权是同级用户之间的越权比如候选人A用自己登录后把请求里的ID改成候选人B的ID直接调用/api/resume/detail?id2拿到B的手机号和简历。垂直越权是低权限用户访问高权限接口比如求职者直接curl调用POST /api/job/publish尝试发布岗位后端只要没有做角色判断就会放行。很多课程设计项目只做了前端按钮隐藏觉得求职者看不到发布岗位的按钮就安全了这是完全错误的认识。攻击者根本不需要看到按钮直接抓包改请求就能绕过前端。防垂直越权的办法是接口层加权限注解每个写操作接口都校验权限码没有对应权限码的角色直接拒绝防水平越权的办法是Service层做数据归属校验查询某条数据前先确认“这条数据是不是属于当前登录用户”或者“当前用户是否被分配了这条数据”。我在项目里还加了一条额外的保险自定义一个越权审计切面凡是返回403的越权请求都记录操作日志包括请求路径、参数、登录用户ID、IP地址。配合日志平台做告警能及时发现有人在批量遍历ID这在招聘系统里往往意味着正在爬取候选人数据。3.4 登录防爆破验证码、限流、账号锁定三级联动登录接口是所有系统最容易被打的入口脚本攻击可以同时控制几千个IP对着登录接口跑字典。单靠图形验证码不够因为验证码本身也能被OCR识别。需要三级联动验证码防自动化脚本、账号级限流防撞库、IP级限流防分布式爆破。账号级限流的逻辑是Redis里用login:fail:username做计数器密码错误一次INCR一次同时设置过期时间。连续失败5次后该账号锁定10分钟锁定期间即使密码正确也拒绝登录。IP级限流用同一个思路login:fail:ip:1.2.3.4作为key同一IP 5分钟内超过30次登录尝试直接封禁1小时。实现时有一个很容易踩的坑计数器的过期时间和自增必须放在同一操作里处理。如果先INCR再EXPIRE中间出现并发请求可能造成计数不准确。正常做法是第一次INCR后检查返回值等于1时马上设置过期时间后续的INCR不会刷新过期时间到点自动清零。另外登录失败提示尽量统一为“用户名或密码错误”不要明确告诉用户“账号已锁定”或“用户名不存在”否则等于帮攻击者探测有效账号。3.5 文件上传与XSS容易被忽略的两个隐形入口简历上传是招聘系统绕不开的功能也是文件上传漏洞的重灾区。只校验扩展名远远不够攻击者可以把一个WebShell改名为resume.pdf上传只要服务器没有正确解析文件类型就能执行。至少要校验三层扩展名白名单、文件头魔数、文件大小限制。PDF文件头部必然有%PDF-字样Office文件头部是特定的ZIP标志或OLE复合文档标识用代码读前几个字节就能判断真实的文件类型。文件落到磁盘时用UUID重命名不要保留张三_简历.pdf这种原始文件名避免文件名注入路径穿越问题。文件存储目录必须放在Web应用的不可执行目录或者直接走OSS/MinIO这类独立文件服务不要把文件丢进Tomcat的webapps目录下。XSS方面面试评价功能一般会用富文本编辑器这就有问题了。攻击者在评价里嵌入一段script代码如果后端原样存储、前端原样渲染就形成了存储型XSS其他HR打开评价页面就会中招。解决办法是后端入库前用白名单过滤只允许p、strong、em、ul、li、a这类安全标签剥掉onclick、onerror、script等危险属性和标签。Java生态里Jsoup的clean方法就能做这件事记得配置白名单而不是黑名单黑名单永远有漏网之鱼。4. 核心源码实现关键代码逐段拆解可以直接抄4.1 数据库初始化脚本先把表和权限数据建好下面这段SQL把RBAC五张核心表建成最小可用版。sys_user的password字段用128位长度容纳BCryptsys_permission的permission_code用冒号分段方便和Spring Security风格对齐。CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL COMMENT 登录名, password varchar(128) NOT NULL COMMENT BCrypt哈希, real_name varchar(64) DEFAULT NULL COMMENT 姓名, mobile varchar(20) DEFAULT NULL COMMENT 手机号, status tinyint(4) DEFAULT 1 COMMENT 1启用 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE sys_role ( id bigint(20) NOT NULL AUTO_INCREMENT, role_code varchar(64) NOT NULL COMMENT 角色编码, role_name varchar(64) NOT NULL COMMENT 角色名称, data_scope tinyint(4) DEFAULT 2 COMMENT 1全部 2本人 3本部门, status tinyint(4) DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY uk_role_code (role_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色表; CREATE TABLE sys_permission ( id bigint(20) NOT NULL AUTO_INCREMENT, parent_id bigint(20) DEFAULT 0, permission_code varchar(128) NOT NULL COMMENT 如 resume:download, name varchar(64) NOT NULL, type tinyint(4) DEFAULT 1 COMMENT 1菜单 2按钮, sort_order int(11) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_permission_code (permission_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT权限表; CREATE TABLE sys_user_role ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, role_id bigint(20) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_user_role (user_id, role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户角色关联表; CREATE TABLE sys_role_permission ( id bigint(20) NOT NULL AUTO_INCREMENT, role_id bigint(20) NOT NULL, permission_id bigint(20) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_role_permission (role_id, permission_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色权限关联表;初始化角色和权限数据时我习惯把常用权限码统一写清楚候选人投递是resume:submit、HR下载简历是resume:download、面试官填写评价是interview:evaluate、HR发布岗位是job:publish。角色绑权限时管理员绑全部HR绑岗位和简历的读写权面试官只绑分配给自己的查询和评价权。这一步想清楚后面接口注解直接引用权限码就行。4.2 Spring Security过滤链与JWT工具类Spring Boot 2.7及之前版本习惯用WebSecurityConfigurerAdapterSpring Boot 3.x则推荐SecurityFilterChain的写法。这里以Spring Boot 3.x为例给出配置核心是把/api/auth/login和OPTIONS预检请求放行其余请求全部走认证过滤器。Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .cors().and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests(auth - auth .antMatchers(/api/auth/login, /api/auth/captcha, /error).permitAll() .antMatchers(HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated() ) .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }JWT工具类的核心就两个方法生成Token和解析Token。生成时只放userId和roles两个字段过期时间按access token 30分钟设置。解析时统一捕获签名异常和过期异常不要把异常细节直接抛给前端。public class JwtUtil { private static final SecretKey KEY Keys.hmacShaKeyFor( your-secret-key-here-change-me-to-a-long-random-string.getBytes()); private static final long EXPIRE_MS 30 * 60 * 1000L; public static String createToken(Long userId, ListString roles) { Date now new Date(); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(roles, roles) .setIssuedAt(now) .setExpiration(new Date(now.getTime() EXPIRE_MS)) .signWith(KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder().setSigningKey(KEY).build() .parseClaimsJws(token).getBody(); } }记得JWT的签名密钥一定要放在配置文件里用环境变量注入不要硬编码写死在类里。密钥长度至少32字节太短会直接启动报错。4.3 基于注解的权限校验一个切面搞定按钮级控制先定义一个权限注解放在方法上就能声明接口所需的权限码。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequirePermission { String value(); }再写一个AOP切面在方法执行前取出当前登录用户的权限集合检查是否包含注解里声明的权限码。这个切面是垂直越权的第一道闸门。Aspect Component public class PermissionAspect { Around(annotation(requirePermission)) public Object checkPermission(ProceedingJoinPoint pjp, RequirePermission requirePermission) throws Throwable { LoginUser loginUser SecurityUtils.getCurrentUser(); if (loginUser null) { throw new BusinessException(401, 未登录); } // 管理员可以直接放行 if (loginUser.isAdmin()) { return pjp.proceed(); } if (!loginUser.getPermissions().contains(requirePermission.value())) { throw new BusinessException(403, 无权限执行此操作); } return pjp.proceed(); } }Controller里用起来就非常简洁接口和权限码一一对应一眼就能看出哪个接口需要什么权限PostMapping(/job/publish) RequirePermission(job:publish) public ResultVoid publishJob(RequestBody JobPublishRequest req) { jobService.publish(req); return Result.success(); }权限集合在登录成功后加载到LoginUser对象里并缓存到Redis和当前请求上下文。注意权限集合不要频繁查数据库否则每个接口请求多几次数据库查询压力全在DB上。4.4 登录接口与防爆破的完整实现登录接口的完整逻辑是验证验证码、查账号、校验BCrypt密码、处理失败计数、签发Token。下面这段核心代码把账号级防爆破写进去了Redis的key和过期时间都要精心设计。Service public class LoginService { Autowired private UserMapper userMapper; Autowired private RedisUtil redisUtil; public LoginResult login(LoginRequest req) { // 1. 校验验证码 String captchaKey captcha: req.getCaptchaId(); Object captcha redisUtil.get(captchaKey); if (captcha null || !captcha.toString().equalsIgnoreCase(req.getCaptcha())) { throw new BusinessException(验证码错误或已过期); } redisUtil.del(captchaKey); // 2. 账号级防爆破 String failKey login:fail:user: req.getUsername(); Integer failCount (Integer) redisUtil.get(failKey); if (failCount ! null failCount 5) { throw new BusinessException(操作过于频繁请10分钟后再试); } // 3. 校验账号密码 User user userMapper.findByUsername(req.getUsername().trim()); if (user null || user.getStatus() ! 1 || !BCrypt.checkpw(req.getPassword(), user.getPassword())) { Long count redisUtil.increment(failKey); if (count 1L) { redisUtil.expire(failKey, 10 * 60); } throw new BusinessException(用户名或密码错误); } // 4. 登录成功清掉失败计数 redisUtil.del(failKey); // 5. 加载权限集合签发令牌 ListString roles roleMapper.selectCodesByUserId(user.getId()); ListString permissions permissionMapper.selectCodesByUserId(user.getId()); String token JwtUtil.createToken(user.getId(), roles); redisUtil.set(user:permission: user.getId(), permissions, 30 * 60); LoginUser loginUser new LoginUser(user.getId(), user.getUsername(), roles, permissions); SecurityUtils.setCurrentUser(loginUser); return new LoginResult(token, user.getRealName()); } }这段逻辑里最关键的细节是第三步的count 1L判断。INCR第一次执行后Redis里没有过期时间必须等返回值是1的时候设置EXPIRE否则每次登录失败都刷新过期时间锁定窗口会被无限拉长。密码校验放在用户为null判断的同一分支里这样攻击者无法区分“用户名不存在”和“密码错误”有效减少账号枚举风险。IP级限流的思路和账号级一样key改成login:fail:ip:请求IP5分钟内超过30次直接拒绝1小时。注意如果走了Nginx层要通过X-Forwarded-For取真实IP避免所有请求都解析到Nginx的地址。4.5 数据级越权校验面试官只能看他该看的人接口层权限注解管住了“谁能调这个接口”但管不住“谁的数据”。比如面试官有resume:detail权限但他能不能看某个具体候选人的简历取决于这个候选人是否分配给他。下面这段Service层代码做了数据归属校验Service public class ResumeService { Autowired private InterviewAssignMapper assignMapper; public ResumeVO getResumeForInterviewer(Long candidateId) { LoginUser user SecurityUtils.getCurrentUser(); // 管理员或HR跳过数据归属校验 if (user.isAdmin() || user.getRoles().contains(HR)) { return buildResume(candidateId, false); } // 面试官必须校验候选人是否分配给自己 LambdaQueryWrapperInterviewAssign wrapper new LambdaQueryWrapper(); wrapper.eq(InterviewAssign::getInterviewerId, user.getId()) .eq(InterviewAssign::getCandidateId, candidateId); if (assignMapper.selectCount(wrapper) 0) { throw new BusinessException(403, 无权查看该候选人的简历); } // 面试官只能看脱敏版 return buildResume(candidateId, true); } private ResumeVO buildResume(Long candidateId, boolean masked) { ResumeVO resume resumeMapper.selectDetail(candidateId); if (masked) { resume.setMobile(maskPhone(resume.getMobile())); resume.setEmail(maskEmail(resume.getEmail())); } return resume; } }这里的数据归属校验不能依赖前端传的面试官ID必须从SecurityUtils里拿当前登录用户否则任何人都可以伪造面试官ID绕过校验。脱敏逻辑统一写在Service层不要在多个Controller里各写一份避免某个接口漏了脱敏就把手机号暴露出来了。5. 踩坑实录与排查经验这些问题我替你趟过一遍5.1 接口一直403先查权限注解还是过滤器顺序项目联调时最常遇到的情况就是接口返回403URL没问题、权限码疑似配了但请求死活进不去。第一件事先看JWT过滤器有没有正确解析Token。过滤器如果放在Spring Security的Filter链最前面异常时还没有进入SecurityContext权限切面自然拿不到用户信息就会误判为未授权。排查思路很固定先看是否命中permitAll路径再看过滤器是否把Token解析成功最后看权限切面拿到的权限集合里有没有注解声明的权限码。大多数403其实是权限码没绑定到角色上或者Redis缓存了旧权限。用Postman打接口时可以在返回体的日志里打印当前用户ID、角色、权限集合一目了然。5.2 OPTIONS预检跨域被拦截前后端分离第一坑前后端分离部署后前端调用后端接口经常出现白屏浏览器控制台提示“CORS error”。问题出在Spring Security默认拦截所有请求而浏览器跨域请求会先发一个不带Token的OPTIONS预检请求直接被401拦掉了。解决方法是两步CORS配置里不要用allowedOrigins(*)因为带Cookie请求时*域名会直接报错要用allowedOriginPatterns(*)同时Spring Security放行所有OPTIONS请求。配置完成后先用一个最简单的GET接口验证跨域通不通再继续后面的权限调试不要混在一起排查。5.3 登录失败计数不生效Redis INCR并发与过期细节登录防爆破上线后测试反馈连续输错10次密码账号也没被锁。排查发现计数器代码写成了先INCR再set expire但因为多线程并发EXPIRE把前面设置的过期时间覆盖掉了。另一个问题是在Redis集群模式下INCR和EXPIRE是两个独立命令不具备原子性一旦Redis主从切换可能丢数据。稳妥的方案是用Lua脚本把INCR和EXPIRE写到同一个脚本里执行保证原子性。单机开发环境下用我之前写的方式没问题生产环境一定用Lua。5.4 改完角色权限不生效版本号比清缓存更省心权限集合缓存到Redis之后管理员在后台给角色新增了一个权限码结果用户刷新页面后仍然提示无权限因为Redis里还是旧权限集合。最简单的处理是在登录时给权限集合key加一个版本号user:permission:{userId}:{version}version放在Redis里角色权限变更时version1用户下一次请求发现版本号不一致就重新加载权限。这样比主动删缓存更可靠因为删缓存存在删除失败或并发重建的竞态问题。5.5 常见问题速查表现象可能原因排查与解决接口一直403权限码未绑定角色、JWT未解析、权限切面误判打印用户权限集合比对注解权限码前端跨域报错OPTIONS被拦截、CORS配置不允许带Cookie放行OPTIONS用allowedOriginPatterns登录失败计数不生效INCR与EXPIRE非原子、Redis重启丢失用Lua脚本保证原子性配置AOF持久化改权限后不生效Redis缓存了旧权限集合key加版本号变更时version1求职者能访问HR接口接口未加权限注解前端隐藏按钮被绕过所有写接口加RequirePermission候选人A看到候选人B简历查询接口缺少数据归属校验Service层校验当前用户与数据归属上传的PHP文件执行了扩展名白名单被绕过、文件在可执行目录校验文件头魔数重命名存储目录禁止执行脚本富文本评价弹脚本存储型XSS原样保存HTML入库前Jsoup白名单过滤展示时前端转义这套方案做完之后我自己最大的体会是权限管理和安全加固不是上线前临时补的功能而是从系统骨架搭建第一天就要设计进去的约束。招聘系统表面上是管简历实际上管的是“谁能对简历做什么”这件事边界定义清楚了系统哪怕只有一两万行代码也站得稳。如果你正在写类似的项目建议先把角色和数据权限的矩阵表画出来再动手建表写接口绝对比边写边补靠谱得多。一个小细节分享给你在本地开发时把权限切面里的日志打开全量打印联调期的403问题基本都能在十分钟内定位。

相关推荐

教材源码运行指南:HTML+CSS+JavaScript项目实战与避坑
教材源码运行指南:HTML+CSS+JavaScript项目实战与避坑

简介:这份源代码压缩包对应《网页设计与制作项目教程(HTMLCSSJavaScript)》,面向网页设计初学者与前端入门学习者,帮助读者通过教材实例与练习掌握HTML、CSS、JavaScript三大核心技术,解决从静态页面结构搭… · 2026/9/26 5:09:30

小米直播训练大模型:每小时20万成本背后的RL工程实践
小米直播训练大模型:每小时20万成本背后的RL工程实践

1. 一场每小时烧掉20万的RL实验到底在做什么第一次看到“小米直播训练大模型,每小时烧掉20万”这个说法,我第一反应不是震惊,而是好奇这钱到底花在哪了。因为在大模型训练这个圈子里,烧钱本身不稀奇,稀奇的是“直播训练… · 2026/9/26 5:09:30

Android架构演进:从MVVM到MVI的实战解析与状态管理之道
Android架构演进:从MVVM到MVI的实战解析与状态管理之道

我们做Android开发的朋友,这几年听到MVI的频率应该不低。我最早接触这个缩写时,第一反应是“怎么又是新轮子”,毕竟从MVC到MVP再到MVVM,光是架构模式的争论就能养活半个技术社区。但真正在几个中大型项目里落地MVI并跑完整个迭代周… · 2026/9/26 5:09:30

5G应急物资配送问题建模与求解:融合通信约束的VRPTW
5G应急物资配送问题建模与求解:融合通信约束的VRPTW

简介:这是一份针对2022年电工杯数学建模竞赛B题的完整参赛方案,围绕5G网络环境下应急物资配送问题展开,适合准备电工杯、国赛等数学建模竞赛的本科生和研究生参考。方案从配送车辆单独配送的VRP模型出发,逐步引入无人机协同配送、… · 2026/9/26 5:47:59

Windows C盘爆满深层清理四步法:安全释放30GB+空间
Windows C盘爆满深层清理四步法:安全释放30GB+空间

1. 为什么C盘爆满不是“删文件”就能解决的问题C盘爆满,是Windows用户最熟悉又最头疼的日常现象。你点开资源管理器,看到那个刺眼的红色进度条,右下角弹出“低磁盘空间”的黄色警告,打开“此电脑”发现C盘只剩不到5GB——这时候第… · 2026/9/26 5:47:59

64天打卡系统复盘:用一张表养成早起、阅读、运动、日更四件事
64天打卡系统复盘:用一张表养成早起、阅读、运动、日更四件事

1. 开头:3.1不是日期,是我给自己设的节点3月1日这天早上六点二十分,我在打卡表上画下了第64个完整的勾。从今年年初决定不再“靠脑子记习惯”开始,每天一张表、一支笔、一个具体的动作,坚持到现在已经过了两个月。很多… · 2026/9/26 5:47:53

MiniMax H3 Semantic Bridge 本地部署:从多镜头一致性到连贯成片
MiniMax H3 Semantic Bridge 本地部署:从多镜头一致性到连贯成片

Seedance 2.5 那一波"连贯成片"的演示,确实让本地视频生成圈的人心里痒了一下。以往我们自己在本地跑的模型,单镜头做得再惊艳,一旦跨镜头、跨场景,角色就跟临时换了个演员一样。MiniMax H3 放出来之后,配套… · 2026/9/26 5:47:53

MiniMax H3本地部署实战:从ComfyUI到导演台工作流全解析
MiniMax H3本地部署实战:从ComfyUI到导演台工作流全解析

"MiniMax H3"这段时间在AI视频圈子里热度确实高,我周围不少做短视频、做动画预演的朋友都已经从其他模型切过来了。我自己也在本地跑了一段时间,从最早用H1、S2那批开源模型,到现在H3配合导演台流程,最大的感受是&#… · 2026/9/26 5:47:53

一个IDE搞定数据库、SSH和Docker:告别工具切换的完整方案
一个IDE搞定数据库、SSH和Docker:告别工具切换的完整方案

告别切换!一个工具搞定数据库、SSH和Docker管理做后端这几年,我每天在 Navicat、Xshell、FinalShell、Docker Desktop 之间来回切换,光连接配置就存了十几个,有时候为了查一条数据要经历“打开数据库客户端 → 发现服务没起 → 切… · 2026/9/26 5:47:53

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码