CatchAdmin PHP 后台管理框架 Logo CatchAdmin

Laravel 中 RBAC 的实战深度解析

引言

授权问题通常开始得很朴素。

也许是先加一个 is_admin 字段,再添上几条 if,一切看起来都挺好。但随着应用规模变大,这种简朴会反过来成为负担。

现在需要一位可以处理账单但不能删除项目的计费经理;需要可以邀请成员但范围只限于自己团队的团队管理员;还需要可以读取所有内容但不能修改任何内容的审计员。于是"简单"的授权规则开始散落到控制器、队列作业、Livewire 组件、Blade 模板以及随手散落的 Service 类中。

RBAC 正是在这个时候开始显出它的意义。

基于角色的访问控制(Role-Based Access Control)给授权提供了更清晰的形状:用户拥有角色,角色拥有权限,应用检查的是权限,而不是把职位头衔硬编码遍布代码库。

本文将对 RBAC 做一次深度剖析,并在不使用任何第三方包的前提下,用 Laravel 落地一套实现。

首先会讲理论,因为跳过设计部分的话,很容易构造出一套"第一天看起来干净、半年后成为痛点"的权限体系。

然后会构建一份切合实践的实现,涵盖:

  • 以团队为边界的角色
  • 通过角色传递的权限继承
  • 职责分离约束(Separation of Duty)
  • Policy 与 Gate
  • 带缓存的权限解析
  • 针对关键授权流程的测试

它不会做到企业级 IAM 的复杂度,但会显著比一个扁平的 isAdmin() 检查更贴近真实应用。

RBAC 究竟是什么

本质上,RBAC 是一种通过角色授予访问权限、而不是对每个用户单独赋权的模型。

其核心逻辑很直接:

  • 一个用户可以被分配一个或多个角色
  • 一个角色聚合一组权限
  • 系统检查用户当前激活的角色是否授予了所需权限

这在今天听起来很显然,但其中有一个重要的设计优势:角色往往比用户和单条访问规则更稳定

成员会加入团队、调动部门、升职离职、临时兼任职责。如果权限直接挂在每个用户身上,授权模型的运维成本会迅速上升。

NIST 形式化的经典 RBAC 通常分层描述:

  • Core RBAC:用户、角色、权限
  • Hierarchical RBAC:角色之间可以彼此继承权限
  • Constrained RBAC:引入如"职责分离"等约束

最后一层在真实应用中非常重要。

"系统有了角色"并不等同于"系统就安全"。经常还需要附加约束,例如:

  • 审计员不应在同一团队里同时担任计费经理
  • 发出邀请的人,不应有权分配比自己等级更高的角色
  • 被停用的团队,应让所有写权限失败,哪怕用户仍持有角色

因此一个有用的心智模型是:RBAC 提供了一个稳固的基础模型,真实系统通常会在其之上叠加上下文规则

RBAC 不等于"用户和角色"

许多应用声称自己"有 RBAC",但它们实际拥有的只是一套"分组标志位"。

这个区别很重要。

在真正的 RBAC 模型里:

  • 权限挂在角色
  • 用户通过角色获得权限
  • 授权检查谈论的是能力,而非角色名

这意味着代码应更倾向于这样问:

php
$user->can('deploy', $project);

而不是:

php
$user->isAdmin() || $user->isOwner()

后一种写法把组织结构泄露进了应用逻辑;前一种则让领域语言聚焦于用户被允许做什么

这也让系统更容易演进。如果哪天 team-adminrelease-manager 都可以部署项目,只要更新"角色 → 权限"映射即可,不需要改动代码库里每一个控制器。

为什么 RBAC 在真实应用中会走样

团队在过快引入 RBAC 时,会反复落入几个陷阱。

到处都是直挂在用户上的权限。
这通常起于图方便:"那就在用户表上放一个带权限的 JSON 列吧。"这条路在你需要审计"谁能做什么"、或要对上百个用户套同一条规则时就会崩溃。

有角色却没有权限。
另一个常见问题是在代码里到处直接检查角色名:

php
if ($user->role === 'admin') {
    // ...
}

这样脆弱,因为代码和具体角色命名紧耦合在了一起。

没有作用域。
这是最大的那一类。一个用户可能在 A 团队是管理员、在 B 团队只是普通观看者。如果角色是全局的,就没法干净地表达这种情况。

没有约束。
一旦财务、合规或审计场景进入讨论,仅仅"一列角色"就不够用了。需要的是护栏

所以一套可用的 RBAC 实现通常必须具备:作用域、约束,以及明确的"在给定上下文中解析出用户有效权限"的方式

设计一套具备团队作用域的 RBAC 模型

接下来为一个 SaaS 风格的应用构建实现,场景是:用户属于团队,并在团队内与项目打交道。

采用的规则:

  • 用户在同一个团队中可以持有多个角色
  • 角色授予权限
  • 有效权限是用户在该团队中所有角色权限的并集
  • 部分角色组合在同一团队内被禁止同时出现
  • 平台管理员(platform admin)绕过普通的团队规则

模型的结构大致如下:

          具备能力于
User —————————————▶ Team
  │                  │
  │                  │
  │        角色分配   │
  └────────▶ Team Role ◀── Role ─┐

                                  │ Role–Permission 关联

                               Permission


                               Project

一个带团队作用域的 RBAC 模型:用户通过在团队内被指派的角色获得权限。

这已经比一列 users.role 现实得多,因为它捕捉到了真实业务的一个常见需求:同一个人在不同边界里可以意味着不同的东西

先从权限开始,而不是角色

避免 RBAC 设计走歪最有效的一条经验,是先定义权限

角色是组织标签。权限才是真实的应用能力。

以下是示例中的一组权限:

php
enum Permission: string
{
    case ViewTeam = 'team.view';
    case UpdateTeam = 'team.update';
    case InviteMembers = 'team.invite-members';
    case UpdateMemberRoles = 'team.update-member-roles';
    case RemoveMembers = 'team.remove-members';

    case ViewProject = 'project.view';
    case CreateProject = 'project.create';
    case UpdateProject = 'project.update';
    case DeleteProject = 'project.delete';
    case DeployProject = 'project.deploy';
    case ManageBilling = 'billing.manage';
}

有了这些,代码就可以围绕 project.deploy 这样的动作来组织,而不是 owneradmindeveloper 这些角色名

现在再把角色映射到权限上。

定义角色及其层级

建模层级最朴素的方式,是给每个角色一个数值 level 与一组权限集合。

示例里采用的角色:

  • owner
  • admin
  • billing-manager
  • developer
  • viewer
  • auditor

先定义一个 RoleDefinition 值对象:

php
final readonly class RoleDefinition
{
    /**
     * @param list<Permission> $permissions
     */
    public function __construct(
        public string $name,
        public int $level,
        public array $permissions,
    ) {}

    public function has(Permission $permission): bool
    {
        return in_array($permission, $this->permissions, true);
    }
}

再为内置角色准备一个注册表:

php
final class RoleRegistry
{
    /** @return array<string, RoleDefinition> */
    public static function all(): array
    {
        return [
            'owner' => new RoleDefinition(
                name: 'owner',
                level: 100,
                permissions: Permission::cases(),
            ),
            'admin' => new RoleDefinition(
                name: 'admin',
                level: 80,
                permissions: [
                    Permission::ViewTeam,
                    Permission::UpdateTeam,
                    Permission::InviteMembers,
                    Permission::UpdateMemberRoles,
                    Permission::RemoveMembers,
                    Permission::ViewProject,
                    Permission::CreateProject,
                    Permission::UpdateProject,
                    Permission::DeleteProject,
                    Permission::DeployProject,
                ],
            ),
            'billing-manager' => new RoleDefinition(
                name: 'billing-manager',
                level: 60,
                permissions: [
                    Permission::ViewTeam,
                    Permission::ManageBilling,
                ],
            ),
            'developer' => new RoleDefinition(
                name: 'developer',
                level: 40,
                permissions: [
                    Permission::ViewTeam,
                    Permission::ViewProject,
                    Permission::CreateProject,
                    Permission::UpdateProject,
                    Permission::DeployProject,
                ],
            ),
            'viewer' => new RoleDefinition(
                name: 'viewer',
                level: 10,
                permissions: [
                    Permission::ViewTeam,
                    Permission::ViewProject,
                ],
            ),
            'auditor' => new RoleDefinition(
                name: 'auditor',
                level: 20,
                permissions: [
                    Permission::ViewTeam,
                    Permission::ViewProject,
                ],
            ),
        ];
    }

    public static function get(string $name): RoleDefinition
    {
        return self::all()[$name]
            ?? throw new InvalidArgumentException("Unknown role [{$name}].");
    }
}

即便未来要把角色与权限持久化到数据库,从这样一个清晰的权限模型起步,也能避免许多模糊的设计决策。

职责分离比多数人想象的更重要

把形式化 RBAC 中最有用的一部分——约束——带到应用里,很多时候就是把"职责分离"规则明确下来。

比如规定:审计员不能在同一团队中同时兼任计费经理。这就是一条职责分离规则。

不是运行时请求上的权限检查,而是角色分配的合法性规则。也就是说,最好的执行时机是在角色被分配时,而不是用户后续要执行某个操作时。

php
final class TeamRoleConstraint
{
    /**
     * @return list<array{0: string, 1: string}>
     */
    public function forbiddenPairs(): array
    {
        return [
            ['auditor', 'billing-manager'],
        ];
    }

    /**
     * @param list<string> $roleNames
     */
    public function assertValid(array $roleNames): void
    {
        foreach ($this->forbiddenPairs() as [$first, $second]) {
            if (in_array($first, $roleNames, true) && in_array($second, $roleNames, true)) {
                throw new DomainException("Roles [{$first}] and [{$second}] cannot be combined.");
            }
        }
    }
}

这种规则能让授权模型保持诚实。没有它,在技术上或许算 RBAC,却仍允许业务从未预期过的危险角色组合。

数据库结构

把这套模型落到数据库:

  • roles
  • permissions
  • permission_role 关联表
  • team_user_roles 关联表,用来在团队内把角色分配给用户

最后一张表是整个方案的关键。

php
Schema::create('roles', function (Blueprint $table): void {
    $table->id();
    $table->string('name')->unique();
    $table->unsignedInteger('level')->default(0);
    $table->timestamps();
});

Schema::create('permissions', function (Blueprint $table): void {
    $table->id();
    $table->string('name')->unique();
    $table->timestamps();
});

Schema::create('permission_role', function (Blueprint $table): void {
    $table->foreignId('role_id')->constrained()->cascadeOnDelete();
    $table->foreignId('permission_id')->constrained()->cascadeOnDelete();

    $table->primary(['role_id', 'permission_id']);
});

Schema::create('team_user_roles', function (Blueprint $table): void {
    $table->foreignId('team_id')->constrained()->cascadeOnDelete();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->foreignId('role_id')->constrained()->cascadeOnDelete();
    $table->foreignId('assigned_by')->nullable()->constrained('users')->nullOnDelete();
    $table->timestamps();

    $table->unique(['team_id', 'user_id', 'role_id']);
    $table->index(['team_id', 'user_id']);
});

这种设计带来几项关键能力:

  • 一个用户在同一团队里可以持有多个角色
  • 同一个角色可以在多个团队中复用
  • 有效权限可以通过 JOIN 解析出来
  • 角色分配本身可被审计

如果应用规则是"每位用户在每个团队内只能担任一个角色",可以把这张表简化为带 role 字段的成员表。但保留多对多分配,会得到一套更忠实的 RBAC 模型。

Eloquent 关联

模型保持直观:

php
final class Role extends Model
{
    public function permissions(): BelongsToMany
    {
        return $this->belongsToMany(PermissionModel::class, 'permission_role');
    }
}

final class Team extends Model
{
    public function roleAssignments(): HasMany
    {
        return $this->hasMany(TeamUserRole::class);
    }
}

final class User extends Authenticatable
{
    public function teamRoleAssignments(): HasMany
    {
        return $this->hasMany(TeamUserRole::class);
    }

    public function rolesForTeam(Team $team): Collection
    {
        return $this->teamRoleAssignments()
            ->with('role.permissions')
            ->where('team_id', $team->id)
            ->get()
            ->pluck('role')
            ->filter();
    }
}

final class TeamUserRole extends Model
{
    protected $table = 'team_user_roles';

    public function role(): BelongsTo
    {
        return $this->belongsTo(Role::class);
    }

    public function team(): BelongsTo
    {
        return $this->belongsTo(Team::class);
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

结构有了。接下来是让这套系统用起来舒服的那一部分:有效权限的解析。

解析有效权限

Policy 里不应每次授权检查都手工去 JOIN 角色和权限——这样的逻辑会散落到各处。更好的做法是做一个 action 或 service,专门回答一个问题:

此刻,这名用户在这个团队里有效持有哪些权限?

php
use Illuminate\Contracts\Cache\Repository as Cache;

final readonly class ResolveTeamPermissions
{
    public function __construct(private Cache $cache) {}

    /**
     * @return array<string, true>
     */
    public function handle(User $user, Team $team): array
    {
        $key = "rbac:team:{$team->id}:user:{$user->id}:permissions";

        return $this->cache->remember($key, now()->addMinutes(10), function () use ($user, $team): array {
            return $user->rolesForTeam($team)
                ->flatMap(fn (Role $role) => $role->permissions)
                ->pluck('name')
                ->unique()
                ->mapWithKeys(fn (string $permission) => [$permission => true])
                ->all();
        });
    }
}

这样能得到一张查表式的权限映射:

php
[
    'team.view' => true,
    'project.deploy' => true,
    'billing.manage' => true,
]

然后在它之上构建一个专门的授权器:

php
use Illuminate\Auth\Access\Response;

final readonly class TeamAuthorizer
{
    public function __construct(private ResolveTeamPermissions $resolver) {}

    public function allows(User $user, Team $team, Permission $permission): Response
    {
        if ($user->is_platform_admin) {
            return Response::allow();
        }

        if ($team->is_suspended) {
            return $permission === Permission::ViewTeam || $permission === Permission::ViewProject
                ? Response::allow()
                : Response::deny('This team is suspended.');
        }

        $permissions = $this->resolver->handle($user, $team);

        return isset($permissions[$permission->value])
            ? Response::allow()
            : Response::deny('You do not have the required permission in this team.');
    }
}

这里是整体业务级护栏最合适的放置位置——它们位于"单纯的角色成员关系"之上。

更稳妥的角色分配流程

接下来把角色分配做稳。这也是另一个常被偷工减料、直接在控制器里调一次 attach() 就完事的地方。

把规则集中到一个专用 action 里更稳:

php
use Illuminate\Support\Facades\DB;

final readonly class AssignRolesToTeamMember
{
    public function __construct(private TeamRoleConstraint $constraint) {}

    /**
     * @param list<int> $roleIds
     */
    public function handle(User $actor, Team $team, User $member, array $roleIds): void
    {
        DB::transaction(function () use ($actor, $team, $member, $roleIds): void {
            $roles = Role::query()
                ->whereKey($roleIds)
                ->get(['id', 'name', 'level']);

            $roleNames = $roles->pluck('name')->all();

            $this->constraint->assertValid($roleNames);

            $highestRequestedLevel = (int) $roles->max('level');
            $highestActorLevel = (int) $actor->rolesForTeam($team)->max('level');

            if ($highestRequestedLevel >= $highestActorLevel && ! $actor->is_platform_admin) {
                throw new DomainException('You cannot assign a role equal to or higher than your own.');
            }

            TeamUserRole::query()
                ->where('team_id', $team->id)
                ->where('user_id', $member->id)
                ->delete();

            foreach ($roles as $role) {
                TeamUserRole::create([
                    'team_id' => $team->id,
                    'user_id' => $member->id,
                    'role_id' => $role->id,
                    'assigned_by' => $actor->id,
                ]);
            }
        });
    }
}

这里有两件重要的事情:

  • 角色约束在写入时就被强制
  • 角色提升在被持久化之前就被拦下

这让数据更干净,授权层更值得信任。

与 Gate 和 Policy 的集成

把 RBAC 接入 Laravel 的授权体系。框架已经通过 Gate 与 Policy 给了一套关于"能力"的干净词汇表——不如顺势使用,而不是另造 API。

先给平台管理员一个全局旁路:

php
use App\Models\User;
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::before(function (User $user, string $ability) {
        return $user->is_platform_admin ? true : null;
    });
}

这和 Laravel 的授权模型契合得很好:before 返回非 null 时,该结果胜出。

接着是项目的 Policy:

php
use Illuminate\Auth\Access\Response;

final readonly class ProjectPolicy
{
    public function __construct(private TeamAuthorizer $authorizer) {}

    public function view(User $user, Project $project): Response
    {
        return $this->authorizer->allows($user, $project->team, Permission::ViewProject);
    }

    public function update(User $user, Project $project): Response
    {
        return $this->authorizer->allows($user, $project->team, Permission::UpdateProject);
    }

    public function deploy(User $user, Project $project): Response
    {
        return $this->authorizer->allows($user, $project->team, Permission::DeployProject);
    }
}

于是调用方非常干净:

php
Gate::authorize('deploy', $project);

对于需要更多上下文的场景,比如"邀请成员并指定角色",Laravel 的能力方法可以接收一组参数:

php
final readonly class TeamPolicy
{
    public function __construct(private TeamAuthorizer $authorizer) {}

    public function inviteMember(User $user, Team $team, Role $requestedRole): Response
    {
        $permissionCheck = $this->authorizer->allows($user, $team, Permission::InviteMembers);

        if ($permissionCheck->denied()) {
            return $permissionCheck;
        }

        $highestActorLevel = (int) $user->rolesForTeam($team)->max('level');

        return $requestedRole->level < $highestActorLevel
            ? Response::allow()
            : Response::deny('You cannot invite a member with that role.');
    }
}

然后控制器层:

php
public function store(StoreTeamInvitationRequest $request, Team $team): JsonResponse
{
    $requestedRole = Role::query()->findOrFail($request->integer('role_id'));

    Gate::authorize('inviteMember', [$team, $requestedRole]);

    // Persist invitation...
}

这类场景恰是 Laravel 与 RBAC 贴合得最好的地方:Policy 方法拿到资源加额外上下文,控制器保持轻薄。

授权完整流程

把这一切接上之后,一次授权的路径大致如此:

Controller ──authorize('deploy', project)──▶ Gate / Policy


                                             TeamAuthorizer

                                       检查平台管理员 / 团队状态


                                         ResolveTeamPermissions


                                           Cache / 数据库
                                       加载权限映射并回传


                                      允许 / 拒绝的 Response


                              Controller 得到授权结果或 AuthorizationException

一次典型请求里,Policy 把真正的 RBAC 判断下推到专用授权器。

这种分层值得保留。Policy 擅长做面向 HTTP 的授权门面;但专用授权器让这套领域规则在队列作业、action、事件监听器以及任何非控制器的地方都可复用。

缓存与失效

如果每次请求都去解析角色与权限关系,RBAC 检查很快就会变得查询密集。因此给"有效权限映射"加一层缓存通常是值得的。

但缓存一旦开启,清理策略也必须清晰。

当以下任一发生时,应当清理对应 team/user 对的缓存:

  • 角色分配被新增或移除
  • 某个角色的权限被修改
  • 团队被停用或恢复
php
final readonly class ForgetTeamPermissionCache
{
    public function __construct(private Cache $cache) {}

    public function handle(int $teamId, int $userId): void
    {
        $this->cache->forget("rbac:team:{$teamId}:user:{$userId}:permissions");
    }
}

看起来是件小事,但它正是"RBAC 层很快"与"任何渲染大量 @can 检查的页面都陷入 N+1 风格的性能痛苦"之间的分水岭。

认真地给 RBAC 写测试

授权代码正是那种值得被认真测试的代码。它保护的是关键行为,随时间会不断增长,细微的回归又很容易被引入。

对这种类型的实现,把测试拆成两组比较合适:

  • 单元测试:聚焦权限解析与角色约束
  • Feature 测试:通过 HTTP 端点或 Gate 验证 Policy 的行为

下面是几段 Pest 示例。

针对有效权限的聚焦测试:

php
it('unions permissions from multiple roles in the same team', function () {
    $user = User::factory()->create();
    $team = Team::factory()->create();

    $developer = Role::factory()->create(['name' => 'developer']);
    $billingManager = Role::factory()->create(['name' => 'billing-manager']);

    $developer->permissions()->attach([
        PermissionModel::fromName(Permission::DeployProject)->id,
    ]);

    $billingManager->permissions()->attach([
        PermissionModel::fromName(Permission::ManageBilling)->id,
    ]);

    TeamUserRole::factory()->create([
        'team_id' => $team->id,
        'user_id' => $user->id,
        'role_id' => $developer->id,
    ]);

    TeamUserRole::factory()->create([
        'team_id' => $team->id,
        'user_id' => $user->id,
        'role_id' => $billingManager->id,
    ]);

    $permissions = app(ResolveTeamPermissions::class)->handle($user, $team);

    expect($permissions)->toHaveKeys([
        Permission::DeployProject->value,
        Permission::ManageBilling->value,
    ]);
});

约束测试:

php
it('rejects forbidden role combinations', function () {
    $constraint = new TeamRoleConstraint();

    expect(fn () => $constraint->assertValid(['auditor', 'billing-manager']))
        ->toThrow(DomainException::class, 'Roles [auditor] and [billing-manager] cannot be combined.');
});

Feature 风格的 Policy 测试:

php
it('allows a developer to deploy projects in their own team', function () {
    $user = User::factory()->create();
    $team = Team::factory()->create();
    $project = Project::factory()->for($team)->create();

    $developer = Role::factory()->create(['name' => 'developer', 'level' => 40]);

    $deployPermission = PermissionModel::factory()->create([
        'name' => Permission::DeployProject->value,
    ]);

    $developer->permissions()->attach($deployPermission);

    TeamUserRole::factory()->create([
        'team_id' => $team->id,
        'user_id' => $user->id,
        'role_id' => $developer->id,
    ]);

    expect(Gate::forUser($user)->allows('deploy', $project))->toBeTrue();
});

关键的不是覆盖率,而是信心。当未来新增一个角色或权限时,这些测试能让调整更踏实,而不是全靠猜。

需要避开的陷阱

有一些坑几乎总会反复出现。

角色爆炸。 如果每一种细微差别都新建一个角色,整套模型会越来越难推理。更好的做法是维护一个较小的角色集合加上清晰的权限集合。模型一旦过度依赖上下文,就说明需要在 RBAC 之上叠加一些 ABAC 风格的检查。

把业务规则写进控制器。 控制器应当触发授权,而不是定义授权。一个接口里是 $user->isAdmin(),另一个接口里是 $user->belongsToTeam($team),系统已经在漂移。

角色分配没有审计线索。 如果角色变更频繁,至少要保留 assigned_by、时间戳,理想情况下还要一份专门的审计日志。访问控制是最后一个还能容忍"我记得上周改过吧"这种模糊说法的领域。

把所有权当成授权。 所有权可能顺带授予很多权限,但它本质仍是一个业务概念。应该把它建模为某个角色或某条专门规则,而不是散落各处的一次性检查。

只测 happy path。 授权 bug 往往藏在负面场景里:错误的团队、违规的角色提升、被停用的租户、互斥的角色组合。这些才是测试需要真正投入注意力的地方。

RBAC 不够用的场景

RBAC 在许多 Laravel 应用中都很好用,但它不是万能药。有时候还需要基于以下维度的规则:

  • 所有权
  • 时间窗口
  • 订阅计划
  • 地理位置
  • 文档状态
  • 用户与资源之间的关系

这并不是 RBAC 的失败,只是意味着授权模型的边界大于"角色本身"。实际中,成熟系统常常出现这样的分层组合:

  • 用 RBAC 承载稳定的权限基线
  • 用 Policy 承载资源感知的检查
  • 用额外约束承载上下文规则

这种组合通常比"把每一条规则都塞进角色名"要可扩展得多。

结语

RBAC 远不只是 users 表上的一个 role 列。

只要实现得当,它能提供一个清晰的授权模型,建立在角色、权限、作用域与约束之上。其价值在于:重复逻辑更少、权限管理更安全、代码讨论的是"能力"而不是"职位头衔"。

在 Laravel 里,一个比较舒适的落点通常是:把 RBAC 模型本身显式地放在领域里,用一个专用 service 解析有效权限,再通过 Gate 与 Policy 暴露规则。这样控制器保持轻薄、授权逻辑保持集中,测试也有稳定的靶子。

要把这套方案落到真实项目中,可以从小处入手:先定义好权限;把角色分配限定在合适的边界上;加上一两条职责分离约束;像对待 happy path 一样认真地测试负面场景。

这样下来,走得远远比"在应用里随手加上 is_admin 检查、指望它能越用越顺"要靠谱得多。

本作品采用《CC 协议》,转载必须注明作者和本文链接