前端分析:Vue 管理后台如何承接 SSO 权限系统
前端分析:Vue 管理后台如何承接 SSO 权限系统
前端不是这个项目里最复杂的协议层,但它决定了系统是否真的可用。SSO 后端提供了大量能力:用户、用户组、角色、权限、应用、审计、安全策略、会话。如果前端只是把接口简单铺成表格,管理员仍然很难理解权限关系。
这个管理后台使用 Vue 3 + Vite + Pinia + Tailwind CSS,实现了一个偏工具型的后台界面。
前端目录结构:
frontend/├── src/│ ├── App.vue│ ├── main.js│ ├── router/│ ├── stores/│ ├── services/│ ├── views/│ ├── components/│ ├── utils/│ └── assets/├── vite.config.js├── tailwind.config.js└── package.json技术栈
依赖比较克制:
vue:页面和组件。vue-router:路由。pinia:登录态和用户信息。axios:请求管理后台 API。lucide-vue-next:图标。tailwindcss:样式。
项目没有引入大型 UI 组件库,而是自己封装了几个通用组件:
PageHeaderTableBlockDrawerModalEntityFormPermissionMatrixScopedChecklistSelectBlockStatusPillFullScreenEditor
这让页面风格统一,也避免了为一个个人项目引入过重的后台框架。
应用入口
main.js 做的事很少:
- 创建 Vue app。
- 创建 Pinia。
- 注册 router。
- 初始化 auth store。
- 挂载应用。
App.vue 负责整体布局:
- 未初始化时显示加载状态。
- 未登录时直接渲染当前路由,例如登录页、授权确认页。
- 已登录时渲染后台框架:顶部栏、侧边导航、内容区域、移动端抽屉导航。
后台页面结构是典型的管理台布局:
┌─────────────────────────── 顶部栏 ───────────────────────────┐│ SSO Admin 刷新 / 当前用户 │├───────────────┬───────────────────────────────────────────────┤│ 侧边菜单 │ 当前页面 ││ Overview │ ││ Users │ ││ Groups │ ││ Roles │ ││ ... │ │└───────────────┴───────────────────────────────────────────────┘登录态设计
登录态集中在 stores/authStore.js。
state 包括:
initializedaccessTokenrefreshTokenuser
这些数据会同步到 localStorage:
sso_access_tokensso_refresh_tokensso_user初始化时,如果本地有 access token:
- token 已过期:清理本地状态。
- token 未过期:调用
/api/auth/me拉取最新用户信息。 - 请求失败:清理本地状态。
这一步很重要。JWT 里虽然有用户信息,但系统存在 session 状态、权限版本和账号状态。前端启动时重新请求 /me,可以尽早发现后端已经撤销会话或更新权限。
Axios 拦截器
services/api.js 封装 axios 实例。
请求拦截器会自动添加:
Authorization: Bearer <accessToken>响应拦截器处理 401:
- 读取 localStorage 中的 refresh token。
- 如果当前请求不是重试请求,则调用
/api/auth/refresh。 - 使用返回的新 token 更新 localStorage。
- 触发
sso:token-refreshed浏览器事件,同步 Pinia store。 - 重试原始请求。
- 如果刷新失败,清理本地状态并触发
sso:session-expired。
这里还有一个并发控制:
let refreshing = null当多个请求同时收到 401 时,前端不会发起多个 refresh 请求,而是复用同一个 refreshing Promise。这能避免 refresh token rotation 下的典型问题:多个并发刷新请求会让第一个请求把 refresh token 标记为 used,后续请求再拿旧 token 刷新就可能触发复用检测。
这是这个前端实现里很值得保留的细节。
路由与权限守卫
路由定义在 router/index.js:
const routes = [ { path: '/login', component: () => import('../views/LoginView.vue'), meta: { requiresAuth: false } }, { path: '/consent', component: () => import('../views/ConsentView.vue'), meta: { requiresAuth: false } }, { path: '/', redirect: '/overview' }, { path: '/overview', component: () => import('../views/OverviewView.vue') }, { path: '/profile', component: () => import('../views/ProfileView.vue') }, { path: '/users', component: () => import('../views/UsersView.vue') }, { path: '/groups', component: () => import('../views/GroupsView.vue') }, { path: '/roles', component: () => import('../views/RolesView.vue') }, { path: '/permissions', component: () => import('../views/PermissionsView.vue') }, { path: '/clients', component: () => import('../views/ClientsView.vue') }, { path: '/sessions', component: () => import('../views/SessionsView.vue') }, { path: '/audit', component: () => import('../views/AuditView.vue') }, { path: '/security', component: () => import('../views/SecurityView.vue') },]守卫逻辑分三层:
- 未登录访问受保护页面:跳转到
/login?returnTo=...。 - 平台管理页面只允许平台管理员访问,例如
/clients、/audit、/security。 - 应用管理页面允许平台管理员或应用管理员访问,例如
/users、/groups、/roles、/permissions。
前端权限工具在 utils/permissions.js,它复刻了后端的 permissionImplies()。
平台管理员判断:
export function isPlatformAdmin(permissions) { return hasPermission(permissions, 'sso.admin')}应用管理员判断:
export function managedApps(permissions) { if (isPlatformAdmin(permissions)) return null const apps = new Set() for (const permission of permissions || []) { const parts = String(permission || '').split('.').filter(Boolean) if (parts[0] && parts[1] === 'admin' && parts[0] !== 'sso') apps.add(parts[0]) } return [...apps].sort()}注意:前端守卫只是用户体验层,真正的安全边界仍然在后端 requireAdminAuth() 和 requireScopedManagement()。
菜单可见性
App.vue 根据当前用户权限计算 visibleNav:
- 平台管理员:显示全部菜单。
- 应用管理员:隐藏应用接入、审计日志、安全策略。
- 普通用户:只显示个人中心和会话。
这个设计和后端范围控制一致:
| 用户类型 | 可见页面 |
|---|---|
| 平台管理员 | 全部页面。 |
| 应用管理员 | 概览、个人中心、会话、用户、用户组、角色、权限。 |
| 普通用户 | 个人中心、会话。 |
应用管理员仍能进入用户、用户组、角色、权限页,但接口会自动限制到自己拥有 {app}.admin 的应用范围。
登录页
LoginView.vue 调用 auth.login(),后者请求 /api/auth/login。
登录失败时,后端可能返回:
unauthorizedcaptcha_requiredinvalid_captchaaccount_lockedforbidden
Axios 拦截器会对通用错误码做映射,但对验证码相关错误保持原始结构,让登录页可以读取 captchaId 和 captchaImage。
成功登录后,页面会根据 returnTo 回到原目标地址。这同样服务于 OAuth 授权流程:业务系统跳到 /oauth/authorize,SSO 判断未登录后重定向到前端登录页,登录完成再回到原授权地址继续流程。
授权确认页
ConsentView.vue 对应 OAuth consent。
它从 URL query 中读取:
client_idredirect_uriscopestatecode_challengecode_challenge_methodaudience
用户同意时,前端调用:
POST /oauth/consent后端返回最终 redirect URL,前端再跳回业务系统。用户拒绝时,会带着 access_denied 回到业务系统。
这里前端只负责展示和提交决定。client、redirect URI、scope、audience、PKCE 的最终校验仍在后端。
概览页
OverviewView.vue 根据用户类型展示不同数据:
平台管理员看到:
- 用户数量。
- 用户组数量。
- 应用数量。
- 失败登录数量。
- 最近登录。
- 最近审计。
应用管理员看到:
- 当前应用范围内的用户数量。
- 用户组数量。
- 角色数量。
- 权限数量。
这体现了项目对“平台管理”和“应用管理”的区分。平台管理员关心全局安全和接入;应用管理员关心自己应用下的人和权限。
应用接入页
ClientsView.vue 只对平台管理员开放。
它管理 OAuth client 的核心字段:
clientKeynameclientTypeaudienceredirectUrispostLogoutRedirectUrisallowedScopesservicePermissionstokenTtlSecondsrefreshTtlSecondsstatus
创建应用后,如果后端返回一次性 secret 或应用管理员账号密码,前端用 alert 展示:
- Client Secret 只显示一次。
- 应用管理员用户名和密码只显示一次。
这类敏感信息不应在后端持久保存明文,也不应该后续再次查询显示。当前实现符合这个原则。
页面还直接显示接入地址:
- Discovery
- Authorize
- JWKS
这对接入方很实用,避免管理员每次去翻文档。
用户、用户组、角色、权限管理
这几类页面结构相似,但各自有不同编辑关系:
- 用户:基础信息、状态、角色、权限、用户组。
- 用户组:基础信息、成员、角色、权限。
- 角色:基础信息、权限。
- 权限:权限字典。
它们共同使用了几个组件。
FullScreenEditor
用户、用户组、角色编辑使用全屏编辑器。原因很现实:权限和关系字段很多,如果用小弹窗会非常拥挤。
全屏编辑器适合这种“单个实体、多块关系”的场景:
- 左侧或上方是基础信息。
- 中间是角色、权限、用户组等多选。
- 保存前可以整体检查。
ScopedChecklist
ScopedChecklist 用于按应用或资源范围展示可选项。它支持搜索、勾选、显示元信息。
这对用户和用户组的授权尤其重要。管理员不应该面对一大串无结构的 ID,而应该看到权限所属应用和资源分组。
PermissionMatrix
PermissionMatrix.vue 是权限选择的核心组件。它会把权限整理成树形:
应用 资源 权限每一层都可以批量选择:
- 勾选应用:选择该应用下全部权限。
- 勾选资源:选择该资源下全部权限。
- 勾选权限:选择单个权限。
组件内部用 selectedSet 做勾选状态计算,用 expanded 维护展开状态。它还会根据 roleClientId 过滤当前角色所属应用,避免把不同应用的权限误分配给某个应用角色。
这种组件比普通多选框更适合权限系统,因为权限本身是层级数据。
个人中心和会话
普通用户至少需要两个页面:
- 个人中心。
- 会话管理。
个人中心提供:
- 查看用户名、邮箱、手机号、显示名、角色、用户组。
- 修改邮箱。
- 修改密码。
- 查看登录日志。
会话页提供:
- 当前用户 session 列表。
- 撤销指定会话。
- 退出其他设备。
这些功能不只是后台管理补充,也属于 SSO 系统的安全基础能力。用户可以发现异常设备,并主动撤销。
安全策略页
SecurityView.vue 管理平台级安全配置:
- 密码长度。
- 密码是否要求数字。
- 密码是否要求字母。
- 验证码阈值。
- 锁定阈值。
- 锁定时间。
- access token TTL。
- refresh token TTL。
- authorization code TTL。
- CORS origin。
- CORS origin suffix。
- 签名密钥轮换。
这个页面只开放给平台管理员。签名密钥轮换是高风险操作,前端提供确认动作,后端负责实际轮换。
审计日志页
审计日志和登录日志是定位问题时最有价值的页面。
登录日志关注:
- 谁尝试登录。
- 成功还是失败。
- 失败原因。
- IP 和 User-Agent。
- 时间。
审计日志关注:
- 谁操作。
- 操作动作。
- 目标类型和目标 ID。
- 结果。
- 元数据。
- 时间。
对个人项目来说,审计日志常被忽略。但 SSO 是所有业务系统的入口,一旦账号、权限或 client 被修改,必须能追踪。
错误文案
utils/errorMessages.js 把后端错误码映射成前端可读文案。这样页面里不需要重复处理每种错误。
同时,Axios 拦截器保留了 errorData,页面仍然能在需要时读取后端原始字段。验证码就是典型例子。
前端设计取舍
这个前端有几个明确取舍:
- 不引入大型后台模板,减少维护负担。
- 用 localStorage 存 token,实现简单,但需要注意 XSS 风险。
- 用 Pinia 管理登录态,不把所有业务数据放进全局 store。
- 路由和菜单做权限感知,但不把它当安全边界。
- 用全屏编辑器承载复杂关系,而不是在表格里硬塞所有操作。
- 权限选择用树形矩阵,让管理员能按应用和资源理解授权。
其中最值得关注的是 refresh 并发控制。因为后端实现了 refresh token rotation,前端如果不控制并发,很容易因为多个请求同时刷新而误触发 token 复用检测。
可以继续改进的点
前端后续可以增强几块:
- 把一次性 secret 展示从
alert改成更安全的确认弹窗,支持复制按钮。 - 对高风险操作增加更明确的二次确认,例如输入应用名或用户名。
- 给权限变更增加差异预览,保存前显示新增和移除项。
- 给表格增加分页、排序和更强过滤。
- 对 token 存储做更严格的安全评估,考虑 httpOnly cookie 或 BFF 模式。
- 为关键页面补充端到端测试,覆盖登录、刷新、用户授权、应用创建。
前端小结
这个 Vue 管理后台的价值在于把后端复杂能力组织成可操作界面:
- 登录态、token 刷新和路由守卫保证基础使用体验。
- 权限菜单和后端范围控制保持一致。
- 用户、用户组、角色、权限编辑围绕应用范围组织。
- 权限矩阵把点号权限转换成更适合管理员理解的树。
- 个人中心、会话、审计和安全策略补齐了 SSO 管理闭环。
下一篇看业务系统如何接入,以及这个项目如何部署和继续演进。
个人 SSO 权限系统
记录我为个人多项目搭建统一登录和权限管理系统的完整过程,覆盖架构、OAuth、安全闭环、权限模型、接入和部署。
March7th