
一、一次权限漏洞让我差点被开除2020年我们的后台管理系统出了一个权限漏洞普通运营人员通过修改URL参数直接访问了管理员的操作页面把一个下架的商品重新上架了。更恐怖的是这个漏洞存在了半年多直到有用户投诉已经下架的商品怎么又出来了我们才发现。排查发现后端只做了菜单级别的权限控制没有做按钮级别的权限控制也没有做数据级别的权限控制。运维人员只要知道API路径就能调用任何接口。从那以后我们对权限系统进行了彻底重构。二、权限模型2.1 RBAC模型RBACRole-Based Access Control基于角色的访问控制 用户 → 角色 → 权限 RBAC0基本模型 用户-角色-权限 三层关系 RBAC1角色继承 管理员 → 运营 → 普通用户 上级角色继承下级角色的权限 RBAC2角色约束 互斥角色同一用户不能同时拥有 基数约束角色用户数量限制 RBAC3RBAC1 RBAC22.2 ABAC模型ABACAttribute-Based Access Control基于属性的访问控制 主体属性用户角色、部门、职级 资源属性资源类型、创建者、敏感级别 环境属性时间、地点、设备 操作属性读、写、删除 策略示例 当 用户.部门 资源.部门 且 用户.职级 5 且 时间.在工作时间 则 允许 读取三、RBAC实现3.1 数据库设计-- 用户表CREATETABLEsys_user(idBIGINTPRIMARYKEY,usernameVARCHAR(50)NOTNULL,passwordVARCHAR(100)NOTNULL,statusTINYINTDEFAULT1,create_timeDATETIME);-- 角色表CREATETABLEsys_role(idBIGINTPRIMARYKEY,role_codeVARCHAR(50)NOTNULL,role_nameVARCHAR(100)NOTNULL,parent_idBIGINT,-- 父角色角色继承statusTINYINTDEFAULT1);-- 权限表CREATETABLEsys_permission(idBIGINTPRIMARYKEY,permission_codeVARCHAR(100)NOTNULL,permission_nameVARCHAR(100)NOTNULL,resource_typeVARCHAR(20),-- MENU/BUTTON/APIresource_pathVARCHAR(200),parent_idBIGINT);-- 用户-角色关联CREATETABLEsys_user_role(user_idBIGINT,role_idBIGINT,PRIMARYKEY(user_id,role_id));-- 角色-权限关联CREATETABLEsys_role_permission(role_idBIGINT,permission_idBIGINT,PRIMARYKEY(role_id,permission_id));3.2 权限加载/** * 权限服务 */ServiceSlf4jpublicclassPermissionService{AutowiredprivateSysPermissionMapperpermissionMapper;AutowiredprivateSysRoleMapperroleMapper;AutowiredprivateStringRedisTemplateredisTemplate;/** * 加载用户权限 */publicUserPermissionloadUserPermission(LonguserId){// 先从缓存获取StringcacheKeyuser:permission:userId;StringcachedredisTemplate.opsForValue().get(cacheKey);if(cached!null){returnJSON.parseObject(cached,UserPermission.class);}// 查询用户的角色ListSysRolerolesroleMapper.selectByUserId(userId);// 查询角色的权限含继承的权限SetStringpermissionsnewHashSet();for(SysRolerole:roles){permissions.addAll(getRolePermissions(role.getId()));}UserPermissionuserPermissionnewUserPermission();userPermission.setUserId(userId);userPermission.setRoles(roles.stream().map(SysRole::getRoleCode).collect(Collectors.toSet()));userPermission.setPermissions(permissions);// 缓存redisTemplate.opsForValue().set(cacheKey,JSON.toJSONString(userPermission),30,TimeUnit.MINUTES);returnuserPermission;}/** * 获取角色权限含继承 */privateSetStringgetRolePermissions(LongroleId){SetStringpermissionsnewHashSet();// 当前角色的权限ListSysPermissionpermspermissionMapper.selectByRoleId(roleId);perms.forEach(p-permissions.add(p.getPermissionCode()));// 查找父角色角色继承SysRoleroleroleMapper.selectById(roleId);if(role.getParentId()!null){permissions.addAll(getRolePermissions(role.getParentId()));}returnpermissions;}}3.3 权限拦截/** * 权限注解 */Target({ElementType.METHOD,ElementType.TYPE})Retention(RetentionPolicy.RUNTIME)publicinterfaceRequiresPermission{Stringvalue();Logicallogical()defaultLogical.AND;}/** * 权限拦截器 */AspectComponentSlf4jpublicclassPermissionAspect{AutowiredprivatePermissionServicepermissionService;Around(annotation(requiresPermission))publicObjectcheckPermission(ProceedingJoinPointjoinPoint,RequiresPermissionrequiresPermission)throwsThrowable{// 获取当前用户LonguserIdgetCurrentUserId();StringrequiredPermissionrequiresPermission.value();// 加载用户权限UserPermissionuserPermissionpermissionService.loadUserPermission(userId);// 检查权限if(!userPermission.getPermissions().contains(requiredPermission)){log.warn(权限不足: userId{}, required{}, has{},userId,requiredPermission,userPermission.getPermissions());thrownewForbiddenException(权限不足);}returnjoinPoint.proceed();}}/** * 使用示例 */RestControllerRequestMapping(/api/admin)publicclassAdminController{RequiresPermission(user:delete)DeleteMapping(/users/{id})publicResultdeleteUser(PathVariableLongid){// 删除用户returnResult.success();}RequiresPermission(product:audit)PostMapping(/products/{id}/audit)publicResultauditProduct(PathVariableLongid){// 审核商品returnResult.success();}}四、数据权限4.1 数据范围控制/** * 数据权限注解 */Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)publicinterfaceDataScope{/** * 数据范围类型 */DataScopeTypevalue()defaultDataScopeType.SELF;/** * 关联的部门字段 */StringdeptField()defaultdept_id;/** * 关联的创建人字段 */StringcreatorField()defaultcreate_by;}/** * 数据权限类型 */publicenumDataScopeType{ALL,// 全部数据DEPT,// 本部门数据DEPT_AND_SUB,// 本部门及下级部门SELF,// 仅本人数据CUSTOM// 自定义}/** * 数据权限拦截器MyBatis插件 */Intercepts({Signature(typeExecutor.class,methodquery,args{MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})})ComponentSlf4jpublicclassDataScopeInterceptorimplementsInterceptor{OverridepublicObjectintercept(Invocationinvocation)throwsThrowable{// 获取方法上的DataScope注解DataScopedataScopegetDataScopeAnnotation(invocation);if(dataScopenull){returninvocation.proceed();}// 获取当前用户LonguserIdgetCurrentUserId();UserPermissionpermissionpermissionService.loadUserPermission(userId);// 构建数据范围SQLStringscopeSqlbuildScopeSql(dataScope,permission);// 修改SQL追加数据范围条件Object[]argsinvocation.getArgs();MappedStatementms(MappedStatement)args[0];Objectparameterargs[1];BoundSqlboundSqlms.getBoundSql(parameter);StringoriginalSqlboundSql.getSql();StringnewSqloriginalSql AND scopeSql;// 替换SQLresetSql(ms,boundSql,newSql);returninvocation.proceed();}/** * 构建数据范围SQL */privateStringbuildScopeSql(DataScopedataScope,UserPermissionpermission){switch(dataScope.value()){caseALL:return11;caseDEPT:returndataScope.deptField() permission.getDeptId();caseDEPT_AND_SUB:returndataScope.deptField() IN (getDeptAndSubIds(permission.getDeptId()));caseSELF:returndataScope.creatorField() permission.getUserId();default:return10;}}}五、OAuth2集成/** * OAuth2权限集成 */ConfigurationEnableResourceServerpublicclassOAuth2ResourceConfigextendsResourceServerConfigurerAdapter{Overridepublicvoidconfigure(HttpSecurityhttp)throwsException{http.authorizeRequests().antMatchers(/api/public/**).permitAll().antMatchers(/api/admin/**).hasRole(ADMIN).antMatchers(/api/user/**).authenticated().anyRequest().authenticated().and().oauth2ResourceServer().jwt();}}/** * JWT权限解析 */ComponentpublicclassJwtPermissionConverterimplementsConverterJwt,CollectionGrantedAuthority{OverridepublicCollectionGrantedAuthorityconvert(Jwtjwt){// 从JWT中提取权限SuppressWarnings(unchecked)MapString,ObjectrealmAccess(MapString,Object)jwt.getClaims().get(realm_access);if(realmAccessnull){returnCollections.emptyList();}SuppressWarnings(unchecked)ListStringroles(ListString)realmAccess.get(roles);returnroles.stream().map(role-newSimpleGrantedAuthority(ROLE_role)).collect(Collectors.toList());}}六、踩坑实录坑1只做了前端权限控制前端隐藏了按钮但后端接口没有权限校验用户可以直接调API。解决前后端都要做权限控制后端是最后一道防线。坑2权限缓存不及时刷新管理员修改了用户权限但缓存没刷新用户还是用旧权限操作。解决权限变更时主动清除缓存。坑3角色爆炸随着业务增长角色越来越多管理混乱。解决引入ABAC减少角色数量用属性控制。坑4数据权限遗漏只做了功能权限没做数据权限用户能看到别人的数据。解决核心业务必须做数据权限控制。坑5超级管理员硬编码到处写if(user.isAdmin())逻辑散落各处。解决超级管理员也是一种角色统一走权限体系。七、总结权限系统设计层级方案功能权限RBAC数据权限MyBatis拦截器接口权限注解拦截器认证授权OAuth2 JWT最佳实践前后端都要做权限控制权限缓存及时刷新核心业务做数据权限统一权限模型不要硬编码定期审计权限配置血的教训权限漏洞不是技术问题是安全意识问题。每个接口上线前都要问自己谁能调能调什么能看到什么思考题你的系统权限控制做到了哪一级有没有遗漏个人观点仅供参考