设计一个权限系统:RBAC、ABAC模型与Spring Security的整合实现
设计一个权限系统:RBAC、ABAC模型与Spring Security的整合实现
适读人群:Java中高级工程师、需要做权限系统的技术负责人 | 阅读时长:约20分钟 | 难度:★★★☆☆
开篇故事
我做过一个多部门协同的内部管理系统,权限需求来自各个部门:财务说"我只能看本部门的账单",运营说"我能看所有数据但不能改",总监说"我要能看所有部门的数据,但看不了薪资",IT管理员说"我能改所有配置但看不了业务数据"。
最开始用的纯RBAC:给每种组合创建一个角色。结果创建了200多个角色,维护的时候一个需求变更要修改几十个角色,一周后没有人搞得清楚角色和权限的对应关系了。
后来我把这套系统重新设计成RBAC+ABAC混合模型,用了大概两周,整个权限体系变得清晰而灵活。这篇文章把权限系统的设计思路和Spring Security的整合实现完整讲一遍。
一、需求分析与规模估算
权限系统的核心概念
- 认证(Authentication): 你是谁?(登录验证身份)
- 授权(Authorization): 你能做什么?(权限控制)
- RBAC: Role-Based Access Control,基于角色的访问控制
- ABAC: Attribute-Based Access Control,基于属性的访问控制
规模估算
以一个中型企业内部系统为例:
用户规模: 1万用户,500个部门,100个角色,1000个权限点
QPS: 每秒1000次接口调用,每次都要做权限校验
权限校验延迟要求: < 5ms(不能显著拖慢接口响应)
存储估算:
- 用户-角色关联:1万用户 × 平均3个角色 = 3万条
- 角色-权限关联:100角色 × 平均20个权限 = 2000条
- 数据量极小,完全可以缓存在内存中
二、RBAC vs ABAC 核心区别
RBAC(基于角色)
权限绑定到角色,用户通过角色获得权限。
用户A → 角色"财务主管" → 权限"查看账单"、"审批报销"优点: 简单直观,管理成本低
缺点: 粒度粗,无法处理"张三只能看自己部门数据"这种数据级别的权限控制
ABAC(基于属性)
权限判断基于多个属性(主体属性、资源属性、环境属性)的组合策略。
策略:如果 用户.部门 == 资源.部门 AND 用户.级别 >= 3 AND 时间 IN 工作时间,则允许查看优点: 非常灵活,可以处理复杂场景
缺点: 策略复杂,难以理解和调试
推荐方案:RBAC + 数据权限(混合模型)
- 功能权限(菜单、按钮、API):用RBAC管理,简单清晰
- 数据权限(能看哪些数据行):用数据范围注解或ABAC策略补充
三、系统架构设计
四、数据模型设计
-- 用户表
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
password VARCHAR(128) NOT NULL,
dept_id BIGINT, -- 所属部门
status TINYINT DEFAULT 1,
create_time DATETIME
);
-- 角色表
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY,
role_name VARCHAR(64) NOT NULL,
role_code VARCHAR(64) UNIQUE NOT NULL, -- 如 ROLE_ADMIN
data_scope TINYINT DEFAULT 1, -- 数据范围: 1=全公司 2=本部门 3=本人
status TINYINT DEFAULT 1
);
-- 权限(菜单/按钮/API)表
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY,
parent_id BIGINT DEFAULT 0,
perm_name VARCHAR(64),
perm_code VARCHAR(128) UNIQUE, -- 如 system:user:edit
perm_type TINYINT, -- 1=菜单 2=按钮 3=API
api_path VARCHAR(256), -- API路径(用于接口级权限控制)
api_method VARCHAR(10) -- GET/POST/PUT/DELETE
);
-- 用户-角色关联
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,
perm_id BIGINT,
PRIMARY KEY (role_id, perm_id)
);
-- 部门表(支持层级组织架构)
CREATE TABLE sys_dept (
id BIGINT PRIMARY KEY,
parent_id BIGINT DEFAULT 0,
dept_name VARCHAR(64),
ancestors VARCHAR(512), -- 祖先部门ID链,如 "0,1,5,10"(便于查询子部门)
order_num INT
);五、关键代码实现
5.1 JWT认证Filter
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenService jwtTokenService;
@Autowired
private UserPermissionService permissionService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null && jwtTokenService.isValid(token)) {
// 检查Token是否在黑名单中(登出后的Token)
if (redisTemplate.hasKey("token:blacklist:" + token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
String userId = jwtTokenService.getUserId(token);
// 加载用户权限(从缓存)
UserSecurityInfo userInfo = permissionService.loadUserPermissions(userId);
// 构建Spring Security认证对象
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
userInfo, null, userInfo.getAuthorities()
);
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}5.2 用户权限加载服务(带缓存)
@Service
@Slf4j
public class UserPermissionService {
@Autowired
private SysUserMapper userMapper;
@Autowired
private SysRoleMapper roleMapper;
@Autowired
private SysPermissionMapper permissionMapper;
@Autowired
private StringRedisTemplate redisTemplate;
private static final String PERM_CACHE_KEY = "user:permissions:";
private static final long PERM_CACHE_TTL = 30 * 60L; // 30分钟
/**
* 加载用户的完整权限信息(含缓存)
*/
public UserSecurityInfo loadUserPermissions(String userId) {
String cacheKey = PERM_CACHE_KEY + userId;
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return JsonUtils.fromJson(cached, UserSecurityInfo.class);
}
// 从DB加载
SysUser user = userMapper.findById(Long.parseLong(userId));
if (user == null) throw new UsernameNotFoundException("用户不存在");
// 加载角色列表
List<SysRole> roles = roleMapper.findByUserId(user.getId());
Set<String> roleCodes = roles.stream()
.map(SysRole::getRoleCode)
.collect(Collectors.toSet());
// 加载权限点(从角色→权限关联表汇总)
Set<Long> roleIds = roles.stream()
.map(SysRole::getId)
.collect(Collectors.toSet());
List<SysPermission> permissions = permissionMapper.findByRoleIds(roleIds);
Set<String> permCodes = permissions.stream()
.filter(p -> p.getPermCode() != null)
.map(SysPermission::getPermCode)
.collect(Collectors.toSet());
// 数据范围(取角色中最大的数据范围)
int dataScope = roles.stream()
.mapToInt(SysRole::getDataScope)
.min() // 数值越小,范围越大(1=全公司最大)
.orElse(3);
UserSecurityInfo userInfo = UserSecurityInfo.builder()
.userId(user.getId())
.username(user.getUsername())
.deptId(user.getDeptId())
.roleCodes(roleCodes)
.permCodes(permCodes)
.dataScope(dataScope)
.build();
// 写缓存
redisTemplate.opsForValue().set(
cacheKey, JsonUtils.toJson(userInfo), PERM_CACHE_TTL, TimeUnit.SECONDS
);
return userInfo;
}
/**
* 权限变更时清除缓存(用户角色修改、角色权限修改时调用)
*/
public void evictCache(String userId) {
redisTemplate.delete(PERM_CACHE_KEY + userId);
}
/**
* 批量清除受影响用户的权限缓存
* 角色权限修改时,需要清除拥有该角色的所有用户的缓存
*/
public void evictCacheByRole(Long roleId) {
List<Long> userIds = userMapper.findUserIdsByRoleId(roleId);
if (!userIds.isEmpty()) {
List<String> keys = userIds.stream()
.map(id -> PERM_CACHE_KEY + id)
.collect(Collectors.toList());
redisTemplate.delete(keys);
}
}
}5.3 权限注解与校验
/**
* 自定义权限注解,支持单权限和多权限AND/OR逻辑
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String[] value(); // 权限码
Logical logical() default Logical.AND; // AND=全部满足,OR=满足一个
}@Aspect
@Component
@Slf4j
public class PermissionAspect {
@Around("@annotation(requirePermission)")
public Object checkPermission(
ProceedingJoinPoint pjp,
RequirePermission requirePermission) throws Throwable {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !(auth.getPrincipal() instanceof UserSecurityInfo)) {
throw new AccessDeniedException("未登录");
}
UserSecurityInfo userInfo = (UserSecurityInfo) auth.getPrincipal();
Set<String> userPerms = userInfo.getPermCodes();
// 超级管理员跳过权限校验
if (userInfo.getRoleCodes().contains("ROLE_SUPER_ADMIN")) {
return pjp.proceed();
}
String[] requiredPerms = requirePermission.value();
boolean hasPermission;
if (requirePermission.logical() == Logical.AND) {
hasPermission = Arrays.stream(requiredPerms)
.allMatch(userPerms::contains);
} else { // OR
hasPermission = Arrays.stream(requiredPerms)
.anyMatch(userPerms::contains);
}
if (!hasPermission) {
log.warn("权限不足, userId={}, required={}, owned={}",
userInfo.getUserId(), Arrays.toString(requiredPerms), userPerms);
throw new AccessDeniedException("权限不足");
}
return pjp.proceed();
}
}5.4 数据权限(最复杂的部分)
/**
* 数据范围注解
* 加在Service方法上,自动根据用户的数据范围追加查询条件
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
String deptAlias() default "d"; // 部门表别名
String userAlias() default "u"; // 用户表别名
}@Aspect
@Component
@Slf4j
public class DataScopeAspect {
@Autowired
private SysDeptMapper deptMapper;
@Before("@annotation(dataScope)")
public void injectDataScope(JoinPoint jp, DataScope dataScope) {
UserSecurityInfo userInfo = getCurrentUser();
if (userInfo == null) return;
// 超级管理员不限制
if (userInfo.getRoleCodes().contains("ROLE_SUPER_ADMIN")) return;
String sqlCondition = buildDataScopeCondition(
userInfo, dataScope.deptAlias(), dataScope.userAlias());
if (sqlCondition == null) return;
// 通过方法参数传递SQL条件(需要基础查询对象有dataScope字段)
Object[] args = jp.getArgs();
for (Object arg : args) {
if (arg instanceof BaseQuery) {
((BaseQuery) arg).setDataScopeCondition(sqlCondition);
break;
}
}
}
private String buildDataScopeCondition(
UserSecurityInfo user, String deptAlias, String userAlias) {
switch (user.getDataScope()) {
case 1: // 全公司数据:不限制
return null;
case 2: // 本部门及下级部门
// 查询所有子部门ID
List<Long> deptIds = deptMapper.findAllChildrenIds(user.getDeptId());
deptIds.add(user.getDeptId());
String deptIdList = deptIds.stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
return deptAlias + ".dept_id IN (" + deptIdList + ")";
case 3: // 仅本人数据
return userAlias + ".create_by = " + user.getUserId();
default:
return userAlias + ".create_by = " + user.getUserId();
}
}
private UserSecurityInfo getCurrentUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof UserSecurityInfo) {
return (UserSecurityInfo) auth.getPrincipal();
}
return null;
}
}使用示例:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 查询订单列表
* @DataScope 会自动根据用户数据范围注入WHERE条件
* - 财务主管(全公司):查所有订单
* - 部门经理(本部门):只查本部门用户的订单
* - 普通员工(本人):只查自己创建的订单
*/
@DataScope(deptAlias = "o", userAlias = "o")
public List<Order> listOrders(OrderQuery query) {
return orderMapper.selectList(query); // query中携带了dataScopeCondition
}
}六、扩展性设计
权限缓存的一致性问题
用户权限修改后,缓存里的数据是旧的,导致权限不生效(或已撤销的权限仍然有效)。
解决方案:
- 权限修改时主动清除对应用户的缓存(需要知道哪些用户受影响)
- 缓存TTL设为30分钟,最多30分钟后生效
- 对安全敏感操作(如转账、高危操作),在操作前实时从DB查权限,不走缓存
微服务场景下的权限传递
多个微服务需要共享权限信息。方案:网关统一做认证+授权,在HTTP Header里把用户信息和权限列表(经过压缩/加密)传给下游服务,下游服务直接从Header解析,不需要再查数据库或Redis。
七、踩坑实录
坑1:角色爆炸问题
业务需求不断增加,导致角色数量暴增到300多个,没有人维护得过来。解决方案:把细粒度的操作权限("查看报表"、"导出报表")提取出来,角色只管大的功能模块,细粒度权限用ABAC策略处理。
坑2:权限缓存和数据库不一致
管理员修改了某用户的角色,但忘记清除缓存(漏调用evictCache),导致用户一直用旧权限工作了半天。解决方案:在Role和Permission的修改入口统一加缓存清除逻辑,用Spring事件机制解耦(发布PermissionChangeEvent,监听器统一处理缓存清除)。
坑3:数据权限注入SQL注入漏洞
早期数据范围条件是字符串拼接:"dept_id IN (" + deptIds + ")",如果deptIds来自用户输入(比如URL参数),存在SQL注入风险。解决方案:deptIds必须来自服务端查询(从组织架构DB查子部门ID),不允许客户端传入,彻底杜绝注入风险。
八、总结
权限系统的核心设计原则:
- 功能权限用RBAC,数据权限用数据范围:两者分离,各自管理好自己的边界
- 缓存权限提升性能,权限变更主动刷新缓存:不能只靠TTL过期
- 超级管理员设计:系统需要一个不受权限限制的超管角色,用于紧急修复
| 场景 | 推荐方案 |
|---|---|
| 菜单/按钮控制 | RBAC + 权限码注解 |
| 接口级权限 | Spring Security @PreAuthorize |
| 数据行级权限 | 数据范围注解 + AOP注入WHERE条件 |
| 动态策略(复杂场景) | ABAC + 策略引擎(Casbin/OPA) |
