【架构实战】权限系统设计:RBAC到ABAC的演进之路

一、一次权限漏洞让我差点被开除

2020年,我们的后台管理系统出了一个权限漏洞:普通运营人员通过修改URL参数,直接访问了管理员的操作页面,把一个下架的商品重新上架了。

更恐怖的是,这个漏洞存在了半年多,直到有用户投诉"已经下架的商品怎么又出来了"我们才发现。

排查发现,后端只做了菜单级别的权限控制,没有做按钮级别的权限控制,也没有做数据级别的权限控制。运维人员只要知道API路径,就能调用任何接口。

从那以后,我们对权限系统进行了彻底重构。


二、权限模型

2.1 RBAC模型

复制代码
RBAC(Role-Based Access Control)基于角色的访问控制:

用户 → 角色 → 权限

RBAC0:基本模型
  用户-角色-权限 三层关系

RBAC1:角色继承
  管理员 → 运营 → 普通用户
  上级角色继承下级角色的权限

RBAC2:角色约束
  互斥角色:同一用户不能同时拥有
  基数约束:角色用户数量限制

RBAC3:RBAC1 + RBAC2

2.2 ABAC模型

复制代码
ABAC(Attribute-Based Access Control)基于属性的访问控制:

主体属性:用户角色、部门、职级
资源属性:资源类型、创建者、敏感级别
环境属性:时间、地点、设备
操作属性:读、写、删除

策略示例:
  当 用户.部门 == 资源.部门 
  且 用户.职级 >= 5
  且 时间.在工作时间
  则 允许 读取

三、RBAC实现

3.1 数据库设计

sql 复制代码
-- 用户表
CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    password VARCHAR(100) NOT NULL,
    status TINYINT DEFAULT 1,
    create_time DATETIME
);

-- 角色表
CREATE TABLE sys_role (
    id BIGINT PRIMARY KEY,
    role_code VARCHAR(50) NOT NULL,
    role_name VARCHAR(100) NOT NULL,
    parent_id BIGINT,  -- 父角色(角色继承)
    status TINYINT DEFAULT 1
);

-- 权限表
CREATE TABLE sys_permission (
    id BIGINT PRIMARY KEY,
    permission_code VARCHAR(100) NOT NULL,
    permission_name VARCHAR(100) NOT NULL,
    resource_type VARCHAR(20),  -- MENU/BUTTON/API
    resource_path VARCHAR(200),
    parent_id BIGINT
);

-- 用户-角色关联
CREATE TABLE sys_user_role (
    user_id BIGINT,
    role_id BIGINT,
    PRIMARY KEY (user_id, role_id)
);

-- 角色-权限关联
CREATE TABLE sys_role_permission (
    role_id BIGINT,
    permission_id BIGINT,
    PRIMARY KEY (role_id, permission_id)
);

3.2 权限加载

java 复制代码
/**
 * 权限服务
 */
@Service
@Slf4j
public class PermissionService {
    
    @Autowired
    private SysPermissionMapper permissionMapper;
    
    @Autowired
    private SysRoleMapper roleMapper;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 加载用户权限
     */
    public UserPermission loadUserPermission(Long userId) {
        // 先从缓存获取
        String cacheKey = "user:permission:" + userId;
        String cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return JSON.parseObject(cached, UserPermission.class);
        }
        
        // 查询用户的角色
        List<SysRole> roles = roleMapper.selectByUserId(userId);
        
        // 查询角色的权限(含继承的权限)
        Set<String> permissions = new HashSet<>();
        for (SysRole role : roles) {
            permissions.addAll(getRolePermissions(role.getId()));
        }
        
        UserPermission userPermission = new UserPermission();
        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
        );
        
        return userPermission;
    }
    
    /**
     * 获取角色权限(含继承)
     */
    private Set<String> getRolePermissions(Long roleId) {
        Set<String> permissions = new HashSet<>();
        
        // 当前角色的权限
        List<SysPermission> perms = permissionMapper.selectByRoleId(roleId);
        perms.forEach(p -> permissions.add(p.getPermissionCode()));
        
        // 查找父角色(角色继承)
        SysRole role = roleMapper.selectById(roleId);
        if (role.getParentId() != null) {
            permissions.addAll(getRolePermissions(role.getParentId()));
        }
        
        return permissions;
    }
}

3.3 权限拦截

java 复制代码
/**
 * 权限注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {
    String value();
    Logical logical() default Logical.AND;
}

/**
 * 权限拦截器
 */
@Aspect
@Component
@Slf4j
public class PermissionAspect {
    
    @Autowired
    private PermissionService permissionService;
    
    @Around("@annotation(requiresPermission)")
    public Object checkPermission(ProceedingJoinPoint joinPoint, 
                                   RequiresPermission requiresPermission) throws Throwable {
        
        // 获取当前用户
        Long userId = getCurrentUserId();
        String requiredPermission = requiresPermission.value();
        
        // 加载用户权限
        UserPermission userPermission = permissionService.loadUserPermission(userId);
        
        // 检查权限
        if (!userPermission.getPermissions().contains(requiredPermission)) {
            log.warn("权限不足: userId={}, required={}, has={}", 
                userId, requiredPermission, userPermission.getPermissions());
            throw new ForbiddenException("权限不足");
        }
        
        return joinPoint.proceed();
    }
}

/**
 * 使用示例
 */
@RestController
@RequestMapping("/api/admin")
public class AdminController {
    
    @RequiresPermission("user:delete")
    @DeleteMapping("/users/{id}")
    public Result deleteUser(@PathVariable Long id) {
        // 删除用户
        return Result.success();
    }
    
    @RequiresPermission("product:audit")
    @PostMapping("/products/{id}/audit")
    public Result auditProduct(@PathVariable Long id) {
        // 审核商品
        return Result.success();
    }
}

四、数据权限

4.1 数据范围控制

java 复制代码
/**
 * 数据权限注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
    /**
     * 数据范围类型
     */
    DataScopeType value() default DataScopeType.SELF;
    
    /**
     * 关联的部门字段
     */
    String deptField() default "dept_id";
    
    /**
     * 关联的创建人字段
     */
    String creatorField() default "create_by";
}

/**
 * 数据权限类型
 */
public enum DataScopeType {
    ALL,        // 全部数据
    DEPT,       // 本部门数据
    DEPT_AND_SUB, // 本部门及下级部门
    SELF,       // 仅本人数据
    CUSTOM      // 自定义
}

/**
 * 数据权限拦截器(MyBatis插件)
 */
@Intercepts({
    @Signature(type = Executor.class, method = "query", 
               args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Component
@Slf4j
public class DataScopeInterceptor implements Interceptor {
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取方法上的DataScope注解
        DataScope dataScope = getDataScopeAnnotation(invocation);
        if (dataScope == null) {
            return invocation.proceed();
        }
        
        // 获取当前用户
        Long userId = getCurrentUserId();
        UserPermission permission = permissionService.loadUserPermission(userId);
        
        // 构建数据范围SQL
        String scopeSql = buildScopeSql(dataScope, permission);
        
        // 修改SQL,追加数据范围条件
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        
        BoundSql boundSql = ms.getBoundSql(parameter);
        String originalSql = boundSql.getSql();
        String newSql = originalSql + " AND " + scopeSql;
        
        // 替换SQL
        resetSql(ms, boundSql, newSql);
        
        return invocation.proceed();
    }
    
    /**
     * 构建数据范围SQL
     */
    private String buildScopeSql(DataScope dataScope, UserPermission permission) {
        switch (dataScope.value()) {
            case ALL:
                return "1=1";
            case DEPT:
                return dataScope.deptField() + " = " + permission.getDeptId();
            case DEPT_AND_SUB:
                return dataScope.deptField() + " IN (" + getDeptAndSubIds(permission.getDeptId()) + ")";
            case SELF:
                return dataScope.creatorField() + " = " + permission.getUserId();
            default:
                return "1=0";
        }
    }
}

五、OAuth2集成

java 复制代码
/**
 * OAuth2权限集成
 */
@Configuration
@EnableResourceServer
public class OAuth2ResourceConfig extends ResourceServerConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .antMatchers("/api/user/**").authenticated()
                .anyRequest().authenticated()
            .and()
            .oauth2ResourceServer()
                .jwt();
    }
}

/**
 * JWT权限解析
 */
@Component
public class JwtPermissionConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
    
    @Override
    public Collection<GrantedAuthority> convert(Jwt jwt) {
        // 从JWT中提取权限
        @SuppressWarnings("unchecked")
        Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get("realm_access");
        
        if (realmAccess == null) {
            return Collections.emptyList();
        }
        
        @SuppressWarnings("unchecked")
        List<String> roles = (List<String>) realmAccess.get("roles");
        
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
            .collect(Collectors.toList());
    }
}

六、踩坑实录

坑1:只做了前端权限控制

前端隐藏了按钮,但后端接口没有权限校验,用户可以直接调API。

解决:前后端都要做权限控制,后端是最后一道防线。

坑2:权限缓存不及时刷新

管理员修改了用户权限,但缓存没刷新,用户还是用旧权限操作。

解决:权限变更时主动清除缓存。

坑3:角色爆炸

随着业务增长,角色越来越多,管理混乱。

解决:引入ABAC,减少角色数量,用属性控制。

坑4:数据权限遗漏

只做了功能权限,没做数据权限,用户能看到别人的数据。

解决:核心业务必须做数据权限控制。

坑5:超级管理员硬编码

到处写if(user.isAdmin()),逻辑散落各处。

解决:超级管理员也是一种角色,统一走权限体系。


七、总结

权限系统设计:

层级 方案
功能权限 RBAC
数据权限 MyBatis拦截器
接口权限 注解+拦截器
认证授权 OAuth2 + JWT

最佳实践:

  1. 前后端都要做权限控制
  2. 权限缓存及时刷新
  3. 核心业务做数据权限
  4. 统一权限模型,不要硬编码
  5. 定期审计权限配置

血的教训:

权限漏洞不是技术问题,是安全意识问题。每个接口上线前,都要问自己:谁能调?能调什么?能看到什么?

思考题: 你的系统权限控制做到了哪一级?有没有遗漏?


个人观点,仅供参考

相关推荐
小小王app小程序开发1 小时前
AI数字人小程序开发玩法深度解析:功能架构、技术实现与落地场景
人工智能·架构
nfgo2 小时前
【架构拆解】从架构师的角度去看Kubernetes系统
架构
装不满的克莱因瓶3 小时前
Servlet 到 Spring MVC 架构演进:Java Web 开发二十年技术变迁史
java·spring·servlet·架构·springmvc
拓研C3 小时前
EM-Core-Agent:AI Agent 具身认知核心系统——架构白皮书 V1.0
人工智能·架构·车载系统·机器人·github
码农阿强3 小时前
PixVerse 全系列视频生成模型技术架构详解 + Python 基于 StartAPI.top 接口实战调用
python·ai·架构·音视频·ai编程
段一凡-华北理工大学3 小时前
工业领域的Hadoop架构学习~系列文章12:Hadoop集群监控与运维
大数据·人工智能·hadoop·学习·架构·高炉炼铁·高炉炼铁智能化
狗凯之家源码网3 小时前
多语言企鹅养殖投资返利系统 自定义产品配置 一键部署源码
前端·架构·php
Wenzar_4 小时前
GeoHash+Redis Streams实时围栏系统实战
java·数据库·redis·junit
nvd114 小时前
深度解析:Apache Beam YAML 部署至 GCP Dataflow 的架构与最佳实践
架构·apache