48 KiB
权限与账户领域技术方案
领域编号: 4.5
微服务: ether-auth
最后更新: 2026-04-26
更新记录:
- 2026-04-26: 反向同步实际代码实现到文档。主要变更:Role.type 改为 SYSTEM/PROJECT/DEPARTMENT 三级分类;Role.status 改为 RoleStatus 枚举;Permission 结构简化(parentCode 替代 parentId);UserRole 简化为 M2M 中间表;OperationLog 更名为 AuditLog(表名和字段结构变更);新增实体(EnterpriseUser/ProjectStaff/ProjectStaffRole/Resident/ResidentSpace/Space/UserProject/DataAccess/Dept/SysConfig);新增 API(部门管理/项目成员管理/数据访问授权/审计日志/系统配置/企业员工/用户项目关联);新增业务规则(登录失败锁定/密码强度校验/审计日志增强/项目成员角色/部门类型/住户认证);角色体系改为数据库动态管理;API路径去掉v1版本号。标注 TODO 项:Permission 需增加菜单路由属性(path/component/icon)。
一、领域概述
1.1 领域职责
权限与账户领域负责 Ether 平台的身份认证与授权管理:
- 用户认证与登录
- RBAC 权限模型(角色-权限-用户关联)
- 项目隔离与数据权限
- 按钮级权限控制
- 状态驱动权限
- 访客凭证管理
- 操作日志审计
1.2 核心概念
| 概念 | 说明 | 对应实体 |
|---|---|---|
| 用户 | 系统登录账户,支持四种用户类型 | User |
| 企业员工 | 企业类型用户的扩展信息 | EnterpriseUser |
| 项目员工 | 项目类型员工的扩展信息,含班次、岗位状态 | ProjectStaff |
| 住户 | 业主/家属/租户,含认证流程 | Resident |
| 角色 | 权限集合,支持系统级/项目级/部门级 | Role |
| 权限 | 功能访问控制点,支持菜单/按钮/API三种类型 | Permission |
| 部门 | 组织架构节点,支持树形结构 | Dept |
| 用户-项目关联 | 用户参与项目的多对多关系 | UserProject |
| 项目员工角色 | 项目员工在项目中的角色分配 | ProjectStaffRole |
| 数据访问授权 | 细粒度数据访问控制记录(接口预留) | DataAccess |
| 审计日志 | 操作审计记录 | AuditLog |
| 房屋空间 | 项目下的房屋/空间信息 | Space |
| 住户-房屋关联 | 住户与房屋的绑定关系 | ResidentSpace |
| 系统配置 | 键值对形式的系统参数 | SysConfig |
二、角色体系设计
2.1 角色总览
| 类型 | 角色编码 | 角色名称 | 数据范围 | 终端类型 | 一线人员 |
|---|---|---|---|---|---|
| 系统级 | |||||
| 系统管理 | SUPER_ADMIN | 超级管理员 | ALL | ALL | 否 |
| 系统管理 | SYS_ADMIN | 系统管理员 | ALL | ADMIN_ONLY | 否 |
| 项目管理 | |||||
| 项目管理 | PROPERTY_MANAGER | 物业经理 | PROJECT | ALL | 否 |
| 项目管理 | PROJECT_MANAGER | 项目经理 | PROJECT | ALL | 否 |
| 部门主管 | |||||
| 工程管理 | ENGINEERING_LEAD | 工程主管 | DEPARTMENT | ALL | 否 |
| 安保管理 | SECURITY_LEAD | 安保主管 | DEPARTMENT | ALL | 否 |
| 保洁管理 | CLEANING_LEAD | 保洁主管 | DEPARTMENT | ALL | 否 |
| 财务管理 | FINANCE_LEAD | 财务主管 | DEPARTMENT | ALL | 否 |
| 一线执行 | |||||
| 工程执行 | MAINTENANCE_STAFF | 维修人员 | SELF | MOBILE_ONLY | 是 |
| 安保执行 | SECURITY_STAFF | 安保人员 | SELF | MOBILE_ONLY | 是 |
| 保洁执行 | CLEANING_STAFF | 保洁人员 | SELF | MOBILE_ONLY | 是 |
| 服务支持 | |||||
| 业主服务 | CS_STAFF | 客服人员 | PROJECT | ALL | 否 |
| 外部用户 | |||||
| 业主 | OWNER | 业主 | SELF | OWNER_APP | 否 |
2.2 角色职责说明
2.2.1 系统级角色
| 角色 | 职责范围 | 典型场景 |
|---|---|---|
| 超级管理员 | 系统最高权限,管理所有项目和用户 | 系统初始化、用户创建、项目创建 |
| 系统管理员 | 系统配置管理,不参与业务操作 | 系统参数配置、日志查看 |
2.2.2 项目管理角色
| 角色 | 职责范围 | 典型场景 |
|---|---|---|
| 物业经理 | 项目经营第一负责人,拥有本项目全部权限 | 项目整体运营、费用审核、合同审批 |
| 项目经理 | 单项目管理,拥有本项目全部业务权限 | 工单管理、人员分配、报表查看 |
2.2.3 部门主管角色
| 角色 | 管理模块 | 职责范围 | 典型场景 |
|---|---|---|---|
| 工程主管 | 工单、设备、工程巡检 | 本部门工程事务管理 | 工单分配、设备管理、巡检计划制定 |
| 安保主管 | 安保巡检、访客管理 | 本部门安保事务管理 | 安保巡检计划、访客核验监督 |
| 保洁主管 | 保洁巡检、品质检查 | 本部门保洁事务管理 | 保洁巡检计划、品质检查 |
| 财务主管 | 账单、收费、费用审核 | 本部门财务事务管理 | 账单生成、费用审核、报表导出 |
2.2.4 一线执行角色
| 角色 | 执行范围 | 终端限制 | 典型场景 |
|---|---|---|---|
| 维修人员 | 工单处理、设备巡检 | 仅移动端 | 接单、维修、填报费用、设备巡检 |
| 安保人员 | 安保巡检、访客核验 | 仅移动端 | 扫码签到、访客核验、异常报送 |
| 保洁人员 | 保洁巡检、品质检查 | 仅移动端 | 扫码签到、品质异常报送 |
2.2.5 服务支持角色
| 角色 | 职责范围 | 典型场景 |
|---|---|---|
| 客服人员 | 业主服务接口,不参与工单流程 | 业主咨询、资料维护、访客辅助核验 |
2.2.6 外部用户角色
| 角色 | 权限范围 | 典型场景 |
|---|---|---|
| 业主 | 本人房产相关数据 | 在线报修、缴费、访客邀请、评价 |
三、领域模型
3.1 聚合根设计
User(系统用户)
@Entity
@Table(name = "auth_user")
@Data
public class User {
@Id
private UUID id;
private String username; // 登录账号
private String password; // BCrypt加密密码
private String salt; // 密码盐值(BCrypt模式下冗余)
private String realName; // 真实姓名
private String phone;
private String email;
private String avatar; // 头像URL
private UserStatus status; // ACTIVE/LOCKED/DISABLED
private UserType userType; // ENTERPRISE/PROJECT_STAFF/RESIDENT/CUSTOMER
private UUID deptId; // 所属部门ID
private LocalDateTime lastLoginTime;
private String lastLoginIp;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "auth_user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private List<Role> roles;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private UUID createdBy;
}
public enum UserStatus {
ACTIVE("正常"),
LOCKED("锁定"),
DISABLED("禁用");
}
public enum UserType {
ENTERPRISE("企业员工"),
PROJECT_STAFF("项目员工"),
RESIDENT("住户"),
CUSTOMER("客户");
}
Role(角色)
@Entity
@Table(name = "auth_role")
@Data
public class Role {
@Id
private UUID id;
private UUID projectId; // 项目隔离(NULL表示系统级)
private String name; // 角色名称
private String code; // 角色编码
private String description; // 角色描述
private RoleType type; // SYSTEM(系统级)/PROJECT(项目级)/DEPARTMENT(部门级)
// 数据权限范围(四级)
private DataScope dataScope; // ALL/PROJECT/DEPARTMENT/SELF
// 角色状态(枚举替代布尔值)
private RoleStatus status; // ENABLED(启用)/DISABLED(禁用)
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "auth_role_permission",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private List<Permission> permissions;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
public enum RoleType {
SYSTEM("系统级"),
PROJECT("项目级"),
DEPARTMENT("部门级");
}
public enum RoleStatus {
ENABLED("启用"),
DISABLED("禁用");
}
// 数据权限范围枚举(四级)
public enum DataScope {
ALL("全部数据"),
PROJECT("本项目数据"),
DEPARTMENT("本部门数据"),
SELF("仅本人数据");
}
注意: 实际代码中角色通过数据库动态管理,无硬编码预定义角色列表。原设计中的 businessType/terminalType/isFrontline 业务属性字段未实现,如需使用需后续补充。
Permission(权限)
@Entity
@Table(name = "auth_permission")
@Data
public class Permission {
@Id
private UUID id;
private String name; // 权限名称
private String code; // 权限编码: module:resource:action
private String type; // 权限类型: MENU/BUTTON/API(String类型)
// 资源信息
private String resource; // 资源路径(API类型)
private String method; // HTTP方法(API类型)
private String description; // 权限描述
// 树形结构(简化:parentCode替代parentId)
private String parentCode; // 父权限代码(替代原 parentId UUID引用)
private Integer sortOrder; // 排序序号
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "permissions")
private List<Role> roles;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
TODO: Permission 当前实现缺少菜单路由属性(path/component/icon),以及模块/资源/操作拆分字段(module/resource/action),需后续补充以支持前端菜单渲染和权限树展示。
3.2 关联实体
UserRole(用户角色关联)
注意: 实际代码中 UserRole 已简化为 JPA 自动管理的 M2M 中间表
auth_user_role,仅包含 user_id 和 role_id 两个外键列,去掉了原设计中的 projectId 和 isDefault 字段。
// auth_user_role 关联表(JPA自动管理,无独立实体)
// 字段: user_id (FK → auth_user), role_id (FK → auth_role)
RolePermission(角色权限关联)
@Entity
@Table(name = "auth_role_permission")
@Data
public class RolePermission {
@Id
private UUID id;
private UUID roleId;
private UUID permissionId;
private LocalDateTime createdAt;
}
四、权限编码规范
4.1 编码格式
格式: module:resource:action
| 组成部分 | 说明 | 示例值 |
|---|---|---|
| module | 模块标识 | system, mdm, ops, asset, finance |
| resource | 资源类型 | user, role, work_order, equipment, bill |
| action | 操作类型 | view, create, edit, delete, assign, accept, complete |
4.2 操作类型说明
| 操作类型 | 说明 | 适用场景 |
|---|---|---|
| view | 查看 | 列表、详情查看 |
| create | 创建 | 新增记录 |
| edit | 编辑 | 修改记录 |
| delete | 删除 | 删除记录 |
| assign | 分配 | 工单分配、任务分配 |
| accept | 接单 | 工单接单 |
| start | 开始 | 开始处理、开始巡检 |
| complete | 完成 | 完成工单、完成巡检 |
| transfer | 转单 | 工单转单 |
| close | 关闭 | 关闭工单 |
| evaluate | 评价 | 工单评价 |
| report_fee | 填报费用 | 工单费用填报 |
| audit_fee | 费用审核 | 费用审核 |
| audit_quality | 质量审核 | 质量审核 |
| export | 导出 | 数据导出 |
| generate_qr | 生成二维码 | 二维码生成 |
| scan_view | 扫码查看 | 扫码查看设备 |
| force_close | 强制闭环 | 逾期任务强制关闭 |
| verify | 核验 | 访客核验 |
五、认证授权流程
5.1 JWT Token 认证
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration:86400000}")
private long jwtExpiration;
private SecretKey secretKey;
@PostConstruct
public void init() {
this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(User user, UUID projectId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(user.getId().toString())
.claim("username", user.getUsername())
.claim("realName", user.getRealName())
.claim("projectId", projectId != null ? projectId.toString() : null)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public UUID getUserIdFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
return UUID.fromString(claims.getSubject());
}
public UUID getProjectIdFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
String projectId = claims.get("projectId", String.class);
return projectId != null ? UUID.fromString(projectId) : null;
}
}
5.2 项目上下文
public class ProjectContextHolder {
private static final ThreadLocal<UUID> PROJECT_CONTEXT = new ThreadLocal<>();
public static void setCurrentProjectId(UUID projectId) {
PROJECT_CONTEXT.set(projectId);
}
public static UUID getCurrentProjectId() {
return PROJECT_CONTEXT.get();
}
public static void clear() {
PROJECT_CONTEXT.remove();
}
}
@Component
public class ProjectContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String projectId = request.getHeader("X-Project-ID");
if (StringUtils.hasText(projectId)) {
try {
ProjectContextHolder.setCurrentProjectId(UUID.fromString(projectId));
} catch (IllegalArgumentException e) {
// 无效的Project ID格式
}
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ProjectContextHolder.clear();
}
}
5.3 权限检查
@Service
public class PermissionService {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
// 检查用户是否有指定权限
public boolean hasPermission(UUID userId, String permissionCode) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException("用户不存在"));
// 超级管理员拥有所有权限
if (isSuperAdmin(user)) {
return true;
}
// 检查用户角色是否包含该权限
for (Role role : user.getRoles()) {
if (!role.getEnabled()) {
continue;
}
for (Permission permission : role.getPermissions()) {
if (permission.getCode().equals(permissionCode)) {
return true;
}
}
}
return false;
}
// 检查用户是否有指定权限(带项目隔离)
public boolean hasPermission(UUID userId, UUID projectId, String permissionCode) {
// 超级管理员
if (isSuperAdmin(userId)) {
return true;
}
// 获取用户在该项目下的权限
List<Permission> permissions = permissionRepository
.findPermissionsByUserIdAndProjectId(userId, projectId);
return permissions.stream()
.anyMatch(p -> p.getCode().equals(permissionCode));
}
// 获取用户菜单
public List<Permission> getUserMenus(UUID userId, UUID projectId) {
List<Permission> permissions = permissionRepository
.findByUserIdAndProjectId(userId, projectId);
return permissions.stream()
.filter(p -> p.getType() == PermissionType.MENU)
.filter(Permission::getEnabled)
.sorted(Comparator.comparing(Permission::getSortOrder))
.collect(Collectors.toList());
}
}
六、数据权限设计
6.1 数据范围级别
| 数据范围 | 编码 | 说明 | SQL预设 |
|---|---|---|---|
| 全部 | ALL | 所有项目数据 | WHERE 1=1 |
| 项目 | PROJECT | 本项目数据 | WHERE project_id = $current_project |
| 部门 | DEPARTMENT | 本部门数据 | WHERE project_id = $current_project AND dept_id = $current_dept |
| 本人 | SELF | 仅本人数据 | WHERE assigned_to = $current_user |
6.2 数据权限工具类
public final class DataScopeHelper {
public static String getDataFilterSql(String tableAlias, String creatorField,
String deptField, String projectField) {
DataScopeInfo scope = DataScopeContext.getDataScopeInfo();
String prefix = tableAlias != null ? tableAlias + "." : "";
return switch (scope.getScope()) {
case ALL -> "1=1";
case PROJECT -> prefix + projectField + " = '" + scope.getProjectId() + "'";
case DEPARTMENT -> prefix + projectField + " = '" + scope.getProjectId() + "' " +
"AND " + prefix + deptField + " = '" + scope.getDepartmentId() + "'";
case SELF -> prefix + creatorField + " = '" + scope.getUserId() + "'";
};
}
public static boolean canAccessData(UUID dataProjectId, UUID dataDeptId, UUID dataCreatorId) {
DataScopeInfo scope = DataScopeContext.getDataScopeInfo();
return switch (scope.getScope()) {
case ALL -> true;
case PROJECT -> dataProjectId.equals(scope.getProjectId());
case DEPARTMENT -> dataProjectId.equals(scope.getProjectId()) &&
(dataDeptId == null || dataDeptId.equals(scope.getDepartmentId()));
case SELF -> dataCreatorId.equals(scope.getUserId());
};
}
}
七、状态驱动权限
7.1 工单状态与操作映射
public enum WorkOrderStatus {
CREATED("已创建"),
ASSIGNED("已分配"),
ACCEPTED("已接单"),
IN_PROGRESS("处理中"),
PENDING("待确认"),
COMPLETED("已完成"),
CLOSED("已关闭"),
SUSPENDED("已挂起"),
RETURNED("已退回");
// 获取该状态下可执行的操作
public Set<WorkOrderAction> getAllowedActions() {
return switch (this) {
case CREATED -> Set.of(WorkOrderAction.ASSIGN, WorkOrderAction.EDIT, WorkOrderAction.DELETE);
case ASSIGNED -> Set.of(WorkOrderAction.ACCEPT, WorkOrderAction.TRANSFER,
WorkOrderAction.SUSPEND, WorkOrderAction.RETURN);
case ACCEPTED -> Set.of(WorkOrderAction.START, WorkOrderAction.TRANSFER, WorkOrderAction.SUSPEND);
case IN_PROGRESS -> Set.of(WorkOrderAction.COMPLETE, WorkOrderAction.REPORT_FEE, WorkOrderAction.SUSPEND);
case PENDING -> Set.of(WorkOrderAction.COMPLETE, WorkOrderAction.AUDIT_QUALITY);
case COMPLETED -> Set.of(WorkOrderAction.EVALUATE, WorkOrderAction.CLOSE, WorkOrderAction.AUDIT_FEE);
case CLOSED -> Set.of();
case SUSPENDED -> Set.of(WorkOrderAction.RESUME);
case RETURNED -> Set.of(WorkOrderAction.ASSIGN, WorkOrderAction.DELETE);
};
}
}
public enum WorkOrderAction {
VIEW, CREATE, EDIT, DELETE,
ASSIGN, ACCEPT, START, COMPLETE, TRANSFER, CLOSE, SUSPEND, RESUME, RETURN,
EVALUATE, REPORT_FEE, AUDIT_FEE, AUDIT_QUALITY
}
7.2 权限校验增强
// WorkOrderServiceImpl.java
public WorkOrder assign(UUID workOrderId, UUID assigneeId, String assigneeName) {
WorkOrder workOrder = getWorkOrder(workOrderId);
UUID currentUserId = getCurrentUserId();
// 1. 状态校验
validateStatusTransition(workOrder.getStatus(), WorkOrderStatus.ASSIGNED);
// 2. 权限校验
if (!hasActionPermission(workOrder, WorkOrderAction.ASSIGN)) {
throw new PermissionDeniedException("无分配工单权限");
}
// 3. 数据权限校验
if (!canAccessWorkOrder(workOrder)) {
throw new PermissionDeniedException("无权访问该工单");
}
// 执行分配逻辑...
}
private boolean hasActionPermission(WorkOrder workOrder, WorkOrderAction action) {
// 1. 检查状态是否允许该操作
if (!workOrder.getStatus().getAllowedActions().contains(action)) {
return false;
}
// 2. 检查用户是否有该操作的权限编码
String permissionCode = "ops:work_order:" + action.name().toLowerCase();
return permissionService.hasPermission(getCurrentUserId(), getProjectId(), permissionCode);
}
private boolean canAccessWorkOrder(WorkOrder workOrder) {
DataScopeInfo scope = dataScopeContext.getDataScopeInfo();
return switch (scope.getScope()) {
case ALL -> true;
case PROJECT -> workOrder.getProjectId().equals(scope.getProjectId());
case DEPARTMENT -> workOrder.getDepartmentId().equals(scope.getDepartmentId());
case SELF -> workOrder.getAssigneeId().equals(scope.getUserId()) ||
workOrder.getCreatorId().equals(scope.getUserId());
};
}
八、访客凭证管理
8.1 访客凭证设计
@Entity
@Table(name = "auth_visitor_credential")
@Data
public class VisitorCredential {
@Id
private UUID id;
private UUID projectId;
private String visitorName;
private String visitorPhone;
private String visitorIdCard;
private String credentialCode;
private String qrCode;
private LocalDateTime expireTime;
private UUID spaceNodeId;
private String accessGates;
private CredentialStatus status;
private LocalDateTime firstUseTime;
private LocalDateTime lastUseTime;
private Integer useCount;
private UUID creatorId;
private String creatorName;
private LocalDateTime createdAt;
}
public enum CredentialStatus {
ACTIVE("有效"),
USED("已使用"),
EXPIRED("已过期"),
REVOKED("已撤销");
}
8.2 二维码生成与验证
@Service
public class VisitorCredentialService {
@Autowired
private VisitorCredentialRepository credentialRepository;
public VisitorCredential generateCredential(VisitorCredentialRequest request) {
VisitorCredential credential = new VisitorCredential();
credential.setId(UUID.randomUUID());
credential.setProjectId(request.getProjectId());
credential.setVisitorName(request.getVisitorName());
credential.setVisitorPhone(request.getVisitorPhone());
credential.setVisitorIdCard(request.getVisitorIdCard());
credential.setSpaceNodeId(request.getSpaceNodeId());
credential.setAccessGates(JsonUtils.toJson(request.getAccessGates()));
credential.setStatus(CredentialStatus.ACTIVE);
credential.setExpireTime(LocalDateTime.now().plusHours(24));
credential.setUseCount(0);
credential.setCreatorId(request.getCreatorId());
credential.setCreatedAt(LocalDateTime.now());
String qrContent = generateQrContent(credential);
credential.setQrCode(qrContent);
return credentialRepository.save(credential);
}
private String generateQrContent(VisitorCredential credential) {
String data = String.format("VC:%s:%d",
credential.getId(),
System.currentTimeMillis());
String signature = generateSignature(data);
return data + ":" + signature;
}
public CredentialVerifyResult verifyCredential(String qrCode, String gateCode) {
String[] parts = qrCode.split(":");
if (parts.length != 4 || !"VC".equals(parts[0])) {
return CredentialVerifyResult.fail("无效的二维码格式");
}
UUID credentialId = UUID.fromString(parts[1]);
VisitorCredential credential = credentialRepository.findById(credentialId)
.orElse(null);
if (credential == null) {
return CredentialVerifyResult.fail("凭证不存在");
}
if (credential.getStatus() == CredentialStatus.EXPIRED ||
credential.getExpireTime().isBefore(LocalDateTime.now())) {
return CredentialVerifyResult.fail("凭证已过期");
}
if (credential.getStatus() == CredentialStatus.REVOKED) {
return CredentialVerifyResult.fail("凭证已撤销");
}
List<String> accessGates = JsonUtils.fromJson(credential.getAccessGates(), List.class);
if (!accessGates.contains(gateCode)) {
return CredentialVerifyResult.fail("无此门禁通行权限");
}
credential.setUseCount(credential.getUseCount() + 1);
credential.setLastUseTime(LocalDateTime.now());
if (credential.getFirstUseTime() == null) {
credential.setFirstUseTime(LocalDateTime.now());
}
credentialRepository.save(credential);
return CredentialVerifyResult.success(credential);
}
}
九、审计日志
9.1 审计日志设计
注意: 实际代码中操作日志已更名为审计日志(AuditLog),表名从
auth_operation_log改为sys_audit_log,字段结构也有较大变更,增加了目标追踪(targetType/targetId)、操作内容(content)、操作结果(result)、租户ID(tenantId)等字段,并采用异步持久化。
@Entity
@Table(name = "sys_audit_log")
@Data
public class AuditLog {
@Id
private UUID id;
private UUID userId;
private String username; // 操作用户名
private String operation; // 操作描述
private String module; // 模块标识
private ActionType action; // 操作类型枚举
// 目标追踪(新增)
private String targetType; // 目标类型
private String targetId; // 目标ID
// 操作详情(替代原 requestBody/responseBody)
private String content; // 操作内容
private String params; // 请求参数
private String result; // 操作结果
private String ipAddress;
private String userAgent;
private String requestUrl;
private String requestMethod;
private Integer executionTimeMs; // 执行耗时(ms)
private AuditStatus status; // SUCCESS/FAIL
private String errorMsg; // 错误信息
private UUID tenantId; // 租户ID(新增)
private LocalDateTime createdAt;
}
public enum ActionType {
CREATE, UPDATE, DELETE, QUERY, VIEW,
LOGIN, LOGOUT, EXPORT, IMPORT, ASSIGN, REVOKE;
}
public enum AuditStatus {
SUCCESS("成功"),
FAIL("失败");
}
9.2 日志记录注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
String operation();
String module();
AuditLog.ActionType action();
}
业务规则:
- 审计日志通过
@OperationLog注解 + AOP 切面自动记录- 采用异步持久化(
auditLogExecutor线程池),避免影响业务性能- 查询窗口强制限制30天,超过30天的查询自动截断起始时间
- 归档策略:90天以上日志归档(当前实现为直接删除,TODO: 导出至对象存储)
- 记录模块:USER / ROLE / PERMISSION / PROJECT / AUTH / DEPT / PROJECT_MEMBER / SYSTEM
十、API 接口
注意: 实际代码中 API 路径已去掉 v1 版本号,统一为
/api/auth前缀。
10.1 认证接口
@RestController
@RequestMapping("/api/auth")
@Tag(name = "认证管理")
public class AuthController {
@PostMapping("/login")
@Operation(summary = "用户登录")
public Result<LoginResponse> login(@RequestBody @Valid LoginRequest request);
@PostMapping("/logout")
@Operation(summary = "用户登出")
public Result<Void> logout();
@PostMapping("/refresh")
@Operation(summary = "刷新Token")
public Result<TokenResponse> refreshToken();
@GetMapping("/me")
@Operation(summary = "获取当前用户信息")
public Result<UserVO> getCurrentUser();
}
登录流程增强(实际代码新增):
- 检查登录失败锁定(Redis,5次失败锁定10分钟)
- 查询用户(含角色)
- 验证密码(BCrypt)
- 检查用户状态(LOCKED/DISABLED拒绝)
- 收集所有角色(用户直接角色 + 项目员工角色)
- 生成JWT Token(含userId、username、roles claims)
- 返回Token和用户基本信息
10.2 用户管理接口
@RestController
@RequestMapping("/api/auth/users")
@Tag(name = "用户管理")
public class UserController {
@PostMapping
@Operation(summary = "创建用户")
public Result<UserVO> create(@RequestBody @Valid UserCreateRequest request);
@GetMapping("/{id}")
@Operation(summary = "获取用户详情")
public Result<UserVO> getById(@PathVariable UUID id);
@GetMapping
@Operation(summary = "分页查询用户")
public Result<Page<UserVO>> page(UserQueryRequest request);
@PutMapping("/{id}")
@Operation(summary = "更新用户")
public Result<UserVO> update(@PathVariable UUID id, @RequestBody @Valid UserUpdateRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "删除用户")
public Result<Void> delete(@PathVariable UUID id);
@PutMapping("/{id}/password")
@Operation(summary = "修改密码")
public Result<Void> changePassword(@PathVariable UUID id, @RequestBody @Valid ChangePasswordRequest request);
@PostMapping("/{id}/roles")
@Operation(summary = "分配角色")
public Result<Void> assignRoles(@PathVariable UUID id, @RequestBody UserRoleAssignRequest request);
@GetMapping("/{id}/projects")
@Operation(summary = "获取用户项目列表")
public Result<List<UserProjectVO>> getUserProjects(@PathVariable UUID id);
@PostMapping("/{id}/projects")
@Operation(summary = "添加用户到项目")
public Result<Void> addUserToProject(@PathVariable UUID id, @RequestBody UserProjectRequest request);
@DeleteMapping("/{id}/projects/{projectId}")
@Operation(summary = "从项目移除用户")
public Result<Void> removeUserFromProject(@PathVariable UUID id, @PathVariable UUID projectId);
@GetMapping("/enterprise")
@Operation(summary = "获取企业员工列表")
public Result<List<EnterpriseUserVO>> getEnterpriseUsers();
}
密码修改规则(实际代码新增):
- 需验证原密码
- 新密码需通过强度校验(8-20位,大小写+数字+特殊字符)
- 弱密码检测(黑名单:password、123456、admin等)
10.3 角色权限接口
@RestController
@RequestMapping("/api/auth/roles")
@Tag(name = "角色管理")
public class RoleController {
@PostMapping
@Operation(summary = "创建角色")
public Result<RoleVO> createRole(@RequestBody @Valid RoleCreateRequest request);
@GetMapping
@Operation(summary = "分页查询角色列表")
public Result<Page<RoleVO>> listRoles();
@GetMapping("/{id}")
@Operation(summary = "获取角色详情")
public Result<RoleVO> getRoleById(@PathVariable UUID id);
@GetMapping("/project/{projectId}")
@Operation(summary = "根据项目ID查询角色")
public Result<List<RoleVO>> getRolesByProject(@PathVariable UUID projectId);
@PutMapping("/{id}")
@Operation(summary = "更新角色")
public Result<RoleVO> updateRole(@PathVariable UUID id, @RequestBody @Valid RoleUpdateRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "删除角色")
public Result<Void> deleteRole(@PathVariable UUID id);
@PostMapping("/{id}/permissions")
@Operation(summary = "分配权限")
public Result<Void> assignPermissions(@PathVariable UUID id, @RequestBody RolePermissionAssignRequest request);
@GetMapping("/{id}/permissions")
@Operation(summary = "获取角色权限列表")
public Result<List<PermissionVO>> getRolePermissions(@PathVariable UUID id);
@GetMapping("/{id}/users")
@Operation(summary = "获取拥有某角色的用户")
public Result<List<UserVO>> getRoleUsers(@PathVariable UUID id);
}
10.4 权限管理接口
@RestController
@RequestMapping("/api/auth/permissions")
@Tag(name = "权限管理")
public class PermissionController {
@PostMapping
@Operation(summary = "创建权限")
public Result<PermissionVO> createPermission(@RequestBody @Valid PermissionCreateRequest request);
@GetMapping
@Operation(summary = "分页查询权限列表")
public Result<Page<PermissionVO>> listPermissions();
@GetMapping("/{id}")
@Operation(summary = "获取权限详情")
public Result<PermissionVO> getPermissionById(@PathVariable UUID id);
@GetMapping("/type/{type}")
@Operation(summary = "根据类型查询权限")
public Result<List<PermissionVO>> getPermissionsByType(@PathVariable String type);
@GetMapping("/menus")
@Operation(summary = "查询所有菜单权限")
public Result<List<PermissionVO>> listMenuPermissions();
@PutMapping("/{id}")
@Operation(summary = "更新权限")
public Result<PermissionVO> updatePermission(@PathVariable UUID id, @RequestBody @Valid PermissionUpdateRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "删除权限")
public Result<Void> deletePermission(@PathVariable UUID id);
}
10.5 部门管理接口(新增)
@RestController
@RequestMapping("/api/auth/depts")
@Tag(name = "部门管理")
public class DeptController {
@GetMapping("/tree")
@Operation(summary = "获取部门树")
public Result<List<DeptTreeVO>> getDeptTree();
@GetMapping
@Operation(summary = "获取所有启用部门")
public Result<List<DeptVO>> listActiveDepts();
@GetMapping("/{id}")
@Operation(summary = "获取部门详情")
public Result<DeptVO> getById(@PathVariable UUID id);
@PostMapping
@Operation(summary = "创建部门")
public Result<DeptVO> create(@RequestBody @Valid DeptCreateRequest request);
@PutMapping("/{id}")
@Operation(summary = "更新部门")
public Result<DeptVO> update(@PathVariable UUID id, @RequestBody @Valid DeptUpdateRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "删除部门")
public Result<Void> delete(@PathVariable UUID id);
@GetMapping("/{deptId}/members")
@Operation(summary = "获取部门成员")
public Result<List<UserVO>> getDeptMembers(@PathVariable UUID deptId);
@GetMapping("/by-type/{deptType}")
@Operation(summary = "根据类型查询部门")
public Result<List<DeptVO>> getDeptsByType(@PathVariable String deptType);
}
部门类型: ADMIN(行政管理) / ENGINEERING(工程部) / SECURITY(安保部) / CS(客服部) / CLEANING(保洁部)
10.6 项目成员管理接口(新增)
@RestController
@RequestMapping("/api/auth/projects")
@Tag(name = "项目成员管理")
public class ProjectMemberController {
@GetMapping("/{projectId}/members")
@Operation(summary = "查询项目成员列表(分页)")
public Result<Page<ProjectStaffVO>> listMembers(@PathVariable UUID projectId);
@GetMapping("/{projectId}/available-members")
@Operation(summary = "获取可添加成员(企业员工)")
public Result<List<EnterpriseUserVO>> getAvailableMembers(@PathVariable UUID projectId);
@PostMapping("/{projectId}/members")
@Operation(summary = "添加项目成员")
public Result<Void> addMember(@PathVariable UUID projectId, @RequestBody AddMemberRequest request);
@DeleteMapping("/{projectId}/members/{userId}")
@Operation(summary = "移除项目成员")
public Result<Void> removeMember(@PathVariable UUID projectId, @PathVariable UUID userId);
}
10.7 数据访问授权接口(新增,接口预留)
@RestController
@RequestMapping("/api/data-access")
@Tag(name = "数据访问授权")
public class DataAccessController {
@PostMapping
@Operation(summary = "授予数据访问权限")
public Result<DataAccessVO> grantAccess(@RequestBody @Valid DataAccessGrantRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "撤销数据访问权限")
public Result<Void> revokeAccess(@PathVariable UUID id);
@GetMapping
@Operation(summary = "查询数据访问记录")
public Result<List<DataAccessVO>> queryAccess(@RequestParam String dataType, @RequestParam UUID dataId);
}
10.8 审计日志接口(新增)
@RestController
@RequestMapping("/api/audit-logs")
@Tag(name = "审计日志")
public class AuditLogController {
@GetMapping
@Operation(summary = "分页查询审计日志")
public Result<Page<AuditLogVO>> page(AuditLogQueryRequest request);
@GetMapping("/modules")
@Operation(summary = "获取模块列表")
public Result<List<String>> getModules();
@GetMapping("/actions")
@Operation(summary = "获取操作类型列表")
public Result<List<String>> getActions();
@GetMapping("/stats")
@Operation(summary = "获取最近30天日志统计")
public Result<AuditStatsVO> getStats();
}
查询约束: 强制限制查询范围不超过30天,防止大量数据查询。
10.9 系统配置接口(新增)
@RestController
@RequestMapping("/api/config")
@Tag(name = "系统配置")
public class SysConfigController {
@GetMapping
@Operation(summary = "获取所有配置项")
public Result<List<SysConfigVO>> listConfigs();
@GetMapping("/{configKey}")
@Operation(summary = "根据键获取配置")
public Result<SysConfigVO> getConfig(@PathVariable String configKey);
@PutMapping("/{configKey}")
@Operation(summary = "更新单个配置")
public Result<Void> updateConfig(@PathVariable String configKey, @RequestBody String configValue);
@PutMapping
@Operation(summary = "批量更新配置")
public Result<Void> batchUpdateConfigs(@RequestBody List<SysConfigUpdateRequest> requests);
}
十一、实现状态与差异
11.1 实现状态
| 功能模块 | 实现状态 | 备注 |
|---|---|---|
| User | 🟢 已实现 | 基础CRUD,含 userType/deptId 扩展字段 |
| Role | 🟢 已实现 | 基础CRUD,三级分类(SYSTEM/PROJECT/DEPARTMENT),RoleStatus枚举 |
| Permission | 🟢 已实现 | 基础CRUD,简化结构(parentCode替代parentId) |
| JWT认证 | 🟢 已实现 | Token生成/验证,含登录失败锁定(Redis) |
| 项目上下文 | 🟢 已实现 | 项目隔离 |
| 审计日志 | 🟢 已实现 | AuditLog(原OperationLog),异步持久化,30天查询窗口 |
| 部门管理 | 🟢 已实现 | 树形结构,5种部门类型 |
| 项目成员管理 | 🟢 已实现 | ProjectStaff + ProjectStaffRole |
| 企业员工 | 🟢 已实现 | EnterpriseUser扩展实体 |
| 住户管理 | 🟢 已实现 | Resident + ResidentSpace + Space |
| 用户项目关联 | 🟢 已实现 | UserProject多对多 |
| 系统配置 | 🟢 已实现 | SysConfig键值对 |
| 数据访问授权 | 🟢 已实现 | DataAccess接口预留 |
| 登录失败锁定 | 🟢 已实现 | Redis实现,5次失败锁10分钟 |
| 密码强度校验 | 🟢 已实现 | 8-20位+大小写数字特殊字符+弱密码检测 |
| 数据权限 | 🟡 部分实现 | DataScopeService提供判断方法,未实现SQL自动注入 |
| 按钮级权限 | ⏳ 待实现 | 细粒度操作权限 |
| 状态驱动权限 | ⏳ 待实现 | 根据业务状态控制操作 |
| 字段脱敏 | ⏳ 待实现 | 敏感字段脱敏 |
| 访客凭证 | 🔴 未实现 | 原设计有但未在认证模块实现 |
| 角色业务属性 | 🔴 未实现 | businessType/terminalType/isFrontline未实现 |
| 权限树端点 | 🔴 未实现 | GET /permissions/tree |
| 权限校验端点 | 🔴 未实现 | POST /check |
| 用户菜单端点 | 🔴 未实现 | GET /users/{id}/menus |
| 用户权限查询端点 | 🔴 未实现 | GET /users/{id}/permissions |
| Permission菜单路由属性 | 🔴 未实现 | path/component/icon字段缺失 |
11.2 改进计划
| 优先级 | 改进项 | 说明 |
|---|---|---|
| P0 | Permission菜单路由属性 | 增加 path/component/icon/module/action 字段,支持前端菜单渲染 |
| P0 | 权限树端点 | 实现 GET /permissions/tree |
| P0 | 用户菜单端点 | 实现 GET /users/{id}/menus |
| P0 | 权限校验端点 | 实现 POST /check |
| P1 | 数据权限完善 | 实现SQL自动注入,补充PROJECT级别 |
| P1 | 角色业务属性 | 新增 businessType/terminalType/isFrontline 字段 |
| P1 | 按钮级权限控制 | 新增操作按钮权限编码 |
| P1 | 状态驱动权限 | 工单状态与操作联动 |
| P2 | 字段脱敏 | 敏感字段脱敏 |
| P2 | 访客凭证 | 在认证模块实现访客凭证管理 |
| P2 | 审计日志归档 | 导出至对象存储替代直接删除 |
十二、数据库表结构
-- 用户表
CREATE TABLE auth_user (
id UUID PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
salt VARCHAR(20) NOT NULL,
real_name VARCHAR(100),
phone VARCHAR(20),
email VARCHAR(100),
avatar VARCHAR(255),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
last_login_time TIMESTAMP,
last_login_ip VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_by UUID,
UNIQUE(username)
);
-- 角色表
CREATE TABLE auth_role (
id UUID PRIMARY KEY,
project_id UUID, -- NULL表示系统级角色
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL,
description VARCHAR(500),
type VARCHAR(20) NOT NULL DEFAULT 'CUSTOM',
data_scope VARCHAR(20) DEFAULT 'SELF',
business_type VARCHAR(30), -- 新增:业务类型
terminal_type VARCHAR(20), -- 新增:终端类型
is_frontline BOOLEAN DEFAULT FALSE, -- 新增:是否一线人员
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(project_id, code)
);
-- 权限表
CREATE TABLE auth_permission (
id UUID PRIMARY KEY,
project_id UUID, -- NULL表示系统级权限
name VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL, -- module:resource:action
type VARCHAR(20) NOT NULL,
module VARCHAR(50), -- 新增:模块标识
resource VARCHAR(50), -- 新增:资源类型
action VARCHAR(30), -- 新增:操作类型
path VARCHAR(255),
component VARCHAR(255),
icon VARCHAR(50),
sort_order INTEGER DEFAULT 0,
parent_id UUID,
level INTEGER DEFAULT 0,
api_method VARCHAR(10),
api_path VARCHAR(255),
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(code, project_id)
);
-- 用户角色关联表
CREATE TABLE auth_user_role (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
role_id UUID NOT NULL,
project_id UUID,
is_default BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(user_id, role_id, project_id)
);
-- 角色权限关联表
CREATE TABLE auth_role_permission (
id UUID PRIMARY KEY,
role_id UUID NOT NULL,
permission_id UUID NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(role_id, permission_id)
);
-- 项目表
CREATE TABLE auth_project (
id UUID PRIMARY KEY,
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL,
description VARCHAR(500),
province VARCHAR(50),
city VARCHAR(50),
district VARCHAR(50),
address VARCHAR(255),
manager_id UUID,
manager_name VARCHAR(100),
manager_phone VARCHAR(20),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
start_date DATE,
end_date DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(code)
);
-- 访客凭证表
CREATE TABLE auth_visitor_credential (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
visitor_name VARCHAR(100) NOT NULL,
visitor_phone VARCHAR(20),
visitor_id_card VARCHAR(18),
credential_code VARCHAR(50) NOT NULL,
qr_code TEXT NOT NULL,
expire_time TIMESTAMP NOT NULL,
space_node_id UUID,
access_gates JSONB,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
first_use_time TIMESTAMP,
last_use_time TIMESTAMP,
use_count INTEGER DEFAULT 0,
creator_id UUID,
creator_name VARCHAR(100),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(credential_code)
);
-- 操作日志表
CREATE TABLE auth_operation_log (
id UUID PRIMARY KEY,
project_id UUID,
user_id UUID,
username VARCHAR(50),
real_name VARCHAR(100),
module VARCHAR(50) NOT NULL,
operation VARCHAR(50) NOT NULL,
description VARCHAR(255),
request_method VARCHAR(10),
request_url VARCHAR(255),
request_params TEXT,
request_body TEXT,
response_status INTEGER,
response_body TEXT,
execution_time BIGINT,
ip_address VARCHAR(50),
user_agent TEXT,
has_error BOOLEAN DEFAULT FALSE,
error_message TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 创建索引
CREATE INDEX idx_user_username ON auth_user(username);
CREATE INDEX idx_user_status ON auth_user(status);
CREATE INDEX idx_role_project ON auth_role(project_id);
CREATE INDEX idx_permission_parent ON auth_permission(parent_id);
CREATE INDEX idx_permission_module ON auth_permission(module);
CREATE INDEX idx_user_role_user ON auth_user_role(user_id);
CREATE INDEX idx_user_role_role ON auth_user_role(role_id);
CREATE INDEX idx_role_permission_role ON auth_role_permission(role_id);
CREATE INDEX idx_credential_project ON auth_visitor_credential(project_id);
CREATE INDEX idx_credential_status ON auth_visitor_credential(status);
CREATE INDEX idx_operation_log_user ON auth_operation_log(user_id);
CREATE INDEX idx_operation_log_created ON auth_operation_log(created_at);
十三、更新记录
| 日期 | 更新内容 | 更新人 |
|---|---|---|
| 2026-02-10 | 创建权限与账户领域技术方案 | - |
| 2026-02-27 | 重构角色体系,新增13个角色 | - |
| 2026-02-27 | 新增权限编码规范 | - |
| 2026-02-27 | 新增数据权限四级范围 | - |
| 2026-02-27 | 新增状态驱动权限设计 | - |
| 2026-02-27 | 新增角色业务属性字段 | - |
| 2026-04-26 | 反向同步实际代码实现:Role.type改为SYSTEM/PROJECT/DEPARTMENT三级分类;Role.status改为RoleStatus枚举;Permission结构简化(parentCode替代parentId);UserRole简化为M2M中间表;OperationLog更名为AuditLog;新增实体(EnterpriseUser/ProjectStaff/ProjectStaffRole/Resident/ResidentSpace/Space/UserProject/DataAccess/Dept/SysConfig);新增API(部门管理/项目成员管理/数据访问授权/审计日志/系统配置/企业员工/用户项目关联);新增业务规则(登录失败锁定/密码强度校验/审计日志增强/项目成员角色/部门类型/住户认证);角色体系改为数据库动态管理;API路径去掉v1版本号;标注TODO: Permission需增加菜单路由属性 | - |
文档维护: 本领域技术方案由 ether-auth 服务负责人维护