1. 为什么需要一套自己的 IAM 子用户体系
在单账号、单用户的系统里,权限往往就是「一个用户表 + 一个角色字段」。但当系统开始面向企业客户、开始出现「一个主账号下多个员工协作」的场景时,这套模型会迅速崩塌:
- 员工离职,主账号密码要不要改?
- 财务只想看账单,不想碰服务器,怎么隔离?
- 第三方服务商需要临时访问部分资源,怎么授权、怎么回收?
- 出了安全事故,怎么追溯「是谁在什么时候做了什么」?
阿里云的 RAM(Resource Access Management)给出的答案就是主账号 + 子用户(RAM User)+ 用户组 + 权限策略(Policy)+ 角色(Role) 这一整套模型。本文用 Java 从零实现一套同构的 IAM 子用户管理,重点讲清楚架构分层 和表结构设计,代码可直接落地。
2. 核心概念对齐:先统一语言
在动手之前,先把概念和阿里云 RAM 对齐,避免后面表设计时命名混乱。
| 概念 | 阿里云 RAM 对应 | 说明 |
|---|---|---|
| 主账号(Account) | Alibaba Cloud Account | 资源归属方,唯一,不可被删除 |
| 子用户(User) | RAM User | 归属于主账号,有独立的登录凭证 |
| 用户组(Group) | RAM User Group | 一批子用户的集合,用于批量授权 |
| 权限策略(Policy) | RAM Policy | JSON 描述的权限集合,分系统策略与自定义策略 |
| 授权(Grant) | Policy Attachment | 把策略绑定到用户/用户组/角色上 |
| 角色(Role) | RAM Role | 供可信实体扮演的身份,常用于跨账号与临时凭证 |
| 访问密钥(AccessKey) | AccessKey | 子用户调用 OpenAPI 的凭证,一对 AK/SK |
几个关键设计原则,直接决定了后面的表结构:
- 身份与凭证分离:用户是身份,AccessKey 是凭证,一个用户可以有多对 AK,且 AK 可独立禁用。
- 权限与身份解耦:权限不写在用户表里,而是通过「策略 + 授权关系」表达,这样才能做到策略复用。
- 授权对象多态:策略可以绑到用户、用户组、角色三种主体上,最终权限是三者并集。
- 一切可审计:所有写操作都要落操作日志。
3. 整体架构设计
3.1 分层架构
#mermaid-svg-UayUmJc5nUj6094s{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UayUmJc5nUj6094s .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UayUmJc5nUj6094s .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UayUmJc5nUj6094s .error-icon{fill:#552222;}#mermaid-svg-UayUmJc5nUj6094s .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UayUmJc5nUj6094s .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UayUmJc5nUj6094s .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UayUmJc5nUj6094s .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UayUmJc5nUj6094s .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UayUmJc5nUj6094s .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UayUmJc5nUj6094s .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UayUmJc5nUj6094s .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UayUmJc5nUj6094s .marker.cross{stroke:#333333;}#mermaid-svg-UayUmJc5nUj6094s svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UayUmJc5nUj6094s p{margin:0;}#mermaid-svg-UayUmJc5nUj6094s .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UayUmJc5nUj6094s .cluster-label text{fill:#333;}#mermaid-svg-UayUmJc5nUj6094s .cluster-label span{color:#333;}#mermaid-svg-UayUmJc5nUj6094s .cluster-label span p{background-color:transparent;}#mermaid-svg-UayUmJc5nUj6094s .label text,#mermaid-svg-UayUmJc5nUj6094s span{fill:#333;color:#333;}#mermaid-svg-UayUmJc5nUj6094s .node rect,#mermaid-svg-UayUmJc5nUj6094s .node circle,#mermaid-svg-UayUmJc5nUj6094s .node ellipse,#mermaid-svg-UayUmJc5nUj6094s .node polygon,#mermaid-svg-UayUmJc5nUj6094s .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UayUmJc5nUj6094s .rough-node .label text,#mermaid-svg-UayUmJc5nUj6094s .node .label text,#mermaid-svg-UayUmJc5nUj6094s .image-shape .label,#mermaid-svg-UayUmJc5nUj6094s .icon-shape .label{text-anchor:middle;}#mermaid-svg-UayUmJc5nUj6094s .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UayUmJc5nUj6094s .rough-node .label,#mermaid-svg-UayUmJc5nUj6094s .node .label,#mermaid-svg-UayUmJc5nUj6094s .image-shape .label,#mermaid-svg-UayUmJc5nUj6094s .icon-shape .label{text-align:center;}#mermaid-svg-UayUmJc5nUj6094s .node.clickable{cursor:pointer;}#mermaid-svg-UayUmJc5nUj6094s .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UayUmJc5nUj6094s .arrowheadPath{fill:#333333;}#mermaid-svg-UayUmJc5nUj6094s .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UayUmJc5nUj6094s .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UayUmJc5nUj6094s .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UayUmJc5nUj6094s .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UayUmJc5nUj6094s .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UayUmJc5nUj6094s .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UayUmJc5nUj6094s .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UayUmJc5nUj6094s .cluster text{fill:#333;}#mermaid-svg-UayUmJc5nUj6094s .cluster span{color:#333;}#mermaid-svg-UayUmJc5nUj6094s div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UayUmJc5nUj6094s .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UayUmJc5nUj6094s rect.text{fill:none;stroke-width:0;}#mermaid-svg-UayUmJc5nUj6094s .icon-shape,#mermaid-svg-UayUmJc5nUj6094s .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UayUmJc5nUj6094s .icon-shape p,#mermaid-svg-UayUmJc5nUj6094s .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UayUmJc5nUj6094s .icon-shape .label rect,#mermaid-svg-UayUmJc5nUj6094s .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UayUmJc5nUj6094s .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UayUmJc5nUj6094s .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UayUmJc5nUj6094s :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 接入层 Gateway
认证层 AuthN
鉴权层 AuthZ
领域服务层 IAM Service
数据访问层 Repository
存储 MySQL / Redis
审计日志 Audit
凭证服务 Credential
各层职责:
- 接入层:统一入口,负责限流、路由、协议转换(HTTP / OpenAPI)。
- 认证层(AuthN):解决「你是谁」。支持两种身份:控制台登录(用户名密码 + MFA)与 OpenAPI 调用(AccessKey 签名)。
- 鉴权层(AuthZ):解决「你能做什么」。基于策略引擎计算最终权限集合。
- 领域服务层:用户、用户组、策略、角色、授权的领域逻辑,事务边界在这一层。
- 数据访问层:屏蔽存储细节,提供聚合查询。
- 审计:所有变更操作异步落日志,不阻塞主流程。
3.2 一次请求的完整链路
"MySQL" "IAM Service" "AuthZ" "AuthN" "Gateway" "客户端" "MySQL" "IAM Service" "AuthZ" "AuthN" "Gateway" "客户端" #mermaid-svg-TA53m4mYn5ykCKIO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TA53m4mYn5ykCKIO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TA53m4mYn5ykCKIO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TA53m4mYn5ykCKIO .error-icon{fill:#552222;}#mermaid-svg-TA53m4mYn5ykCKIO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TA53m4mYn5ykCKIO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TA53m4mYn5ykCKIO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TA53m4mYn5ykCKIO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TA53m4mYn5ykCKIO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TA53m4mYn5ykCKIO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TA53m4mYn5ykCKIO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TA53m4mYn5ykCKIO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TA53m4mYn5ykCKIO .marker.cross{stroke:#333333;}#mermaid-svg-TA53m4mYn5ykCKIO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TA53m4mYn5ykCKIO p{margin:0;}#mermaid-svg-TA53m4mYn5ykCKIO .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-TA53m4mYn5ykCKIO text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-TA53m4mYn5ykCKIO .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-TA53m4mYn5ykCKIO .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-TA53m4mYn5ykCKIO .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-TA53m4mYn5ykCKIO .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-TA53m4mYn5ykCKIO #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-TA53m4mYn5ykCKIO .sequenceNumber{fill:white;}#mermaid-svg-TA53m4mYn5ykCKIO #sequencenumber{fill:#333;}#mermaid-svg-TA53m4mYn5ykCKIO #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-TA53m4mYn5ykCKIO .messageText{fill:#333;stroke:none;}#mermaid-svg-TA53m4mYn5ykCKIO .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-TA53m4mYn5ykCKIO .labelText,#mermaid-svg-TA53m4mYn5ykCKIO .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-TA53m4mYn5ykCKIO .loopText,#mermaid-svg-TA53m4mYn5ykCKIO .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-TA53m4mYn5ykCKIO .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-TA53m4mYn5ykCKIO .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-TA53m4mYn5ykCKIO .noteText,#mermaid-svg-TA53m4mYn5ykCKIO .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-TA53m4mYn5ykCKIO .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-TA53m4mYn5ykCKIO .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-TA53m4mYn5ykCKIO .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-TA53m4mYn5ykCKIO .actorPopupMenu{position:absolute;}#mermaid-svg-TA53m4mYn5ykCKIO .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-TA53m4mYn5ykCKIO .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-TA53m4mYn5ykCKIO .actor-man circle,#mermaid-svg-TA53m4mYn5ykCKIO line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-TA53m4mYn5ykCKIO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} "请求 + AccessKey 签名""校验签名与 AK 状态""查询 AK 与所属用户""返回用户与状态""身份上下文 Principal""Principal + Action + Resource""加载策略并计算权限""策略集合""Allow / Deny""放行后调用业务""读写业务数据""返回结果"
3.3 权限计算模型
最终权限 = 用户直接授权 ∪ 用户所在用户组授权 ∪ 角色授权,其中 显式 Deny 优先于 Allow。这是阿里云 RAM 的核心语义,实现时务必遵守。
java
public final class PermissionEvaluator {
public boolean isAllowed(Principal principal, String action, String resource) {
List<PolicyStatement> statements = new ArrayList<>();
statements.addAll(loadUserPolicies(principal.getUserId()));
statements.addAll(loadGroupPolicies(principal.getGroupIds()));
statements.addAll(loadRolePolicies(principal.getRoleIds()));
boolean allowed = false;
for (PolicyStatement st : statements) {
if (!st.matches(action, resource)) {
continue;
}
if (st.getEffect() == Effect.DENY) {
return false; // 显式拒绝优先
}
allowed = true;
}
return allowed;
}
}
4. 数据库表设计
下面给出核心表结构,以 MySQL 8.0 为例,字符集统一 utf8mb4。
4.1 主账号表 iam_account
sql
CREATE TABLE iam_account (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
account_id VARCHAR(64) NOT NULL COMMENT '账号唯一标识,如 1234567890',
account_name VARCHAR(128) NOT NULL COMMENT '账号名称',
status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0冻结',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_account_id (account_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主账号';
4.2 子用户表 iam_user
sql
CREATE TABLE iam_user (
id BIGINT NOT NULL AUTO_INCREMENT,
account_id VARCHAR(64) NOT NULL COMMENT '所属主账号',
user_id VARCHAR(64) NOT NULL COMMENT '子用户唯一标识',
user_name VARCHAR(128) NOT NULL COMMENT '登录名,账号内唯一',
display_name VARCHAR(128) DEFAULT NULL COMMENT '显示名',
email VARCHAR(128) DEFAULT NULL,
mobile VARCHAR(32) DEFAULT NULL,
password_hash VARCHAR(255) DEFAULT NULL COMMENT 'BCrypt 哈希,控制台登录用',
mfa_enabled TINYINT NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0禁用',
comment VARCHAR(255) DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_account_user (account_id, user_name),
UNIQUE KEY uk_user_id (user_id),
KEY idx_account (account_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='子用户';
注意:
password_hash允许为空,因为有些子用户只用于 OpenAPI 调用,不开启控制台登录。
4.3 用户组表 iam_group 与成员关系 iam_group_member
sql
CREATE TABLE iam_group (
id BIGINT NOT NULL AUTO_INCREMENT,
account_id VARCHAR(64) NOT NULL,
group_id VARCHAR(64) NOT NULL,
group_name VARCHAR(128) NOT NULL,
comment VARCHAR(255) DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_account_group (account_id, group_name),
UNIQUE KEY uk_group_id (group_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户组';
CREATE TABLE iam_group_member (
id BIGINT NOT NULL AUTO_INCREMENT,
group_id VARCHAR(64) NOT NULL,
user_id VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_group_user (group_id, user_id),
KEY idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户组成员关系';
4.4 权限策略表 iam_policy
sql
CREATE TABLE iam_policy (
id BIGINT NOT NULL AUTO_INCREMENT,
account_id VARCHAR(64) NOT NULL COMMENT '系统策略可为 SYSTEM',
policy_id VARCHAR(64) NOT NULL,
policy_name VARCHAR(128) NOT NULL,
policy_type VARCHAR(16) NOT NULL COMMENT 'SYSTEM / CUSTOM',
description VARCHAR(255) DEFAULT NULL,
document JSON NOT NULL COMMENT '策略文档',
version INT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_policy_id (policy_id),
KEY idx_account_type (account_id, policy_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限策略';
策略文档示例:
json
{
"version": "1",
"statement": [
{
"effect": "Allow",
"action": ["oss:GetObject", "oss:ListObjects"],
"resource": ["acs:oss:*:1234567890:bucket-a/*"]
},
{
"effect": "Deny",
"action": ["oss:DeleteObject"],
"resource": ["*"]
}
]
}
4.5 授权关系表 iam_policy_attachment
这是整个模型的关键表,用「主体类型 + 主体 ID」实现多态授权。
sql
CREATE TABLE iam_policy_attachment (
id BIGINT NOT NULL AUTO_INCREMENT,
account_id VARCHAR(64) NOT NULL,
policy_id VARCHAR(64) NOT NULL,
principal_type VARCHAR(16) NOT NULL COMMENT 'USER / GROUP / ROLE',
principal_id VARCHAR(64) NOT NULL COMMENT '对应 user_id / group_id / role_id',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_policy_principal (policy_id, principal_type, principal_id),
KEY idx_principal (principal_type, principal_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='策略授权关系';
4.6 访问密钥表 iam_access_key
sql
CREATE TABLE iam_access_key (
id BIGINT NOT NULL AUTO_INCREMENT,
account_id VARCHAR(64) NOT NULL,
user_id VARCHAR(64) NOT NULL,
access_key_id VARCHAR(64) NOT NULL,
secret_hash VARCHAR(255) NOT NULL COMMENT 'SK 加密存储,不可明文',
status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0禁用',
last_used_at DATETIME DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_ak (access_key_id),
KEY idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访问密钥';
安全要点:SK 必须用 KMS 或 AES-GCM 加密后存储,且只在创建时返回一次明文。
4.7 角色表 iam_role
sql
CREATE TABLE iam_role (
id BIGINT NOT NULL AUTO_INCREMENT,
account_id VARCHAR(64) NOT NULL,
role_id VARCHAR(64) NOT NULL,
role_name VARCHAR(128) NOT NULL,
trust_policy JSON NOT NULL COMMENT '信任策略,谁能扮演',
max_session_sec INT NOT NULL DEFAULT 3600,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_role_id (role_id),
UNIQUE KEY uk_account_role (account_id, role_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色';
4.8 操作审计表 iam_audit_log
sql
CREATE TABLE iam_audit_log (
id BIGINT NOT NULL AUTO_INCREMENT,
account_id VARCHAR(64) NOT NULL,
operator_id VARCHAR(64) NOT NULL COMMENT '操作者 user_id',
action VARCHAR(64) NOT NULL COMMENT '如 CreateUser',
target_type VARCHAR(32) DEFAULT NULL,
target_id VARCHAR(64) DEFAULT NULL,
request_ip VARCHAR(64) DEFAULT NULL,
detail JSON DEFAULT NULL,
result TINYINT NOT NULL DEFAULT 1 COMMENT '1成功 0失败',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_account_time (account_id, created_at),
KEY idx_operator (operator_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作审计日志';
4.9 表关系总览
#mermaid-svg-qcLQNvdZJUrIm3TD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qcLQNvdZJUrIm3TD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qcLQNvdZJUrIm3TD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qcLQNvdZJUrIm3TD .error-icon{fill:#552222;}#mermaid-svg-qcLQNvdZJUrIm3TD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qcLQNvdZJUrIm3TD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qcLQNvdZJUrIm3TD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qcLQNvdZJUrIm3TD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qcLQNvdZJUrIm3TD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qcLQNvdZJUrIm3TD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qcLQNvdZJUrIm3TD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qcLQNvdZJUrIm3TD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qcLQNvdZJUrIm3TD .marker.cross{stroke:#333333;}#mermaid-svg-qcLQNvdZJUrIm3TD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qcLQNvdZJUrIm3TD p{margin:0;}#mermaid-svg-qcLQNvdZJUrIm3TD .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-qcLQNvdZJUrIm3TD .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-qcLQNvdZJUrIm3TD .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-qcLQNvdZJUrIm3TD .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-qcLQNvdZJUrIm3TD .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-qcLQNvdZJUrIm3TD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qcLQNvdZJUrIm3TD .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-qcLQNvdZJUrIm3TD .node rect,#mermaid-svg-qcLQNvdZJUrIm3TD .node circle,#mermaid-svg-qcLQNvdZJUrIm3TD .node ellipse,#mermaid-svg-qcLQNvdZJUrIm3TD .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qcLQNvdZJUrIm3TD .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-qcLQNvdZJUrIm3TD .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-qcLQNvdZJUrIm3TD .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qcLQNvdZJUrIm3TD .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-qcLQNvdZJUrIm3TD .edgeLabel .label text{fill:#333;}#mermaid-svg-qcLQNvdZJUrIm3TD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 拥有
拥有
拥有
拥有
持有
加入
包含
被授权
被绑定
被绑定
被绑定
iam_account
iam_user
iam_group
iam_policy
iam_role
iam_access_key
iam_group_member
iam_policy_attachment
5. Java 实现核心代码
5.1 工程结构
text
iam-service
├── iam-api // 对外接口与 DTO
├── iam-domain // 领域模型与领域服务
├── iam-infrastructure // 持久化、缓存、KMS
└── iam-application // 应用服务、事务编排
5.2 领域实体
java
public class IamUser {
private Long id;
private String accountId;
private String userId;
private String userName;
private String displayName;
private String passwordHash;
private boolean mfaEnabled;
private UserStatus status;
public void disable() {
if (this.status == UserStatus.DISABLED) {
throw new IllegalStateException("用户已处于禁用状态");
}
this.status = UserStatus.DISABLED;
}
public void resetPassword(String rawPassword, PasswordEncoder encoder) {
this.passwordHash = encoder.encode(rawPassword);
}
}
5.3 创建子用户的应用服务
java
@Service
public class UserApplicationService {
private final IamUserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuditLogger auditLogger;
public UserApplicationService(IamUserRepository userRepository,
PasswordEncoder passwordEncoder,
AuditLogger auditLogger) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.auditLogger = auditLogger;
}
@Transactional
public String createUser(CreateUserCommand cmd, String operatorId) {
if (userRepository.existsByAccountAndName(cmd.getAccountId(), cmd.getUserName())) {
throw new BizException("用户名已存在");
}
IamUser user = new IamUser();
user.setAccountId(cmd.getAccountId());
user.setUserId(IdGenerator.uuid());
user.setUserName(cmd.getUserName());
user.setDisplayName(cmd.getDisplayName());
user.setStatus(UserStatus.ACTIVE);
if (cmd.isConsoleLogin()) {
user.resetPassword(cmd.getPassword(), passwordEncoder);
}
userRepository.save(user);
auditLogger.log(operatorId, "CreateUser", "USER", user.getUserId());
return user.getUserId();
}
}
5.4 策略文档解析与匹配
java
public class PolicyStatement {
private Effect effect;
private List<String> actions;
private List<String> resources;
public boolean matches(String action, String resource) {
return matchAny(actions, action) && matchAny(resources, resource);
}
private boolean matchAny(List<String> patterns, String value) {
for (String p : patterns) {
if ("*".equals(p) || p.equals(value)) {
return true;
}
if (p.endsWith("*") && value.startsWith(p.substring(0, p.length() - 1))) {
return true;
}
}
return false;
}
}
5.5 AccessKey 生成与校验
java
@Service
public class AccessKeyService {
private final IamAccessKeyRepository repository;
private final SecretCipher cipher;
public AccessKeyService(IamAccessKeyRepository repository, SecretCipher cipher) {
this.repository = repository;
this.cipher = cipher;
}
public AccessKeyPair create(String accountId, String userId) {
String ak = "LTAI" + RandomStringUtils.randomAlphanumeric(20);
String sk = RandomStringUtils.randomAlphanumeric(32);
IamAccessKey entity = new IamAccessKey();
entity.setAccountId(accountId);
entity.setUserId(userId);
entity.setAccessKeyId(ak);
entity.setSecretHash(cipher.encrypt(sk));
entity.setStatus(1);
repository.save(entity);
// 明文 SK 仅此一次返回
return new AccessKeyPair(ak, sk);
}
public boolean verify(String ak, String signature, String payload) {
IamAccessKey entity = repository.findByAccessKeyId(ak);
if (entity == null || entity.getStatus() != 1) {
return false;
}
String sk = cipher.decrypt(entity.getSecretHash());
String expected = HmacUtils.hmacSha256Hex(sk, payload);
return MessageDigest.isEqual(expected.getBytes(), signature.getBytes());
}
}
6. 关键设计要点与踩坑记录
- 权限缓存:策略计算是热点路径,建议把「用户 → 策略集合」缓存到 Redis,授权变更时主动失效,TTL 兜底 5 分钟。
- AK 泄露应急 :提供「一键禁用 AK」接口,并支持按
last_used_at清理长期未使用的密钥。 - 删除用户 :不要物理删除,改为状态置为
DELETED,否则审计日志里的operator_id会变成孤儿数据。 - 越权防护 :所有查询必须带
account_id条件,防止跨账号访问,这是多租户系统最容易出的漏洞。 - 策略文档校验 :写入前用 JSON Schema 校验
document字段,避免脏数据导致鉴权引擎异常。 - 并发授权 :
iam_policy_attachment的唯一索引天然防重,插入时用INSERT IGNORE或捕获唯一键冲突。
7. 总结
本文从阿里云 RAM 的模型出发,给出了一套可落地的 Java IAM 子用户管理实现:认证与鉴权分层、身份与凭证分离、策略与授权解耦 。表结构上以 iam_user、iam_policy、iam_policy_attachment 三张表为核心,配合用户组、角色、AccessKey 与审计日志,即可覆盖绝大多数企业级权限场景。
后续可以继续扩展的方向:策略可视化编辑器、临时凭证 STS、跨账号角色扮演、权限边界(Permission Boundary)。这些都是在上述模型上的自然延伸,而不需要推翻重来。