1. 项目背景与核心价值
做后台管理系统开发这么多年,权限管理始终是个绕不开的痛点。每次新项目都要重新实现一套RBAC(基于角色的访问控制),不仅重复造轮子,还容易埋下权限漏洞的隐患。去年接手一个需要同时管理Web端、移动端和小程序的多平台项目时,我终于决定开发这套"风汐通用管理系统"。
这个系统的核心价值在于:
- 一套代码适配所有主流技术栈(Vue/React/小程序/App)
- 可视化配置取代硬编码权限
- 支持多租户的权限隔离
- 内置审计日志和操作回溯
- 权限变更实时生效无需重启
目前已在3个中大型项目实际应用,权限配置效率提升60%以上,权限相关故障归零。下面分享这套系统的设计思路和关键技术实现。
2. 系统架构设计
2.1 整体技术栈选型
采用分层架构设计:
code复制前端层:Vue3 + TypeScript + Pinia(适配React只需替换视图层)
网关层:Spring Cloud Gateway + JWT
服务层:Spring Boot 3 + MyBatis-Plus + Redis
存储层:MySQL 8(分表)+ MongoDB(日志)
选择这套组合主要考虑:
- 前端轻量易扩展,方便不同技术栈接入
- Java生态在企业级权限管理更成熟稳定
- Redis实现权限缓存避免频繁查库
- 混合存储平衡性能与扩展性
2.2 核心数据模型设计
RBAC经典五表模型优化:
sql复制-- 角色表增加租户字段
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY,
tenant_id VARCHAR(32) NOT NULL, -- 多租户隔离
role_name VARCHAR(50) NOT NULL,
data_scope TINYINT DEFAULT 1 -- 数据权限范围(1全部 2本部门 3自定义)
);
-- 权限表采用树形结构
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY,
parent_id BIGINT DEFAULT 0,
perm_type TINYINT NOT NULL, -- 1菜单 2按钮 3API
perm_key VARCHAR(100) UNIQUE -- 前端路由/API路径
);
-- 用户-角色关联支持过期时间
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
expire_time DATETIME -- 临时权限控制
PRIMARY KEY (user_id, role_id)
);
关键改进点:
- 增加租户隔离字段
- 权限支持树形继承
- 角色可设置数据可见范围
- 用户权限支持时效控制
3. 核心功能实现
3.1 动态权限加载
通过自定义Spring Security的SecurityMetadataSource实现动态权限:
java复制public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Override
public Collection<ConfigAttribute> getAttributes(Object object) {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
// 1. 白名单直接放行
if (whiteList.contains(requestUrl)) {
return SecurityConfig.createList("permitAll");
}
// 2. 从Redis缓存获取权限规则
String cacheKey = "perm:" + requestUrl;
Set<String> requiredRoles = redisTemplate.opsForSet().members(cacheKey);
if (CollectionUtils.isEmpty(requiredRoles)) {
// 3. 缓存未命中时查数据库并刷新缓存
requiredRoles = permissionMapper.selectRolesByUrl(requestUrl);
redisTemplate.opsForSet().add(cacheKey, requiredRoles.toArray(new String[0]));
redisTemplate.expire(cacheKey, 1, TimeUnit.HOURS);
}
return SecurityConfig.createList(requiredRoles.toArray(new String[0]));
}
}
性能优化点:
- 接口权限缓存1小时
- 采用Set存储避免重复查询
- 白名单前置过滤减少IO
3.2 前端权限控制
实现Vue全局权限指令:
javascript复制// 注册v-permission指令
app.directive('permission', {
mounted(el, binding) {
const { value } = binding;
const permissions = store.getters.permissions;
if (!permissions.includes(value)) {
el.parentNode?.removeChild(el);
}
}
});
// 组件中使用
<button v-permission="'user:delete'">删除用户</button>
React版本采用高阶组件实现:
jsx复制function withPermission(WrappedComponent, permission) {
return function (props) {
const { permissions } = useStore();
if (!permissions.includes(permission)) {
return null;
}
return <WrappedComponent {...props} />;
};
}
4. 特色功能详解
4.1 权限变更实时生效
传统方案痛点:修改权限必须重新登录
我们的解决方案:
- 权限变更时发布事件:
java复制@Transactional
public void updateRolePermissions(Long roleId, List<Long> permIds) {
// 更新数据库...
applicationContext.publishEvent(
new PermissionUpdateEvent(roleId)
);
}
- 网关层监听事件刷新缓存:
java复制@EventListener
public void handlePermissionUpdate(PermissionUpdateEvent event) {
String cacheKey = "role_perm:" + event.getRoleId();
redisTemplate.delete(cacheKey);
// 通知在线用户刷新权限
websocketManager.sendMsg(
"/topic/permission-update",
new PermissionRefreshDTO(event.getRoleId())
);
}
- 前端收到WS消息后静默刷新权限:
javascript复制socket.on("/topic/permission-update", (data) => {
if (currentUser.roles.includes(data.roleId)) {
store.dispatch('refreshPermissions');
}
});
4.2 数据权限控制
通过MyBatis插件实现数据过滤:
java复制@Intercepts({
@Signature(type= Executor.class, method="query",
args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DataPermissionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取当前用户数据权限范围
DataScope scope = SecurityUtils.getDataScope();
if (scope == DataScope.ALL) {
return invocation.proceed();
}
BoundSql boundSql = ((MappedStatement)invocation.getArgs()[0])
.getBoundSql(invocation.getArgs()[1]);
// 修改SQL添加部门过滤条件
String newSql = boundSql.getSql() +
" AND dept_id IN (" + scope.getDeptIds() + ")";
resetSql(invocation, newSql);
return invocation.proceed();
}
}
支持三种数据范围:
- 全部数据(管理员)
- 本部门数据(部门主管)
- 自定义数据范围(特殊角色)
5. 部署与性能优化
5.1 缓存策略设计
采用三级缓存结构:
code复制请求 -> 本地缓存(Caffeine) -> 分布式缓存(Redis) -> 数据库
缓存更新策略:
java复制@Cacheable(value = "role_perms", key = "#roleId")
public List<String> getRolePermissions(Long roleId) {
return baseMapper.selectPermKeysByRole(roleId);
}
// 更新时清除缓存
@CacheEvict(value = "role_perms", key = "#roleId")
public void updateRolePermissions(Long roleId, List<Long> permIds) {
// 更新操作...
}
5.2 压力测试数据
使用JMeter模拟1000并发:
| 场景 | 平均响应时间 | 吞吐量 | 错误率 |
|---|---|---|---|
| 无缓存直接查库 | 320ms | 1200/s | 0.2% |
| 仅Redis缓存 | 45ms | 6800/s | 0% |
| 本地+Redis二级缓存 | 12ms | 9200/s | 0% |
6. 踩坑经验分享
6.1 权限继承的循环依赖
初期设计角色继承时遇到死循环问题:
code复制角色A继承角色B
角色B继承角色C
角色C又继承角色A → 形成循环
解决方案:
- 数据库添加
path字段记录继承链:
sql复制ALTER TABLE sys_role ADD COLUMN inherit_path VARCHAR(500);
-- 示例: /1/3/5/ 表示继承链1->3->5
- 修改时校验:
java复制public void checkInheritLoop(Long roleId, Long parentId) {
String parentPath = roleMapper.selectInheritPath(parentId);
if (parentPath.contains("/" + roleId + "/")) {
throw new BusinessException("检测到角色继承循环");
}
}
6.2 前后端权限不同步
曾出现前端隐藏按钮但接口仍可访问的问题。现采用双校验方案:
- 前端:组件级权限控制
- 后端:接口注解校验
java复制@PreAuthorize("@pms.hasPermission('user:delete')")
@DeleteMapping("/users/{id}")
public Result deleteUser(@PathVariable Long id) {
// ...
}
- 网关层:动态路由过滤
yaml复制spring:
cloud:
gateway:
routes:
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/api/**
filters:
- name: PermissionFilter
args:
excludePaths: /api/public/**,/api/auth/login
7. 扩展能力设计
7.1 多租户隔离方案
支持三种租户数据隔离级别:
- 独立数据库(金融等高安全场景)
- 共享数据库独立Schema(中型企业)
- 共享Schema通过tenant_id字段隔离(SAAS小微客户)
通过抽象TenantContext实现动态切换:
java复制public class TenantDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}
7.2 权限导入导出
支持Excel格式的权限模板:
java复制@PostMapping("/import")
public Result importPermissions(@RequestParam MultipartFile file) {
List<PermissionImportDTO> list = EasyExcel.read(file.getInputStream())
.head(PermissionImportDTO.class)
.sheet()
.doReadSync();
// 校验并批量插入
permissionService.batchImport(list);
return Result.success();
}
导出时自动生成树形结构:
java复制public void exportPermissionTree(HttpServletResponse response) {
List<Permission> all = permissionMapper.selectList(null);
List<PermissionTreeVO> tree = buildTree(all);
response.setContentType("application/vnd.ms-excel");
EasyExcel.write(response.getOutputStream())
.head(PermissionTreeVO.class)
.sheet("权限树")
.doWrite(tree);
}
这套系统经过多次迭代,目前已经形成完整的权限管理开发生态。在实际项目中落地时,建议根据具体需求进行裁剪,核心的RBAC模型和动态权限机制可以适用于大多数后台管理系统场景。