权限系统 / 已完成

后台权限系统设计

理解用户、角色、权限、菜单、按钮权限、路由权限和后端接口权限如何组成权限闭环。

返回文章积累

一句话:后台权限系统就是回答“谁登录了、他是什么角色、他能看到什么、能操作什么、后端要不要放行”。

本篇学完你会什么:理解用户、角色、权限、菜单、按钮权限、路由权限和后端接口权限之间的关系,知道怎么从最小版一步步做成可维护的后台权限系统。

1. 权限系统解决什么问题

后台系统不是所有人都能做所有事。

比如:

普通运营:只能看用户列表
客服主管:可以编辑用户状态
系统管理员:可以新增用户、分配角色、删除用户

权限系统要回答四个问题:

你是谁?
你有哪些角色?
你的角色有哪些权限?
这个接口或按钮能不能给你用?

2. 用户、角色、权限分别是什么

先用公司门禁理解:

名词像什么例子
用户 User具体的人张三
角色 Role一张岗位牌管理员、运营、客服
权限 Permission能做某件事的钥匙user:create
菜单 Menu能看到哪个房间入口用户管理菜单
按钮 Button房间里的具体操作新增、删除
接口 API真正办事的窗口POST /users

最重要的一句话:角色不是权限本身,角色只是权限的集合。

3. 最小数据模型怎么设计

最小可维护版建议 5 张表:

users
roles
permissions
user_roles
role_permissions

关系是:

一个用户可以有多个角色
一个角色可以有多个权限
用户最终拥有的权限 = 他所有角色的权限合集

如果第一版很小,也可以先简化:

users 表里加 is_admin

但只适合学习 Demo。只要系统里开始有“运营、客服、管理员、只读人员”这些区别,就应该上角色和权限。

4. 权限码怎么命名

权限码要稳定、可读、方便搜索。

推荐格式:

资源:动作

例子:

权限码大白话
user:read查看用户
user:create新增用户
user:update编辑用户
user:delete删除用户
role:read查看角色
role:assign分配角色
permission:manage管理权限

不要用这种看不懂的名字:

p001
admin_user_btn_3
canDoSomething

权限码以后会出现在前端路由、按钮、后端接口、数据库和日志里,名字越清楚,排查越省力。

5. 登录后要返回什么

登录成功后,后端可以返回:

{
  "access_token": "eyJxxx",
  "user": {
    "id": 1,
    "username": "admin"
  },
  "roles": ["admin"],
  "permissions": [
    "user:read",
    "user:create",
    "user:update",
    "user:delete"
  ]
}

前端拿到后:

  1. 保存 token。
  2. 保存用户信息。
  3. 保存权限码列表。
  4. 根据权限生成菜单、路由、按钮状态。

注意:这里的 permissions 更适合放在登录响应体里,或者放在 /auth/me 返回的数据里,给前端渲染菜单和按钮用。

不建议把所有权限码都塞进 JWT payload。JWT 里通常只放 sub 用户 id、exp 过期时间,最多再放一个 token version 这类轻量字段。权限变化时,服务端重新查询权限会更可控。

刷新页面后,前端不要只相信本地缓存,最好调用:

GET /api/v1/auth/me

重新确认当前用户和权限。

6. 前端路由权限怎么做

路由可以写上需要的权限:

{
  path: '/users',
  component: () => import('@/pages/users/UserListPage.vue'),
  meta: {
    title: '用户管理',
    permission: 'user:read'
  }
}

路由守卫检查:

router.beforeEach((to) => {
  const permission = to.meta.permission as string | undefined;

  if (permission && !authStore.permissions.includes(permission)) {
    return '/403';
  }

  return true;
});

大白话:进页面前先看门票,没有 user:read 就不能进用户管理页。

7. 前端按钮权限怎么做

按钮也可以按权限显示:

<button v-if="hasPermission('user:create')">
  新增用户
</button>

<button v-if="hasPermission('user:delete')">
  删除
</button>

hasPermission 可以很简单:

export function hasPermission(code: string) {
  return authStore.permissions.includes(code);
}

注意:按钮隐藏只是用户体验,不是安全。真正安全要看后端。

8. 后端接口权限怎么做

后端接口也要声明需要什么权限:

def require_permission(code: str):
    async def checker(current_user: User = Depends(get_current_user)):
        permissions = await get_user_permissions(current_user.id)
        if code not in permissions:
            raise HTTPException(status_code=403, detail="没有权限")
        return current_user

    return checker

使用:

@router.post("/users")
async def create_user(
    body: UserCreate,
    current_user: User = Depends(require_permission("user:create")),
):
    ...

这一步是权限系统的底线。即使前端按钮被人绕过,后端也不能放行。

9. 菜单和权限是什么关系

菜单是入口,权限是钥匙。

有两种常见做法:

做法说明适合
前端固定菜单,根据权限过滤菜单写在前端代码里小项目、菜单变化少
后端返回菜单树菜单存在数据库里后台系统、菜单可配置

固定菜单例子:

const menus = [
  {
    title: '用户管理',
    path: '/users',
    permission: 'user:read'
  }
];

const visibleMenus = menus.filter((menu) => hasPermission(menu.permission));

后端菜单树例子:

[
  {
    "title": "系统管理",
    "children": [
      {
        "title": "用户管理",
        "path": "/users",
        "permission": "user:read"
      }
    ]
  }
]

第一版建议先用前端固定菜单。等菜单确实需要后台配置,再把菜单放到后端。

10. 从简单版到正式版怎么升级

不要一步到位。可以这样升级:

阶段做法适合什么时候
第 1 阶段is_admin 判断管理员学习 Demo
第 2 阶段用户、角色、权限三层有多个岗位
第 3 阶段菜单权限、按钮权限、接口权限统一正式后台
第 4 阶段数据权限不同部门只能看自己的数据
第 5 阶段审计日志要追踪谁改了什么

大多数项目先做到第 3 阶段就够用了。数据权限和审计日志可以后面再补。

11. 常见错误

错误后果修正
只做前端权限接口可被直接调用后端接口必须校验
权限码命名混乱排查困难资源:动作
把角色写死在代码里后期难改角色和权限放数据库
菜单权限和接口权限分离看得到但点了 403菜单、按钮、接口用同一套权限码
JWT payload 里塞太多权限token 太大且权限变化不及时可只放 user id,权限从服务端查
没有 /auth/me刷新后状态不可靠前端启动时重新拉当前用户

12. 权限系统检查清单

[ ] 是否区分用户、角色、权限
[ ] 权限码是否用统一命名规则
[ ] 登录后能拿到当前用户权限
[ ] 刷新页面后能通过 /auth/me 恢复权限
[ ] 路由进入前会检查页面权限
[ ] 按钮会根据权限显示或隐藏
[ ] 后端接口会校验权限
[ ] 无权限时返回 403
[ ] 菜单和按钮使用同一套权限码
[ ] 管理员变更权限后有明确刷新策略

13. 总结表

名词大白话
用户具体登录的人
角色一组权限的集合
权限码能不能做某件事的钥匙
菜单权限能不能看到入口
按钮权限能不能看到操作按钮
接口权限后端是否真正放行
403已登录,但没权限
/auth/me刷新后重新确认当前用户和权限

上一篇建议:大白话讲解——前后端接口联调.md

下一篇建议:回到真实项目,先用 user:readuser:createuser:updateuser:delete 做一套最小权限闭环。