认证与 RBAC
LY Fullstack 的权限闭环由数据库、Admin API 和 Admin 三层共同组成。前端负责展示和交互,最终授权决定始终在服务端。
五表模型
核心关系为:
User:管理员账号、状态、密码摘要和会话版本。Role:角色标识、状态与系统保护属性。Menu:目录、菜单、按钮权限及前端页面绑定信息。UserRole:用户与角色多对多关系。RoleMenu:角色与菜单/权限多对多关系。
超级管理员角色由 Seed 初始化并受保护,不应像普通业务角色一样被删除或削弱。
登录链路
- Admin 请求
GET /api/auth/captcha获取服务端生成的缺口图和拼图块。 - 用户完成拖动后,Admin 请求
POST /api/auth/captcha/verify。 - 验证通过会得到一次性凭证;正确位置不会直接下发给浏览器。
- Admin 携带账号、密码和一次性凭证请求
POST /api/auth/login。 - Admin API 校验账号状态和密码摘要,签发管理 JWT。
- Pinia 保存认证状态,路由进入受保护区域。
验证码挑战与登录凭证均只能消费一次,并有接口限流。它提供基础的人机校验,不应被宣传为可替代专业风控系统。
会话恢复为什么还要查数据库
JWT 不保存完整角色、菜单和权限快照。浏览器刷新后调用 GET /api/auth/me,AdminJwtGuard 根据账号标识重新读取:
- 账号是否仍存在且启用;
- Token 中的
tokenVersion是否仍有效; - 当前角色和角色状态;
- 当前菜单树和权限码。
因此,管理员禁用账号、调整角色或修改菜单权限后,不需要等待旧权限快照自然过期。
tokenVersion 如何撤销旧 Token
修改当前密码或由管理员重置密码时,服务端递增账号的 tokenVersion。旧 JWT 仍携带签发时的旧版本,下一次受保护请求会被 Guard 拒绝。
这解决账号级会话撤销,但不能替代 JWT 密钥轮换。JWT_SECRET 泄露时仍必须更换生产密钥并重启服务。
动态菜单和接口权限
一项功能通常同时具有:
- 静态路由:Admin 知道页面组件在哪里。
- 数据库菜单:当前角色是否在侧栏看到入口。
- 按钮权限码:页面是否展示新增、编辑、删除等操作。
- 服务端权限声明:直接请求接口时是否允许执行。
只有前三项没有第四项,攻击者仍可以绕过页面直接调用 API。所有敏感写操作都应使用 @RequirePermissions(...) 并由 PermissionGuard 校验。
测试一个新角色
建议保留一个非超级管理员测试账号:
- 在角色管理创建角色。
- 只勾选目标菜单和部分操作权限。
- 在用户管理把角色分配给测试账号。
- 使用无痕窗口登录该账号。
- 验证菜单和按钮隐藏。
- 使用浏览器网络面板或测试直接调用无权接口,确认返回
403。
页面看不到入口只是体验验收;接口明确拒绝才是安全验收。
当前边界
这套认证只属于后台管理员。默认 apps/api 不包含终端用户注册、登录、刷新令牌、第三方 OAuth 或多租户会话,后续必须按真实 C 端业务单独设计。