目录
- 一、概念
- 二、权限与访问控制的「能力全景图」
- 三、前端视角的「权限控制分层模型」(核心)
-
- [1、登录态层(Authentication State)](#1、登录态层(Authentication State))
- [2、路由层(Page Access Control)](#2、路由层(Page Access Control))
- [3、菜单层(Navigation Control)](#3、菜单层(Navigation Control))
- [4、组件 / 操作层(Action Control)](#4、组件 / 操作层(Action Control))
- 四、前端权限系统的"典型数据流"
- 五、权限模型的三种常见设计(前端必须懂)
- 六、重点「权限与访问控制」业务剖析
-
- 1、用户鉴权
-
- (1)、用户鉴权概念
- (2)、用户鉴权的「标准模型」(企业级通用)
- (3)、主流鉴权方案全景对比(你必须会)
- [(4)、JWT 鉴权机制(前端必须吃透)](#(4)、JWT 鉴权机制(前端必须吃透))
-
- [①、JWT 本质](#①、JWT 本质)
- [②、企业级 JWT 设计(重点)](#②、企业级 JWT 设计(重点))
- (5)、前端如何"正确"做用户鉴权(分层讲)
-
- ①、登录阶段(Login)
- ②、凭证存储(非常关键)
- [③、请求鉴权(Request Authentication)](#③、请求鉴权(Request Authentication))
- ④、过期处理(企业级重点)
- (6)、一个「简单但完整」的企业级典例
- 2、黑白名单
- 3、动态菜单
一、概念
"用户权限与访问控制类业务"是指在用户身份已确认的前提下,通过角色、权限、规则和策略,决定用户:
- 能不能访问
- 能不能操作
- 能看到什么
"用户权限与访问控制"在系统中的位置(非常重要):
typescript
用户身份认证
(你是谁)
↓
用户权限与访问控制
(你能不能)
↓
业务功能执行
权限系统是身份认证与业务执行之间的"闸门"。
"用户权限与访问控制类业务"在前端主要体现为:
- 登录态管理
- 路由访问控制
- 动态菜单
- 操作级权限控制。
前端负责:权限的展示与引导
- 前端权限系统的目标不是保证安全,而是降低误操作、减少越权尝试、提升用户体验。
后端负责:权限的裁决与执行
- 真正的安全控制必须由后端统一裁决,前后端形成权限的双层校验体系。
二、权限与访问控制的「能力全景图」
从前端视角,把这个业务域拆成 8 大核心能力:
| 能力 | 解决的问题 | 前端是否参与 |
|---|---|---|
| 身份状态管理 | 是否登录 | ✅ |
| 角色模型 | 用户是什么身份 | ✅ |
| 权限点模型 | 能做哪些操作 | ✅ |
| 路由访问控制 | 能进哪些页面 | ✅ |
| 动态菜单 | 能看到哪些入口 | ✅ |
| 按钮级权限 | 能点哪些操作 | ✅ |
| 黑白名单 | 是否强制禁止/放行 | ⚠️(展示) |
| 接口级校验 | 是否真正允许 | ❌(后端) |
📌 前端主要负责:入口、展示、引导、体验控制
三、前端视角的「权限控制分层模型」(核心)
前端权限不是一层,而是四层:
- 登录态层
- 路由层
- 菜单层
- 组件 / 操作层
1、登录态层(Authentication State)
前端职责:
- 判断是否已登录
- 维护 Token / Session
- 处理登录失效
典型实现:
- 路由守卫
- 请求拦截器
- Token 过期重定向
📌 这是所有权限的前提条件
2、路由层(Page Access Control)
问题:用户能否访问某个页面?
实现方式:
- 路由 meta.permission
- 登录后动态注册路由
- 未授权跳转 403
📌 防止"手输 URL 访问"
3、菜单层(Navigation Control)
问题:用户能看到哪些功能入口?
实现方式:
- 后端返回菜单树
- 或前端根据权限过滤菜单
- 菜单 ≠ 路由(重要)
📌 菜单是权限的 UI 映射
4、组件 / 操作层(Action Control)
问题:用户能不能执行具体操作?
实现方式:
- v-if / 权限指令
- hooks / 高阶组件
- 操作前二次校验
📌 按钮权限永远不能当安全机制
四、前端权限系统的"典型数据流"
typescript
登录成功
↓
获取用户信息(角色 / 权限 / 菜单 / 名单状态)
↓
初始化权限上下文(Store)
↓
动态生成路由
↓
渲染菜单
↓
控制组件 / 操作
📌 这是90% 企业级系统的真实流程
五、权限模型的三种常见设计(前端必须懂)
- 基于角色的 RBAC
- 基于权限点的 PBAC
- 基于属性/规则的 ABAC
1、RBAC(基于角色)
typescript
用户 → 角色 → 权限
前端特点:
- 好理解
- 菜单生成简单
- 扩展性一般
2、PBAC(基于权限点)
typescript
用户 → 权限点
前端特点:
- 按钮级控制友好
- 菜单/操作粒度细
- 前端维护成本较高
3、ABAC(基于属性/规则)
typescript
用户属性 + 资源属性 + 环境属性
前端特点:
- 前端只做展示
- 复杂规则交给后端
- 常见于金融风控
📌 金融系统:RBAC + ABAC 混合
六、重点「权限与访问控制」业务剖析
1、用户鉴权
没有可靠的用户鉴权,后面的权限、菜单、风控全部没有意义。
用户鉴权是整个系统安全的第一道防线:
- 前端负责凭证的安全存储、携带和失效处理。
- 后端负责身份真实性校验和凭证签发。
在企业和金融系统中通常采用双 Token 机制以兼顾安全性和用户体验。
(1)、用户鉴权概念
用户鉴权,是用于确认"当前发起请求的主体是谁,并且是否真实可信"的过程。
关键词只有一个:
- 你是谁
鉴权 ≠ 权限(必须先区分):
| 项目 | 用户鉴权 | 用户权限 |
|---|---|---|
| 核心问题 | 你是谁 | 你能做什么 |
| 是否必须 | ✅ | ❌(视业务) |
| 是否安全核心 | ✅ | ⚠️ |
| 典型产物 | Token / Session | 菜单 / 按钮 / 规则 |
📌 鉴权是所有安全体系的地基
(2)、用户鉴权的「标准模型」(企业级通用)
抽象成 4 个核心步骤:
- 身份声明(Credentials)
- 身份验证(Verify)
- 身份凭证发放(Token / Session)
- 身份凭证校验(Request Auth)
各步骤的职责分工(前端视角):
| 阶段 | 前端职责 | 后端职责 |
|---|---|---|
| 登录 | 收集凭证 | 验证真实性 |
| 发证 | 存储凭证 | 生成 Token |
| 使用 | 携带 Token | 校验 Token |
| 失效 | 处理过期 | 判定失效 |
📌 前端 不验证身份真实性,只负责携带与管理凭证
(3)、主流鉴权方案全景对比(你必须会)
| 方案 | 是否企业常用 | 特点 |
|---|---|---|
| Cookie + Session | ✅ | 简单、CSRF 风险 |
| JWT(Bearer Token) | ✅ | 无状态、最主流 |
| OAuth2 / SSO | ✅ | 企业/平台级 |
| 双 Token(Access + Refresh) | ✅ | 金融级 |
| 证书 / Key | ⚠️ | 系统对系统 |
(4)、JWT 鉴权机制(前端必须吃透)
①、JWT 本质
typescript
JWT = Header + Payload + Signature
- Payload:用户身份声明(userId / role / exp)
- Signature:防篡改
📌 JWT ≠ 加密,只是签名
②、企业级 JWT 设计(重点)
❌ 错误做法
- JWT 放 localStorage
- 永久有效
- 前端完全信任 JWT
✅ 正确做法(金融级)
- Access Token:短期(5~30min)
- Refresh Token:长期(7~30d)
- Refresh Token 存 HttpOnly Cookie
- Access Token 仅用于请求
(5)、前端如何"正确"做用户鉴权(分层讲)
①、登录阶段(Login)
前端:
- 收集账号 / 密码 / 验证码
- HTTPS 发送
后端:
- 验证身份
- 返回 Token
typescript
{
"accessToken": "xxx",
"expiresIn": 1800,
"user": {
"id": 1,
"name": "Alice"
}
}
②、凭证存储(非常关键)
企业级推荐方案:
| 凭证 | 存储位置 |
|---|---|
| Access Token | 内存 / Store |
| Refresh Token | HttpOnly Cookie |
📌 防 XSS + 防 CSRF 的平衡方案
③、请求鉴权(Request Authentication)
前端统一拦截器:
typescript
axios.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${accessToken}`
return config
})
④、过期处理(企业级重点)
typescript
接口返回 401
↓
使用 Refresh Token
↓
刷新 Access Token
↓
重放原请求
📌 用户无感刷新登录态
(6)、一个「简单但完整」的企业级典例
下面是一个典型后台管理系统的鉴权方案。
技术栈:
前端:React / Vue SPA
后端:REST API
鉴权:JWT + 双 Token
场景:金融后台
①、登录流程
typescript
/login
↓
校验账号密码
↓
返回 accessToken
↓
refreshToken 写入 HttpOnly Cookie
②、前端核心代码(示意)
登录成功处理:
typescript
store.setAccessToken(token)
store.setUser(userInfo)
请求拦截器:
typescript
axios.interceptors.response.use(
res => res,
async err => {
if (err.response.status === 401) {
await refreshToken()
return axios(err.config)
}
return Promise.reject(err)
}
)
路由守卫(鉴权入口):
typescript
if (!accessToken && to.path !== '/login') {
redirect('/login')
}
③、后端核心原则(前端要懂)
所有接口校验 Token:
- Token 校验独立于菜单 / 权限
- Token 失效立即拒绝请求
📌 鉴权是"所有接口"的第一道门
2、黑白名单
黑白名单并不属于权限模型,而是一种高优先级的访问控制与风控机制。
在系统中,它通常位于身份认证和权限校验之后,
前端仅负责展示与引导,最终是否允许访问由后端统一裁决。
权限决定"正常能不能做",黑白名单决定"特殊情况下能不能做"。
(1)、黑白名单概念
黑白名单是一种"强制访问控制与风控兜底机制",
用于在身份与权限校验通过之后,对特定主体进行"额外禁止或额外放行"。
📌 核心关键词:
强制 / 兜底 / 高优先级
黑白名单解决的不是"权限问题":
| 问题 | 是否由黑白名单解决 |
|---|---|
| 你是谁 | ❌ |
| 你有什么权限 | ❌ |
| 是否强制禁止 | ✅ |
| 是否特权放行 | ✅ |
| 是否临时控制 | ✅ |
📌 黑白名单永远高于角色、权限、菜单
(2)、黑白名单在系统中的"正确位置"
标准安全顺序(金融系统):
- 身份认证(Authentication)
- 权限校验(Authorization)
- 黑白名单 / 风控校验(Access Control)
- 业务执行
黑白名单不是前置认证,而是"最终闸门"
(3)、黑白名单的业务分类(前端必须理解)
①、按"对象"分类
| 对象 | 示例 |
|---|---|
| 用户 | 封禁账号、风控用户 |
| 角色 | 禁止某类用户操作 |
| IP | 风控 IP |
| 设备 | 异常设备 |
| 接口 | 临时封禁能力 |
📌 前端通常只感知"用户级 / 功能级"
②、按"作用方式"分类
黑名单(Block):
- 强制禁止
- 无视权限
- 常用于风控
白名单(Allow):
- 强制放行
- 可绕过部分限制
- 常用于内部 / 灰度
(4)、前端在黑白名单中的真实职责(重点)
⚠️ 非常关键的一点:
- 前端不是黑白名单的裁决者,只是"感知者"和"执行者(展示层)"。
前端能做什么?
| 能力 | 是否允许 |
|---|---|
| 隐藏入口 | ✅ |
| 禁用按钮 | ✅ |
| 操作前提示 | ✅ |
| 阻断请求 | ⚠️(体验层) |
| 决定是否安全 | ❌ |
🚫 前端不能做什么?
- 决定是否真正允许
- 作为唯一拦截手段
- 依赖本地逻辑保证安全
(5)、企业级黑白名单的"数据模型"(你要会看)
后端返回的典型结构:
typescript
{
"userId": 1001,
"blacklist": {
"global": false,
"actions": ["withdraw", "transfer"],
"reason": "风控冻结"
},
"whitelist": {
"actions": ["internal-debug"]
}
}
📌 特点:
- 不是简单 true / false
- 有作用范围
- 有原因
- 可动态调整
(6)、前端如何"正确"使用黑白名单(分层讲)
①、初始化阶段(登录后)
typescript
登录成功
↓
获取用户画像(含名单状态)
↓
写入权限上下文(Store)
②、菜单 / 页面层控制
typescript
if (blacklist.actions.includes('withdraw')) {
hideMenu('withdraw')
}
📌 只做展示控制
③、操作前校验(体验级)
typescript
function beforeAction(action) {
if (blacklist.actions.includes(action)) {
showToast('当前账号已被限制操作')
return false
}
return true
}
📌 防止无意义请求 + 明确提示用户
④、接口异常兜底(关键)
即使前端没拦住:
typescript
接口请求
↓
后端黑名单校验
↓
返回 403 / 451
↓
前端统一提示
📌 这是最终安全兜底
(7)、一个「简单但企业级」的典例
场景说明:金融后台系统
功能:提现
风控:部分用户被临时冻结提现
①、后端规则(你要懂)
typescript
if (user in withdraw_blacklist) {
reject request
}
②、前端权限上下文(示意)
typescript
const authContext = {
permissions: ['withdraw'],
blacklist: ['withdraw']
}
③、前端按钮控制
html
<Button
disabled={blacklist.includes('withdraw')}
onClick={handleWithdraw}
>
提现
</Button>
④、操作前提示
typescript
function handleWithdraw() {
if (blacklist.includes('withdraw')) {
alert('账号风控中,暂不可提现')
return
}
submitWithdraw()
}
⑤、接口兜底处理
typescript
axios.post('/withdraw').catch(err => {
if (err.status === 403) {
alert('操作被系统拦截')
}
})
📌 前后端双层拦截
3、动态菜单
动态菜单本质上是权限与访问控制结果在前端导航层的映射,
它的目标是降低误操作风险、提升可用性与合规性,
但并不参与最终的安全裁决,真正的权限校验必须由后端完成。
动态菜单不是"你能不能做",而是"你该不该看到"。
(1)、动态菜单的概念
动态菜单,是"用户权限与访问控制结果在前端的导航层映射",用于决定"当前用户能看到哪些功能入口"。
📌 关键词:
权限结果 / 导航层 / 展示与引导
🚫 动态菜单不解决什么?
- 不解决身份认证
- 不解决真正的安全校验
- 不防止接口越权
📌 它不是安全机制,而是"权限的 UI 表现"
(2)、动态菜单在系统中的"正确位置"
在企业 / 金融系统中,完整链路是:
typescript
身份认证(你是谁)
↓
权限 / 黑白名单(你能不能)
↓
动态菜单(你能看到什么)
↓
路由访问
↓
具体操作
菜单一定发生在"权限已确定之后"
(3)、为什么企业系统一定要用动态菜单?
业务复杂度决定的:
- 不同角色看到完全不同系统
- 功能频繁开关 / 灰度
- 多租户 / 多业务线
📌 静态菜单根本不可维护
金融系统的现实需求:
- 降低误操作风险
- 减少越权尝试
- 强化合规性("你不该看到的就不要看到")
(4)、动态菜单的三种主流设计模式(前端必须懂)
①、后端全量下发菜单(最常见)
typescript
后端根据权限
↓
直接返回可用菜单树
↓
前端直接渲染
特点:
- 前端最简单
- 权限集中在后端
- 金融系统最常用
📌 推荐指数:⭐⭐⭐⭐⭐
②、前端根据权限过滤菜单(次常见)
typescript
后端返回权限点
↓
前端本地菜单表
↓
根据权限过滤
特点:
- 前端更灵活
- 维护成本高
- 易出错
📌 适合中后台、内部系统
③、混合模式(企业级常见)
typescript
后端返回菜单骨架 + 权限
↓
前端补充结构 / 本地能力
📌 大型系统 / 微前端常用
(5)、动态菜单的"标准数据模型"
企业级菜单一定是树结构 + 元数据:
typescript
[
{
"key": "fund",
"title": "资金管理",
"icon": "money",
"path": "/fund",
"children": [
{
"key": "withdraw",
"title": "提现",
"path": "/fund/withdraw",
"permission": "withdraw"
}
]
}
]
核心字段说明:
| 字段 | 作用 |
|---|---|
| key | 唯一标识 |
| title | 菜单名 |
| path | 路由路径 |
| permission | 所需权限 |
| children | 子菜单 |
📌 菜单 ≠ 路由,但通常一一对应
(6)、前端做动态菜单的"标准步骤"
①、登录后初始化权限上下文
typescript
登录成功
↓
获取用户信息(角色 / 权限 / 菜单)
↓
存入 Store
②、根据权限生成菜单树
typescript
function filterMenu(menu, permissions) {
return menu
.filter(item =>
!item.permission || permissions.includes(item.permission)
)
.map(item => ({
...item,
children: item.children
? filterMenu(item.children, permissions)
: []
}))
}
📌 纯函数、可测试
③、渲染菜单(UI 框架无关
html
<Menu>
{menu.map(item => (
<MenuItem key={item.key} to={item.path}>
{item.title}
</MenuItem>
))}
</Menu>
④、路由层兜底(非常关键)
即使菜单隐藏了:
typescript
用户手输 URL
↓
路由守卫校验权限
↓
无权限 → 403
📌 菜单永远不能当路由安全
(7)、一个「简单但企业级」的动态菜单典例
场景说明:金融后台
角色:普通用户 / 管理员
功能:查看账户 / 提现 / 审批
①、后端返回数据(示意)
typescript
{
"permissions": ["view", "withdraw"],
"menu": [
{
"key": "account",
"title": "账户",
"path": "/account"
},
{
"key": "withdraw",
"title": "提现",
"path": "/withdraw",
"permission": "withdraw"
},
{
"key": "approve",
"title": "审批",
"path": "/approve",
"permission": "approve"
}
]
}
②、前端生成菜单
typescript
const menu = filterMenu(menuFromServer, permissions)
结果只剩:
- 账户
- 提现
③、路由守卫兜底
typescript
if (!hasPermission(route.permission)) {
redirect('/403')
}
📌 企业级必备