SaToken 结合 RBAC 实现接口级权限隔离最佳实践

621 阅读6分钟

一、背景与核心概念

在微服务架构中,接口级权限控制是保障系统安全的重要环节。基于 RBAC(Role-Based Access Control)模型,结合轻量级权限认证框架 SaToken,可实现细粒度的接口权限控制。核心设计思路如下:

  • 角色权限解耦:通过用户-角色-权限三级结构实现权限分配
  • 动态鉴权:基于接口绑定权限标识实现动态验证
  • 最小权限原则:通过白名单与细粒度注解控制访问边界

二、数据库设计关键点

1. 核心表结构

# 用户表
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_email` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱',
  `user_phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号',
  `user_password` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '密码',
  `user_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户昵称',
  `user_avatar` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户头像',
  `user_profile` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户简介',
  `user_card` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '身份证号',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted_flag` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户' ROW_FORMAT = Dynamic;

# 菜单表
DROP TABLE IF EXISTS `tb_menu`;
CREATE TABLE `tb_menu`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单id',
  `menu_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单名称',
  `menu_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单类型:目录,菜单',
  `parent_id` bigint NULL DEFAULT NULL COMMENT '父菜单id',
  `order_num` int NULL DEFAULT NULL COMMENT '显示顺序',
  `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '路由地址',
  `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组件路径',
  `perm_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限标识键',
  `disabled_flag` tinyint(1) NULL DEFAULT 0 COMMENT '禁用状态',
  `deleted_flag` tinyint(1) NOT NULL DEFAULT 0 COMMENT '删除状态',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单表' ROW_FORMAT = DYNAMIC;

# 角色和菜单关联表
DROP TABLE IF EXISTS `tb_role_menu`;
CREATE TABLE `tb_role_menu`  (
  `role_id` bigint NOT NULL COMMENT '角色id',
  `menu_id` bigint NOT NULL COMMENT '菜单'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = Dynamic;

# 角色表
DROP TABLE IF EXISTS `tb_role`;
CREATE TABLE `tb_role`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色id',
  `role_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色名称',
  `role_key` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色权限字符串',
  `role_status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色状态(0、正常;1、禁用)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;

# 用户和角色关联表
DROP TABLE IF EXISTS `tb_user_role`;
CREATE TABLE `tb_user_role`  (
  `user_id` bigint NOT NULL COMMENT '用户id',
  `role_id` bigint NOT NULL COMMENT '角色id'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户和角色关联表' ROW_FORMAT = Dynamic;

2. 权限标识规范

  • 采用 资源:操作 格式(如:user:add
  • 支持通配符(如:user:* 表示用户所有操作)

三、SaToken 集成实现

1. 依赖配置

<!-- Sa-Token 核心包 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot3-starter</artifactId>
    <version>1.37.0</version>
</dependency>

<!-- Redis 集成(推荐生产环境使用) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis</artifactId>
    <version>1.37.0</version>
</dependency>

2. 权限加载实现

@Component
public class StpInterfaceImpl implements StpInterface {

    @Resource
    private AuthService authService;

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        return authService.loadPermissions((Long)loginId);
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return authService.loadRoles((Long)loginId);
    }
}
/**
 * 自定义权限接口实现类 获取用户权限和角色
 */
@Component
public class StpInterfaceImpl implements StpInterface {

    @Resource
    private AuthService authService;

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        return authService.getPermissionList(loginId);
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return authService.getRoleList(loginId);
    }
}

这是可以看到所有的权限标识 image.png 权限服务实现要点

  • 使用 @Cacheable 注解缓存权限数据
  • 角色变更时需清理缓存
  • 建议使用批量查询优化性能

四、权限拦截配置

1. 全局拦截器配置

@Configuration
public class SaTokenConfig implements WebMvcConfigurer {

    private final AntPathMatcher pathMatcher = new AntPathMatcher();
    
    // 动态白名单(可从配置中心加载)
    private static final List<String> WHITE_LIST = Arrays.asList(
        "/public/**", 
        "/swagger/**",
        "/v3/api-docs/**"
    );

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SaInterceptor(handler -> {
            // 动态鉴权逻辑
            SaRouter
                .match("/**") // 拦截所有路径
                .notMatch(WHITE_LIST) // 排除白名单
                .check(r -> {
                    // 登录检查
                    StpUtil.checkLogin();
                    
                    // 权限检查(通过注解更灵活)
                });
        })).addPathPatterns("/**");
    }
}

优化建议:这里直接放行了路径第二个是 public的接口

2. 注解式权限控制

// 角色校验
@SaCheckRole("admin")
@PostMapping("/users")
public Result<?> createUser() { ... }

// 权限校验
@SaCheckPermission("user:delete")
@DeleteMapping("/users/{id}")
public Result<?> deleteUser() { ... }

// 复合校验(满足任意条件)
@SaCheckOr(
    permission = "log:export", 
    role = "audit_admin"
)
@GetMapping("/logs/export")
public void exportLogs() { ... }

如果没有这些标识符,那么将无法访问这个接口

五、动态权限管理方案

1. 权限更新策略

  • 实时性要求高:通过 Redis Pub/Sub 通知节点更新缓存
  • 一般场景:设置合理的缓存过期时间(建议 5-10 分钟)
  • 用户级更新:修改权限后调用 StpUtil.logoutByLoginId(userId)

2. 管理接口示例

@RestController
@RequestMapping("/system/auth")
public class AuthController {

    @PostMapping("/role/bind")
    @SaCheckPermission("system:role:edit")
    public Result<?> bindRolePermission(
        @RequestParam String roleKey,
        @RequestBody List<String> permKeys) {
        // 实现角色-权限绑定逻辑
        return Result.success();
    }

    @GetMapping("/user/perms")
    @SaCheckLogin
    public Result<List<String>> getCurrentUserPerms() {
        return Result.success(StpUtil.getPermissionList());
    }
}

六、最佳实践建议

  1. 权限粒度控制

    • 接口级别:使用 @SaCheckPermission
    • 模块级别:使用 @SaCheckRole
    • 开放接口:通过白名单配置
  2. 性能优化

    • 使用二级缓存(Redis + 本地缓存)
    • 批量查询用户权限
    • 启用 SaToken 的注解缓存
  3. 安全增强

    sa-token:
      token-style: random-64  # 使用随机token
      jwt-secret-key: ${SA_JWT_SECRET} # 从环境变量读取
      token-prefix: "Bearer " # 符合OAuth2规范
    
  4. 前端配合

    • 动态路由:根据权限列表过滤前端路由
    • 按钮级控制:通过 v-if="hasPerm('user:add')" 实现

七、常见问题解决方案

Q1 新权限未及时生效?

  • 检查缓存策略
  • 调用 StpUtil.getPermissionList(userId, true) 强制刷新

Q2 超级管理员权限处理?

@SaCheckPermission(
    value = {"user:delete", "admin:full"}, 
    mode = SaMode.OR
)

Q3 接口未配置鉴权?

  • 开启开发环境鉴权检查
  • 使用 AOP 扫描所有接口并告警
@Aspect
@Component
public class AuthCheckAspect {
    @Around("@within(org.springframework.web.bind.annotation.RestController)")
    public Object checkAuthAnnotation(ProceedingJoinPoint joinPoint) {
        Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
        if (!hasAuthAnnotation(method)) {
            log.warn("接口未配置权限注解: {}", method.toString());
        }
        return joinPoint.proceed();
    }
}

八、总结

通过 SaToken 与 RBAC 模型的深度整合,我们实现了:

  1. 灵活的角色权限管理:通过可视化界面动态配置权限
  2. 细粒度的接口控制:精确到单个接口的操作权限
  3. 高性能的鉴权方案:多级缓存保证系统吞吐量
  4. 安全的访问控制:多重校验机制保障系统安全

实现之后也可以轻易的给用户进行权限的分配,直接给角色勾选可以访问的接口即可 也可以设置好角色,给用户分配角色

image.png