Skip to content

使用 NENE2 构建群组成员管理系统

本指南介绍如何构建群组系统,用户可以创建群组、邀请具有不同角色(owner/admin/member)的成员、管理成员资格并控制角色晋升。

字段试验:FT138 NENE2 版本:^1.5 涵盖主题:基于角色的成员资格,所有者自动加入,自主离开,MySQL 保留字陷阱(groups),漏洞评估


我们要构建的内容

  • POST /groups — 创建群组(创建者成为所有者)
  • GET /groups/{groupId}/members — 列出成员(仅成员可查)
  • POST /groups/{groupId}/members — 添加成员(仅所有者/管理员,角色:member 或 admin)
  • DELETE /groups/{groupId}/members/{userId} — 移除成员(所有者/管理员可移除他人;任何人可主动离开)
  • PUT /groups/{groupId}/members/{userId}/role — 更改角色(仅所有者)

数据库结构——重要:避免使用 groups 作为表名

groupsMySQL 的保留字(用于 GROUP BY)。请改用 user_groups

sql
-- SQLite
CREATE TABLE user_groups (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    name       TEXT    NOT NULL,
    owner_id   INTEGER NOT NULL,
    created_at TEXT    NOT NULL,
    FOREIGN KEY (owner_id) REFERENCES users(id)
);

CREATE TABLE memberships (
    id        INTEGER PRIMARY KEY AUTOINCREMENT,
    group_id  INTEGER NOT NULL,
    user_id   INTEGER NOT NULL,
    role      TEXT    NOT NULL DEFAULT 'member',
    joined_at TEXT    NOT NULL,
    UNIQUE (group_id, user_id),
    CHECK (role IN ('owner', 'admin', 'member')),
    FOREIGN KEY (group_id) REFERENCES user_groups(id),
    FOREIGN KEY (user_id)  REFERENCES users(id)
);
sql
-- MySQL
CREATE TABLE user_groups (
    id         INT          NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name       VARCHAR(255) NOT NULL,
    owner_id   INT          NOT NULL,
    created_at VARCHAR(32)  NOT NULL,
    FOREIGN KEY (owner_id) REFERENCES users(id)
) ENGINE=InnoDB;

CREATE TABLE memberships (
    id        INT         NOT NULL AUTO_INCREMENT PRIMARY KEY,
    group_id  INT         NOT NULL,
    user_id   INT         NOT NULL,
    role      VARCHAR(16) NOT NULL DEFAULT 'member',
    joined_at VARCHAR(32) NOT NULL,
    UNIQUE KEY uq_group_user (group_id, user_id),
    CONSTRAINT chk_role CHECK (role IN ('owner', 'admin', 'member')),
    FOREIGN KEY (group_id) REFERENCES user_groups(id),
    FOREIGN KEY (user_id)  REFERENCES users(id)
) ENGINE=InnoDB;

带能力方法的角色枚举

php
enum MemberRole: string
{
    case Owner  = 'owner';
    case Admin  = 'admin';
    case Member = 'member';

    public function canManageMembers(): bool
    {
        return $this === self::Owner || $this === self::Admin;
    }

    public function canChangeRoles(): bool
    {
        return $this === self::Owner;
    }
}

枚举上的能力方法将授权逻辑从处理程序中分离出去。


创建群组时所有者自动加入

创建群组时,所有者以 owner 角色自动添加为成员:

php
public function createGroup(string $name, int $ownerId, string $now): int
{
    $this->executor->execute(
        'INSERT INTO user_groups (name, owner_id, created_at) VALUES (?, ?, ?)',
        [$name, $ownerId, $now],
    );

    $groupId = (int) $this->executor->lastInsertId();

    // 所有者以 'owner' 角色自动成为成员
    $this->executor->execute(
        'INSERT INTO memberships (group_id, user_id, role, joined_at) VALUES (?, ?, ?, ?)',
        [$groupId, $ownerId, 'owner', $now],
    );

    return $groupId;
}

添加成员处理程序——角色校验

无法通过添加成员 API 分配 owner 角色。应用 TokenScope::tryFrom() 模式到 MemberRole::tryFrom()

php
$role = MemberRole::tryFrom($roleValue);

if ($role === null || $role === MemberRole::Owner) {
    return $this->responseFactory->create(['error' => 'role must be member or admin'], 422);
}

移除成员——主动离开与管理员移除

成员无需管理员权限即可主动离开群组。管理员可以移除他人。群主永远不能被移除:

php
$isSelfLeave = $actorId === $userId;

if (!$isSelfLeave && !$actorRole->canManageMembers()) {
    return $this->responseFactory->create(['error' => 'only owner or admin can remove members'], 403);
}

$targetRole = MemberRole::tryFrom($targetMembership['role']) ?? MemberRole::Member;

if ($targetRole === MemberRole::Owner) {
    return $this->responseFactory->create(['error' => 'cannot remove the group owner'], 422);
}

MySQL FK 清理——顺序很重要

在测试中重置 MySQL 时,使用 FOREIGN_KEY_CHECKS = 0 先删除依赖 FK 的表:

php
$this->pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
$this->pdo->exec('DROP TABLE IF EXISTS memberships');
$this->pdo->exec('DROP TABLE IF EXISTS user_groups');
$this->pdo->exec('DROP TABLE IF EXISTS users');
$this->pdo->exec('SET FOREIGN_KEY_CHECKS = 1');

漏洞评估(FT138)

十二项漏洞测试验证:

ID攻击期望结果
VULN-AIDOR:非成员读取成员列表403通过
VULN-BIDOR:非成员添加成员403通过
VULN-C普通成员尝试添加他人403通过
VULN-D管理员尝试设置 owner 角色非 200通过
VULN-E成员尝试将自己晋升为 admin403通过
VULN-F移除群主422通过
VULN-G创建时缺少 X-User-Id非 201通过
VULN-H非数字 X-User-Id非 200通过
VULN-I群组名称 SQL 注入201(原样存储)通过
VULN-J跨群组成员操作403通过
VULN-K负数群组 ID404通过
VULN-L管理员不能更改角色403通过

12 项漏洞测试全部通过。未发现漏洞。


常见陷阱

陷阱解决方案
在 MySQL 中使用 groups 作为表名使用 user_groups——groups 是 MySQL 保留字
所有者未自动添加到 membershipscreateGroup() 中 INSERT 所有者成员资格
管理员能够更改角色canChangeRoles() 仅对 Owner 返回 true
允许通过添加成员 API 分配 owner 角色拒绝 role === MemberRole::Owner → 422
非成员绕过 403(缺少操作者)检查 findMembership(groupId, actorId) !== null
MySQL DROP TABLE 因 FK 约束失败在 DROP 前使用 SET FOREIGN_KEY_CHECKS = 0

基于 MIT 许可证发布。