用 Java 实现阿里云式 IAM 子用户管理:架构设计与表结构详解

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

几个关键设计原则,直接决定了后面的表结构:

  1. 身份与凭证分离:用户是身份,AccessKey 是凭证,一个用户可以有多对 AK,且 AK 可独立禁用。
  2. 权限与身份解耦:权限不写在用户表里,而是通过「策略 + 授权关系」表达,这样才能做到策略复用。
  3. 授权对象多态:策略可以绑到用户、用户组、角色三种主体上,最终权限是三者并集。
  4. 一切可审计:所有写操作都要落操作日志。

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. 关键设计要点与踩坑记录

  1. 权限缓存:策略计算是热点路径,建议把「用户 → 策略集合」缓存到 Redis,授权变更时主动失效,TTL 兜底 5 分钟。
  2. AK 泄露应急 :提供「一键禁用 AK」接口,并支持按 last_used_at 清理长期未使用的密钥。
  3. 删除用户 :不要物理删除,改为状态置为 DELETED,否则审计日志里的 operator_id 会变成孤儿数据。
  4. 越权防护 :所有查询必须带 account_id 条件,防止跨账号访问,这是多租户系统最容易出的漏洞。
  5. 策略文档校验 :写入前用 JSON Schema 校验 document 字段,避免脏数据导致鉴权引擎异常。
  6. 并发授权 :iam_policy_attachment 的唯一索引天然防重,插入时用 INSERT IGNORE 或捕获唯一键冲突。

7. 总结

本文从阿里云 RAM 的模型出发,给出了一套可落地的 Java IAM 子用户管理实现:认证与鉴权分层、身份与凭证分离、策略与授权解耦 。表结构上以 iam_user、iam_policy、iam_policy_attachment 三张表为核心,配合用户组、角色、AccessKey 与审计日志,即可覆盖绝大多数企业级权限场景。

后续可以继续扩展的方向:策略可视化编辑器、临时凭证 STS、跨账号角色扮演、权限边界(Permission Boundary)。这些都是在上述模型上的自然延伸,而不需要推翻重来。

相关推荐
用户2986985301438 分钟前
Python 将 Word 文档转换为 XML 的实践要点
后端·python·api
2601_9520477941 分钟前
金蝶云星空销售订单附件查询接口权威教程(BOS_Attachment)
python·机器人·钉钉
神仙别闹44 分钟前
基于Python Yolo实现绝缘子目标检测程序
python·yolo·目标检测
超级架构师1 小时前
让 COBOL 长出 AI 大脑:原生 Agent 核心框架 praxo-core 深度剖析
c语言·开发语言·c++·人工智能·python·架构·ai编程
甜到心里的蛋糕1 小时前
抖音评论自动回复机器人部署实践:无头 Chrome + DeepSeek + NapCat(2C2G 云主机)
java·jvm·chrome·python·抖音·deepseek
Java爱好狂.1 小时前
阿里1658页Java面试题总结(含答案)
java·后端·高并发·架构师·java面试·java面试题·java八股文
Thanks_ks1 小时前
从 0.1+0.2 不等于 0.3 说起:聊聊浮点数精度问题
java·python·数值计算·精度丢失·bigdecimal·浮点数·ieee754
(●—●)橘子……1 小时前
力扣 第525场周赛 练习理解
数据结构·python·学习·算法·leetcode
海鸥-w1 小时前
区域名称同步存储实现笔记
java·笔记