目录
- RBAC2 模型核心概念
- 数据库表结构设计(含约束与审计)
- 工作流程详解
- Java 实现示例(含详细中文注释)
- API 使用示例
- 约束规则实现
- 审计功能增强
- 测试用例验证
一、RBAC2 模型核心概念
1.1 什么是 RBAC2?
RBAC2(Role-Based Access Control Level 2)在 RBAC1 的基础上增加了约束机制,用于定义角色分配和权限使用的业务规则,主要包括:
- 角色互斥(Mutual Exclusion):某些角色不能同时分配给同一个用户
- 基数约束(Cardinality Constraints):限制角色的用户数量或用户的角色数量
- 先决条件约束(Prerequisite Constraints):分配某角色前必须先拥有其他角色
- 运行时约束(Runtime Constraints):动态权限检查(如时间、IP等)
1.2 核心约束类型详解
1.2.1 角色互斥约束
- 静态互斥:用户不能同时拥有互斥角色(如会计和出纳)
- 动态互斥:用户在同一会话中不能同时激活互斥角色
1.2.2 基数约束
- 用户基数:限制单个用户可拥有的角色数量
- 角色基数:限制单个角色可分配的用户数量
1.2.3 先决条件约束
- 分配高级角色前必须先拥有基础角色
- 例如:分配"总监"角色前必须先拥有"经理"角色
1.3 形象类比
银行内部管控:
- 互斥约束:会计角色和出纳角色互斥,同一人不能既管账又管钱
- 基数约束:系统管理员角色最多只能分配给3个人
- 先决条件:要成为高级审计员,必须先担任普通审计员至少1年
这些约束确保了职责分离和风险控制,符合金融行业的合规要求
1.4 RBAC 模型演进对比
| 特性 | RBAC0 | RBAC1 | RBAC2 |
|---|---|---|---|
| 基础关系 | 用户-角色-权限 | +角色继承 | +约束机制 |
| 灵活性 | 低 | 中 | 高 |
| 复杂度 | 简单 | 中等 | 复杂 |
| 适用场景 | 小型应用 | 中型企业 | 大型企业/金融/政府 |
二、数据库表结构设计(含约束与审计)
2.1 表结构总览
| 表名 | 说明 | 关系 |
|---|---|---|
sys_user | 用户表 | - |
sys_role | 角色表 | - |
sys_role_hierarchy | 角色继承关系表 | 父角色 ↔ 子角色 |
sys_permission | 权限表 | - |
sys_user_role | 用户-角色关联表 | 用户 ↔ 角色 |
sys_role_permission | 角色-权限关联表 | 角色 ↔ 权限 |
sys_role_constraint | 角色约束表 | 定义各种约束规则 |
sys_constraint_violation_log | 约束违规日志表 | 记录约束违规尝试 |
sys_audit_log | 审计日志表 | 记录所有权限变更 |
2.2 新增约束相关表结构
2.2.1 角色约束表 (sys_role_constraint) - 核心新增
CREATE TABLE `sys_role_constraint` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '约束ID',
`constraint_type` VARCHAR(30) NOT NULL COMMENT '约束类型:MUTUAL_EXCLUSION-互斥, CARDINALITY-基数, PREREQUISITE-先决条件',
`constraint_subtype` VARCHAR(30) DEFAULT NULL COMMENT '约束子类型:STATIC-静态, DYNAMIC-动态(互斥约束用)',
`primary_role_id` BIGINT NOT NULL COMMENT '主角色ID(约束的主体角色)',
`secondary_role_id` BIGINT DEFAULT NULL COMMENT '次角色ID(互斥约束的另一个角色,先决条件的前置角色)',
`cardinality_limit` INT DEFAULT NULL COMMENT '基数限制值(基数约束用)',
`constraint_scope` VARCHAR(20) NOT NULL COMMENT '约束范围:USER-用户级, ROLE-角色级',
`description` VARCHAR(200) DEFAULT NULL COMMENT '约束描述',
`status` TINYINT DEFAULT 1 COMMENT '状态:1-启用,0-禁用',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_primary_role` (`primary_role_id`),
KEY `idx_secondary_role` (`secondary_role_id`),
KEY `idx_constraint_type` (`constraint_type`),
-- 互斥约束需要两个角色ID
CONSTRAINT `chk_mutual_exclusion_roles` CHECK (
(`constraint_type` = 'MUTUAL_EXCLUSION' AND `secondary_role_id` IS NOT NULL) OR
(`constraint_type` != 'MUTUAL_EXCLUSION')
),
-- 基数约束需要限制值
CONSTRAINT `chk_cardinality_limit` CHECK (
(`constraint_type` = 'CARDINALITY' AND `cardinality_limit` IS NOT NULL) OR
(`constraint_type` != 'CARDINALITY')
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色约束表';
2.2.2 约束违规日志表 (sys_constraint_violation_log) - 新增
CREATE TABLE `sys_constraint_violation_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '违规日志ID',
`constraint_id` BIGINT NOT NULL COMMENT '违反的约束ID',
`constraint_type` VARCHAR(30) NOT NULL COMMENT '约束类型',
`user_id` BIGINT NOT NULL COMMENT '违规用户ID',
`attempted_operation` VARCHAR(50) NOT NULL COMMENT '尝试的操作:ASSIGN_ROLE, ACTIVATE_ROLE, ACCESS_RESOURCE',
`violation_details` JSON DEFAULT NULL COMMENT '违规详情(JSON格式)',
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT '操作IP地址',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '违规时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_constraint_id` (`constraint_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='约束违规日志表';
2.2.3 角色表增强 (sys_role)
-- 在原有角色表基础上增加约束相关字段
ALTER TABLE `sys_role`
ADD COLUMN `max_users` INT DEFAULT NULL COMMENT '最大用户数(基数约束)',
ADD COLUMN `min_prerequisite_roles` INT DEFAULT 0 COMMENT '最少前置角色数';
2.3 示例约束数据
2.3.1 互斥约束示例
-- 会计和出纳角色互斥(静态互斥)
INSERT INTO sys_role_constraint (
constraint_type,
constraint_subtype,
primary_role_id,
secondary_role_id,
constraint_scope,
description
) VALUES
('MUTUAL_EXCLUSION', 'STATIC', 4, 5, 'USER', '会计和出纳角色互斥'),
('MUTUAL_EXCLUSION', 'STATIC', 5, 4, 'USER', '出纳和会计角色互斥');
-- 系统管理员和普通用户角色互斥
INSERT INTO sys_role_constraint (
constraint_type,
constraint_subtype,
primary_role_id,
secondary_role_id,
constraint_scope,
description
) VALUES
('MUTUAL_EXCLUSION', 'STATIC', 6, 7, 'USER', '管理员和普通用户互斥');
2.3.2 基数约束示例
-- 系统管理员角色最多3个用户
INSERT INTO sys_role_constraint (
constraint_type,
constraint_scope,
primary_role_id,
cardinality_limit,
description
) VALUES
('CARDINALITY', 'ROLE', 6, 3, '系统管理员最多3人');
-- 每个用户最多拥有5个角色
INSERT INTO sys_role_constraint (
constraint_type,
constraint_scope,
primary_role_id,
cardinality_limit,
description
) VALUES
('CARDINALITY', 'USER', 0, 5, '用户角色数量限制'); -- primary_role_id=0表示全局约束
2.3.3 先决条件约束示例
-- 分配总监角色前必须先拥有经理角色
INSERT INTO sys_role_constraint (
constraint_type,
constraint_scope,
primary_role_id,
secondary_role_id,
description
) VALUES
('PREREQUISITE', 'USER', 3, 2, '总监角色需要先拥有经理角色');
-- 分配高级审计员前必须先拥有普通审计员
INSERT INTO sys_role_constraint (
constraint_type,
constraint_scope,
primary_role_id,
secondary_role_id,
description
) VALUES
('PREREQUISITE', 'USER', 8, 9, '高级审计员需要先拥有普通审计员角色');
三、RBAC2 工作流程详解
3.1 角色分配约束检查流程
3.2 权限验证约束检查流程
3.3 核心约束检查逻辑
3.3.1 互斥约束检查
- 获取用户当前所有角色
- 获取待分配角色的所有互斥角色
- 检查当前角色中是否包含互斥角色
- 如果包含,拒绝分配
3.3.2 基数约束检查
- 用户基数:检查用户当前角色数量 + 新角色 ≤ 限制值
- 角色基数:检查角色当前用户数量 + 新用户 ≤ 限制值
3.3.3 先决条件约束检查
- 获取待分配角色的所有先决条件角色
- 检查用户当前角色是否包含所有先决条件角色
- 如果不包含,拒绝分配
四、Java 实现示例(含详细中文注释)
4.1 约束相关实体类
角色约束实体
/**
* 角色约束实体类
* 对应数据库表:sys_role_constraint
* 定义各种RBAC2约束规则
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleConstraint {
/**
* 约束ID,主键
*/
private Long id;
/**
* 约束类型:
* MUTUAL_EXCLUSION - 互斥约束
* CARDINALITY - 基数约束
* PREREQUISITE - 先决条件约束
*/
private String constraintType;
/**
* 约束子类型(主要用于互斥约束):
* STATIC - 静态互斥(用户不能同时拥有)
* DYNAMIC - 动态互斥(会话中不能同时激活)
*/
private String constraintSubtype;
/**
* 主角色ID(约束的主体角色)
* 例如:互斥约束中的会计角色ID
* 基数约束中的被限制角色ID
* 先决条件约束中的目标角色ID
*/
private Long primaryRoleId;
/**
* 次角色ID(约束的关联角色):
* 互斥约束:另一个互斥角色ID
* 先决条件约束:前置角色ID
* 基数约束:此字段为null
*/
private Long secondaryRoleId;
/**
* 基数限制值(仅基数约束使用)
* 例如:角色最多分配给3个用户
* 用户最多拥有5个角色
*/
private Integer cardinalityLimit;
/**
* 约束范围:
* USER - 用户级约束(限制用户的行为)
* ROLE - 角色级约束(限制角色的分配)
*/
private String constraintScope;
/**
* 约束描述,用于显示和日志记录
*/
private String description;
/**
* 约束状态:1-启用,0-禁用
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
约束违规日志实体
/**
* 约束违规日志实体类
* 对应数据库表:sys_constraint_violation_log
* 记录所有违反约束规则的尝试
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ConstraintViolationLog {
/**
* 违规日志ID,主键
*/
private Long id;
/**
* 违反的约束ID
*/
private Long constraintId;
/**
* 约束类型(冗余存储,便于查询)
*/
private String constraintType;
/**
* 违规用户ID
*/
private Long userId;
/**
* 尝试的操作类型:
* ASSIGN_ROLE - 分配角色
* ACTIVATE_ROLE - 激活角色
* ACCESS_RESOURCE - 访问资源
*/
private String attemptedOperation;
/**
* 违规详情(JSON格式)
* 包含违规的具体原因、相关角色、当前状态等
*/
private String violationDetails;
/**
* 操作IP地址
*/
private String ipAddress;
/**
* 违规时间
*/
private LocalDateTime createTime;
}
4.2 约束服务核心实现
/**
* RBAC2约束服务实现类
* 负责各种约束规则的检查和验证
*/
@Service
@Slf4j
public class Rbac2ConstraintService {
@Autowired
private RoleConstraintMapper roleConstraintMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private ConstraintViolationLogService violationLogService;
/**
* 检查分配角色给用户是否违反约束规则
* 这是RBAC2的核心约束检查方法
*
* 检查顺序:
* 1. 互斥约束检查
* 2. 基数约束检查
* 3. 先决条件约束检查
*
* @param userId 用户ID
* @param roleId 待分配的角色ID
* @param operatorId 操作人ID(用于审计)
* @param request 审计信息
* @throws ConstraintViolationException 违反约束时抛出异常
*/
public void checkRoleAssignmentConstraints(Long userId, Long roleId,
Long operatorId, AuditRequest request)
throws ConstraintViolationException {
log.info("开始检查角色分配约束: userId={}, roleId={}", userId, roleId);
try {
// 1. 获取用户当前所有角色
List<Long> currentUserRoleIds = userMapper.selectRoleIdsByUserId(userId);
log.debug("用户当前角色ID列表: {}", currentUserRoleIds);
// 2. 检查互斥约束
checkMutualExclusionConstraints(userId, roleId, currentUserRoleIds, request);
// 3. 检查基数约束
checkCardinalityConstraints(userId, roleId, currentUserRoleIds, request);
// 4. 检查先决条件约束
checkPrerequisiteConstraints(userId, roleId, currentUserRoleIds, request);
log.info("角色分配约束检查通过: userId={}, roleId={}", userId, roleId);
} catch (ConstraintViolationException e) {
// 记录约束违规日志
violationLogService.logViolation(
e.getConstraintId(),
e.getConstraintType(),
userId,
"ASSIGN_ROLE",
e.getViolationDetails(),
request.getIpAddress()
);
throw e;
} catch (Exception e) {
log.error("约束检查发生异常", e);
throw new ConstraintViolationException("约束检查失败", e);
}
}
/**
* 检查互斥约束
* 确保用户不会同时拥有互斥角色
*
* @param userId 用户ID
* @param newRoleId 待分配的新角色ID
* @param currentUserRoleIds 用户当前角色ID列表
* @param request 审计请求
* @throws ConstraintViolationException 违反互斥约束时抛出
*/
private void checkMutualExclusionConstraints(Long userId, Long newRoleId,
List<Long> currentUserRoleIds,
AuditRequest request)
throws ConstraintViolationException {
log.debug("开始检查互斥约束: newRoleId={}", newRoleId);
// 查询新角色的所有互斥角色
List<RoleConstraint> mutualExclusionConstraints =
roleConstraintMapper.selectMutualExclusionConstraints(newRoleId);
if (CollectionUtils.isEmpty(mutualExclusionConstraints)) {
log.debug("角色 {} 没有互斥约束", newRoleId);
return;
}
// 检查用户当前角色是否包含任何互斥角色
Set<Long> mutualExclusionRoleIds = mutualExclusionConstraints.stream()
.map(RoleConstraint::getSecondaryRoleId)
.collect(Collectors.toSet());
for (Long currentRoleId : currentUserRoleIds) {
if (mutualExclusionRoleIds.contains(currentRoleId)) {
// 找到互斥角色,构建违规详情
RoleConstraint violatedConstraint = mutualExclusionConstraints.stream()
.filter(c -> c.getSecondaryRoleId().equals(currentRoleId))
.findFirst()
.orElse(null);
String violationDetails = buildViolationDetails(
"用户不能同时拥有互斥角色",
Map.of(
"userId", userId,
"newRoleId", newRoleId,
"existingRoleId", currentRoleId,
"constraintId", violatedConstraint != null ? violatedConstraint.getId() : null
)
);
log.warn("违反互斥约束: userId={}, newRoleId={}, existingRoleId={}",
userId, newRoleId, currentRoleId);
throw new ConstraintViolationException(
"角色分配违反互斥约束",
violatedConstraint != null ? violatedConstraint.getId() : null,
"MUTUAL_EXCLUSION",
violationDetails
);
}
}
log.debug("互斥约束检查通过");
}
/**
* 检查基数约束
* 包括用户角色数量限制和角色用户数量限制
*
* @param userId 用户ID
* @param newRoleId 待分配的新角色ID
* @param currentUserRoleIds 用户当前角色ID列表
* @param request 审计请求
* @throws ConstraintViolationException 违反基数约束时抛出
*/
private void checkCardinalityConstraints(Long userId, Long newRoleId,
List<Long> currentUserRoleIds,
AuditRequest request)
throws ConstraintViolationException {
log.debug("开始检查基数约束");
// 1. 检查用户角色数量限制(用户级基数约束)
checkUserCardinalityConstraint(userId, newRoleId, currentUserRoleIds);
// 2. 检查角色用户数量限制(角色级基数约束)
checkRoleCardinalityConstraint(newRoleId);
log.debug("基数约束检查通过");
}
/**
* 检查用户角色数量限制
*/
private void checkUserCardinalityConstraint(Long userId, Long newRoleId,
List<Long> currentUserRoleIds)
throws ConstraintViolationException {
// 查询用户级基数约束(primary_role_id = 0 表示全局用户约束)
RoleConstraint userCardinalityConstraint =
roleConstraintMapper.selectUserCardinalityConstraint();
if (userCardinalityConstraint == null) {
return; // 没有用户基数约束
}
int newUserRoleCount = currentUserRoleIds.size() + 1;
int limit = userCardinalityConstraint.getCardinalityLimit();
if (newUserRoleCount > limit) {
String violationDetails = buildViolationDetails(
"用户角色数量超出限制",
Map.of(
"userId", userId,
"currentRoleCount", currentUserRoleIds.size(),
"newRoleCount", newUserRoleCount,
"limit", limit,
"constraintId", userCardinalityConstraint.getId()
)
);
log.warn("违反用户基数约束: userId={}, currentCount={}, newCount={}, limit={}",
userId, currentUserRoleIds.size(), newUserRoleCount, limit);
throw new ConstraintViolationException(
String.format("用户最多只能拥有%d个角色", limit),
userCardinalityConstraint.getId(),
"CARDINALITY",
violationDetails
);
}
}
/**
* 检查角色用户数量限制
*/
private void checkRoleCardinalityConstraint(Long roleId)
throws ConstraintViolationException {
// 查询角色级基数约束
RoleConstraint roleCardinalityConstraint =
roleConstraintMapper.selectRoleCardinalityConstraint(roleId);
if (roleCardinalityConstraint == null) {
return; // 没有角色基数约束
}
// 获取角色当前用户数量
int currentUserCount = userMapper.selectUserCountByRoleId(roleId);
int newTotalCount = currentUserCount + 1;
int limit = roleCardinalityConstraint.getCardinalityLimit();
if (newTotalCount > limit) {
String violationDetails = buildViolationDetails(
"角色用户数量超出限制",
Map.of(
"roleId", roleId,
"currentUserCount", currentUserCount,
"newTotalCount", newTotalCount,
"limit", limit,
"constraintId", roleCardinalityConstraint.getId()
)
);
log.warn("违反角色基数约束: roleId={}, currentCount={}, newCount={}, limit={}",
roleId, currentUserCount, newTotalCount, limit);
throw new ConstraintViolationException(
String.format("角色最多只能分配给%d个用户", limit),
roleCardinalityConstraint.getId(),
"CARDINALITY",
violationDetails
);
}
}
/**
* 检查先决条件约束
* 确保分配高级角色前用户已拥有必要的前置角色
*
* @param userId 用户ID
* @param newRoleId 待分配的新角色ID
* @param currentUserRoleIds 用户当前角色ID列表
* @param request 审计请求
* @throws ConstraintViolationException 违反先决条件约束时抛出
*/
private void checkPrerequisiteConstraints(Long userId, Long newRoleId,
List<Long> currentUserRoleIds,
AuditRequest request)
throws ConstraintViolationException {
log.debug("开始检查先决条件约束: newRoleId={}", newRoleId);
// 查询新角色的所有先决条件角色
List<RoleConstraint> prerequisiteConstraints =
roleConstraintMapper.selectPrerequisiteConstraints(newRoleId);
if (CollectionUtils.isEmpty(prerequisiteConstraints)) {
log.debug("角色 {} 没有先决条件约束", newRoleId);
return;
}
Set<Long> currentUserRoleSet = new HashSet<>(currentUserRoleIds);
Set<Long> missingPrerequisites = new HashSet<>();
// 检查是否缺少任何先决条件角色
for (RoleConstraint constraint : prerequisiteConstraints) {
Long prerequisiteRoleId = constraint.getSecondaryRoleId();
if (!currentUserRoleSet.contains(prerequisiteRoleId)) {
missingPrerequisites.add(prerequisiteRoleId);
}
}
if (!missingPrerequisites.isEmpty()) {
String violationDetails = buildViolationDetails(
"缺少先决条件角色",
Map.of(
"userId", userId,
"targetRoleId", newRoleId,
"missingPrerequisites", missingPrerequisites,
"currentRoles", currentUserRoleIds,
"constraintIds", prerequisiteConstraints.stream()
.map(RoleConstraint::getId)
.collect(Collectors.toList())
)
);
log.warn("违反先决条件约束: userId={}, targetRoleId={}, missingPrerequisites={}",
userId, newRoleId, missingPrerequisites);
// 使用第一个约束的ID作为代表
Long constraintId = prerequisiteConstraints.get(0).getId();
throw new ConstraintViolationException(
"分配角色前必须先拥有必要的前置角色",
constraintId,
"PREREQUISITE",
violationDetails
);
}
log.debug("先决条件约束检查通过");
}
/**
* 构建违规详情JSON字符串
*/
private String buildViolationDetails(String reason, Map<String, Object> details) {
Map<String, Object> violationInfo = new HashMap<>();
violationInfo.put("reason", reason);
violationInfo.put("details", details);
violationInfo.put("timestamp", LocalDateTime.now());
try {
return JsonUtils.toJson(violationInfo);
} catch (Exception e) {
log.error("构建违规详情JSON失败", e);
return "{\"reason\":\"" + reason + "\",\"error\":\"JSON序列化失败\"}";
}
}
}
4.3 约束违规异常定义
/**
* 约束违规异常
* 用于表示违反RBAC2约束规则的情况
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ConstraintViolationException extends RuntimeException {
/**
* 违反的约束ID
*/
private Long constraintId;
/**
* 约束类型
*/
private String constraintType;
/**
* 违规详情
*/
private String violationDetails;
public ConstraintViolationException(String message, Long constraintId,
String constraintType, String violationDetails) {
super(message);
this.constraintId = constraintId;
this.constraintType = constraintType;
this.violationDetails = violationDetails;
}
public ConstraintViolationException(String message, Throwable cause) {
super(message, cause);
}
}
4.4 约束违规日志服务
/**
* 约束违规日志服务
* 负责记录所有违反约束规则的尝试
*/
@Service
@Slf4j
public class ConstraintViolationLogService {
@Autowired
private ConstraintViolationLogMapper violationLogMapper;
/**
* 记录约束违规日志
*
* @param constraintId 约束ID
* @param constraintType 约束类型
* @param userId 违规用户ID
* @param attemptedOperation 尝试的操作
* @param violationDetails 违规详情
* @param ipAddress 操作IP
*/
public void logViolation(Long constraintId, String constraintType,
Long userId, String attemptedOperation,
String violationDetails, String ipAddress) {
try {
ConstraintViolationLog log = ConstraintViolationLog.builder()
.constraintId(constraintId)
.constraintType(constraintType)
.userId(userId)
.attemptedOperation(attemptedOperation)
.violationDetails(violationDetails)
.ipAddress(ipAddress)
.build();
violationLogMapper.insert(log);
log.info("约束违规日志记录成功: userId={}, constraintType={}", userId, constraintType);
} catch (Exception e) {
log.error("记录约束违规日志失败", e);
// 违规日志失败不应影响主业务流程
}
}
/**
* 查询用户的约束违规历史
*/
public List<ConstraintViolationLog> getUserViolationHistory(Long userId) {
return violationLogMapper.selectByUserId(userId);
}
/**
* 查询特定约束的违规统计
*/
public ViolationStatistics getConstraintViolationStats(Long constraintId) {
return violationLogMapper.selectViolationStatsByConstraint(constraintId);
}
}
五、API 使用示例
5.1 角色分配API(带约束检查)
/**
* 角色管理Controller(支持RBAC2约束)
*/
@RestController
@RequestMapping("/api/role")
public class Rbac2RoleController {
@Autowired
private Rbac2ConstraintService constraintService;
@Autowired
private UserRoleService userRoleService;
/**
* 分配角色给用户(带RBAC2约束检查)
*
* 请求示例:
* POST /api/role/assign
* {
* "userId": 1001,
* "roleId": 4 // 会计角色
* }
*
* 约束检查:
* 1. 如果用户已拥有出纳角色(ID=5),则分配失败(互斥约束)
* 2. 如果会计角色已分配给3个用户,则分配失败(基数约束)
* 3. 如果分配总监角色但用户没有经理角色,则分配失败(先决条件)
*/
@PostMapping("/assign")
public Result<Void> assignRoleToUser(@RequestBody @Valid AssignRoleRequest request,
HttpServletRequest httpRequest) {
// 获取当前操作人
Long currentUserId = getCurrentUserId(httpRequest);
// 构建审计信息
AuditRequest auditRequest = AuditRequest.builder()
.ipAddress(getClientIpAddress(httpRequest))
.userAgent(httpRequest.getHeader("User-Agent"))
.build();
try {
// 1. 执行RBAC2约束检查
constraintService.checkRoleAssignmentConstraints(
request.getUserId(),
request.getRoleId(),
currentUserId,
auditRequest
);
// 2. 约束检查通过,执行角色分配
userRoleService.assignRoleToUser(request.getUserId(), request.getRoleId());
return Result.success();
} catch (ConstraintViolationException e) {
// 返回具体的约束违规信息
log.warn("角色分配违反约束: {}", e.getMessage());
return Result.error(ResultCode.CONSTRAINT_VIOLATION, e.getMessage())
.addData("constraintType", e.getConstraintType())
.addData("violationDetails", e.getViolationDetails());
}
}
/**
* 获取角色约束详情(用于前端展示)
*/
@GetMapping("/{roleId}/constraints")
public Result<RoleConstraintInfo> getRoleConstraints(@PathVariable Long roleId) {
RoleConstraintInfo constraintInfo = buildRoleConstraintInfo(roleId);
return Result.success(constraintInfo);
}
}
5.2 约束管理API
/**
* 约束管理Controller
*/
@RestController
@RequestMapping("/api/constraint")
public class ConstraintController {
@Autowired
private RoleConstraintService roleConstraintService;
/**
* 创建角色约束
*
* 互斥约束示例:
* POST /api/constraint
* {
* "constraintType": "MUTUAL_EXCLUSION",
* "constraintSubtype": "STATIC",
* "primaryRoleId": 4,
* "secondaryRoleId": 5,
* "constraintScope": "USER",
* "description": "会计和出纳互斥"
* }
*
* 基数约束示例:
* {
* "constraintType": "CARDINALITY",
* "constraintScope": "ROLE",
* "primaryRoleId": 6,
* "cardinalityLimit": 3,
* "description": "系统管理员最多3人"
* }
*/
@PostMapping
public Result<Void> createConstraint(@RequestBody @Valid CreateConstraintRequest request) {
roleConstraintService.createConstraint(request);
return Result.success();
}
/**
* 查询约束违规日志
*/
@GetMapping("/violations")
public Result<PageResult<ConstraintViolationLog>> getViolationLogs(
ConstraintViolationQuery query) {
PageResult<ConstraintViolationLog> result =
constraintViolationLogService.getViolationLogs(query);
return Result.success(result);
}
}
六、约束规则实现细节
6.1 互斥约束实现
静态互斥 vs 动态互斥
/**
* 动态互斥约束检查(运行时检查)
* 用于会话中角色激活的场景
*/
public boolean checkDynamicMutualExclusion(Long userId, Set<Long> activeRoleIds) {
// 获取用户所有角色的动态互斥约束
List<RoleConstraint> dynamicConstraints =
roleConstraintMapper.selectDynamicMutualExclusionConstraints(
new ArrayList<>(activeRoleIds)
);
// 检查激活角色中是否包含互斥角色对
Set<Long> activeRoleSet = new HashSet<>(activeRoleIds);
for (RoleConstraint constraint : dynamicConstraints) {
if (activeRoleSet.contains(constraint.getSecondaryRoleId())) {
return false; // 发现互斥角色同时激活
}
}
return true;
}
6.2 基数约束优化
缓存角色用户数量
/**
* 使用Redis缓存角色用户数量,避免频繁数据库查询
*/
@Service
public class RoleCardinalityCacheService {
@Autowired
private RedisTemplate<String, Integer> redisTemplate;
private static final String ROLE_USER_COUNT_KEY = "role:user:count:%d";
/**
* 获取角色用户数量(带缓存)
*/
public int getRoleUserCount(Long roleId) {
String key = String.format(ROLE_USER_COUNT_KEY, roleId);
Integer count = redisTemplate.opsForValue().get(key);
if (count == null) {
// 缓存未命中,查询数据库
count = userMapper.selectUserCountByRoleId(roleId);
redisTemplate.opsForValue().set(key, count, 5, TimeUnit.MINUTES);
}
return count;
}
/**
* 更新角色用户数量缓存
*/
public void updateRoleUserCount(Long roleId) {
String key = String.format(ROLE_USER_COUNT_KEY, roleId);
int newCount = userMapper.selectUserCountByRoleId(roleId);
redisTemplate.opsForValue().set(key, newCount, 5, TimeUnit.MINUTES);
}
}
七、审计功能增强
7.1 约束相关审计日志
约束创建审计
{
"operationType": "CREATE",
"targetType": "CONSTRAINT",
"targetId": 101,
"operationDetails": {
"constraintType": "MUTUAL_EXCLUSION",
"primaryRoleId": 4,
"secondaryRoleId": 5,
"constraintScope": "USER",
"description": "会计和出纳互斥"
}
}
约束违规审计
{
"operationType": "VIOLATION",
"targetType": "CONSTRAINT",
"targetId": 101,
"operationDetails": {
"userId": 1001,
"attemptedOperation": "ASSIGN_ROLE",
"violationReason": "用户不能同时拥有互斥角色",
"currentRoles": [5],
"attemptedRole": 4
}
}
7.2 审计查询增强
/**
* 审计日志查询支持约束相关操作
*/
public List<AuditLog> getConstraintRelatedAuditLogs(Long constraintId) {
return auditLogMapper.selectByCondition(AuditLogQuery.builder()
.targetType("CONSTRAINT")
.targetId(constraintId)
.build());
}
八、测试用例验证
8.1 互斥约束测试
@Test
void testMutualExclusionConstraint_ShouldPreventConflictingRoleAssignment() {
// 准备数据:
// - 用户ID=1001 已拥有出纳角色(ID=5)
// - 会计角色(ID=4)和出纳角色(ID=5)互斥
// 尝试分配会计角色给用户
ConstraintViolationException exception = assertThrows(
ConstraintViolationException.class,
() -> constraintService.checkRoleAssignmentConstraints(1001L, 4L, 1L, createAuditRequest())
);
assertEquals("MUTUAL_EXCLUSION", exception.getConstraintType());
assertTrue(exception.getMessage().contains("互斥约束"));
// 验证违规日志已记录
List<ConstraintViolationLog> logs = violationLogService.getUserViolationHistory(1001L);
assertEquals(1, logs.size());
assertEquals("ASSIGN_ROLE", logs.get(0).getAttemptedOperation());
}
8.2 基数约束测试
@Test
void testCardinalityConstraint_ShouldLimitRoleAssignments() {
// 准备数据:
// - 系统管理员角色(ID=6)基数限制为3
// - 当前已有3个用户拥有该角色
// 尝试分配第4个用户
ConstraintViolationException exception = assertThrows(
ConstraintViolationException.class,
() -> constraintService.checkRoleAssignmentConstraints(1004L, 6L, 1L, createAuditRequest())
);
assertEquals("CARDINALITY", exception.getConstraintType());
assertTrue(exception.getMessage().contains("最多只能分配给3个用户"));
}
8.3 先决条件约束测试
@Test
void testPrerequisiteConstraint_ShouldRequireBaseRoles() {
// 准备数据:
// - 用户ID=1001 没有任何角色
// - 总监角色(ID=3)需要先拥有经理角色(ID=2)
// 尝试直接分配总监角色
ConstraintViolationException exception = assertThrows(
ConstraintViolationException.class,
() -> constraintService.checkRoleAssignmentConstraints(1001L, 3L, 1L, createAuditRequest())
);
assertEquals("PREREQUISITE", exception.getConstraintType());
assertTrue(exception.getMessage().contains("前置角色"));
}
8.4 约束组合测试
@Test
void testMultipleConstraints_ShouldCheckAllConstraints() {
// 准备复杂场景:
// - 用户已拥有出纳角色(与会计互斥)
// - 会计角色已达基数限制
// - 用户缺少必要的前置角色
// 应该优先报告互斥约束(第一个检查的约束)
ConstraintViolationException exception = assertThrows(
ConstraintViolationException.class,
() -> constraintService.checkRoleAssignmentConstraints(1001L, 4L, 1L, createAuditRequest())
);
assertEquals("MUTUAL_EXCLUSION", exception.getConstraintType());
}
总结
RBAC2 模型通过引入约束机制,为权限系统提供了更强的业务规则控制能力,特别适用于对安全性要求较高的场景。
关键特性
- 互斥约束:确保职责分离,防止利益冲突
- 基数约束:控制权限分配的规模和范围
- 先决条件约束:保证权限分配的逻辑合理性
- 完整审计:记录所有约束相关操作和违规尝试
实施建议
- 约束粒度:根据业务需求合理设置约束粒度,避免过度约束
- 性能优化:对频繁检查的约束进行缓存
- 用户体验:提供清晰的约束违规提示信息
- 监控告警:对频繁的约束违规行为设置监控告警
RBAC2 是企业级权限系统的理想选择,能够有效支撑复杂的合规要求和安全策略。
1548

被折叠的 条评论
为什么被折叠?



