后台系统中的认证与授权解决的是两个不同问题:

  • 认证用于确认当前请求来自哪个用户。
  • 授权用于判断这个用户可以执行哪些操作、访问哪些数据。

在 IAM 平台中,授权又被分为功能权限和数据权限。功能权限控制菜单、按钮和接口,数据权限控制用户能够看到或处理的数据范围。

🔐 用户认证

用户登录时,系统先根据用户名查询账号,使用 BCrypt 校验密码,并检查账号是否处于启用状态。验证通过后生成 JWT,Token 中只保存用户身份,不直接保存角色和权限列表。

用户名和密码

校验账号、密码与用户状态

生成 JWT

在 Redis 中保存当前有效 Token

返回 Token 和过期时间

后续请求携带 Bearer Token。认证中间件负责验证签名、签发方和有效期,解析出用户 ID,并把当前用户保存到请求上下文中。

系统还会将请求中的 Token 与 Redis 中保存的当前 Token 比较。这样虽然不再是完全无状态的 JWT,但可以支持主动退出和 Token 失效:

  • 用户退出后删除 Redis 中的 Token,旧 Token 立即失效。
  • 同一用户产生新 Token 后,之前的 Token 无法继续使用。
  • 权限不需要写入 JWT,角色发生变化时不用等待 Token 过期。

登录等公开接口可以跳过认证;获取当前用户信息、修改本人密码等接口只要求登录,不再检查具体功能权限。

🧭 授权模型

系统以 RBAC 为基础:用户关联角色,角色关联资源。

用户 ── 用户角色 ── 角色 ── 角色资源 ── 资源
  │                                      ▲
  └──────────── 用户直接资源 ─────────────┘

大部分权限通过角色统一分配。对于临时或特殊授权,也可以直接给用户分配资源,不必为少量例外创建新的角色。

用户的最终权限是启用角色权限与用户直接权限的并集:

用户有效权限 = 启用角色拥有的权限 ∪ 用户直接拥有的权限

超级管理员只进行身份和登录状态检查,随后跳过普通资源授权。

🎛️ 功能权限

功能权限回答的是“用户能不能执行某个操作”。系统使用统一的资源模型表示:

  • 系统模块。
  • 目录和菜单。
  • 页面按钮或自定义功能点。
  • IAM 平台自身的 API。
  • 外部系统的 API。

每个资源都有稳定的资源编码,同时保存资源类型、访问路径和启用状态。角色或用户实际获得的是资源,而不是直接绑定 Controller 或页面组件。

前端获取当前用户信息时,系统根据用户拥有的资源生成模块列表、菜单路由和功能权限码。后端收到业务请求时,再根据同一套资源权限进行校验。

一次请求的检查顺序如下:

验证 JWT

确认 Token 仍然有效

读取当前用户

判断接口是否需要功能授权

检查用户是否拥有对应资源

检查资源是否启用并与当前接口匹配

进入业务逻辑

这样,前端隐藏按钮只是用户体验控制,真正的安全边界仍然位于后端。即使客户端手动构造请求,没有对应功能权限也无法进入业务接口。

🗃️ 数据权限

功能权限判断用户能否调用某个功能,数据权限则表示用户在其他业务系统中被分配了哪些具体数据,例如频道、栏目或其他业务对象。

系统没有实现按部门、本人或组织层级自动过滤的数据范围,而是支持两种数据权限来源:

  1. 通过 HTTP 调用其他服务提供的接口,定期同步业务数据。
  2. 由管理员直接配置 JSON 格式的数据权限。

从其他服务同步

远程数据权限会配置所属外部服务和查询地址。后台运行着专门的定时服务,每 30 秒扫描远程类型的数据权限,通过 HTTP 调用对应业务服务。

外部接口返回固定结构的响应,其中 data 是对象数组。数组中的每个对象至少需要包含:

  • guid:数据的稳定唯一标识。
  • name:分配界面和返回结果中显示的名称。

对象还可以携带业务需要的其他字段,IAM 平台会保留完整对象。远程查询成功后,以数据权限定义 ID 组成 Redis Key,再以对象的 guid 作为 Hash Field,将完整 JSON 对象缓存到 Redis。

后台定时服务
  ↓ HTTP
其他业务服务
  ↓ 固定格式的对象数组
校验 guid

按 guid 缓存完整对象到 Redis

这种方式适合频道等由其他业务系统维护、内容会动态变化的数据。IAM 平台不复制它们的业务表,只缓存用于授权的数据快照。

自定义 JSON

对于不需要从其他服务同步的数据,管理员可以直接配置 JSON 对象数组。数据格式与远程方式保持一致,每个对象同样需要包含 guidname,完整数组存储在数据权限定义的 PermContent 字段中。

[
  { "guid": "channel-news", "name": "新闻频道" },
  { "guid": "channel-sports", "name": "体育频道" }
]

远程方式和自定义方式只是在数据来源与存储位置上不同:

类型候选数据来源候选数据存储位置
远程同步其他服务的 HTTP 接口Redis Hash
自定义配置管理员填写的 JSON数据库 PermContent

两种方式最终都转换成相同的对象数组,因此分配界面和用户查询逻辑不需要关心数据来自哪里。

权限分配

进入权限分配页面时,系统会读取所有数据权限定义:远程类型从 Redis 取得候选对象,自定义类型从数据库解析 JSON,然后把两种来源组合成统一的权限树。

管理员选择具体对象后,系统不会把完整 JSON 再复制一份,而是保存:

数据权限定义 ID + 对象 guid

分配结果仍然持久化到数据库,并且支持角色分配和用户直接分配:

角色 ── 角色数据权限 ── 数据权限
用户 ── 用户数据权限 ── 数据权限

查询用户的数据权限

查询某个用户的信息时,系统先从数据库合并角色数据权限和用户直接数据权限,得到用户已分配的“定义 ID + guid”。随后根据数据权限类型还原完整对象:

  • 远程类型从 Redis 中读取对应 guid 的对象。
  • 自定义类型从数据库中的 JSON 数组查找对应 guid 的对象。

最后按照数据权限的名称和编码进行分组,把匹配到的完整对象数组返回。这样远程数据更新后,用户下次查询可以获得 Redis 中的最新对象内容,而数据库中的分配关系仍然保持稳定。

⚡ 权限缓存与生效

功能权限和远程数据权限采用不同的缓存策略。

功能权限采用预热与懒加载结合的方式:

  • 登录成功后预先加载用户的 API 和自定义功能权限。
  • 请求到达时,如果缓存不存在,再从数据库计算。
  • 用户、角色或资源权限变化后删除相关缓存。
  • 下一次请求重新计算最新权限。

权限变更时不尝试逐项修改缓存,而是直接删除受影响用户的权限 Key。这种方式实现简单,也能减少增量更新遗漏造成的脏数据。

资源路径和启用状态还会缓存在每个应用实例的内存中。资源发生变化后,通过 Redis Pub/Sub 通知所有实例重新加载,避免多实例之间使用不同版本的资源信息。

远程数据权限则由后台服务周期性刷新到 Redis。自定义数据权限和最终分配关系始终保存在数据库中,不依赖 Redis 持久化。

🔄 完整请求链路

用户登录、功能授权和数据权限查询的关系如下:

HTTP 请求

JWT 身份认证

Redis 登录状态检查

功能权限检查

Controller / Service

查询数据库中的用户分配关系

远程类型读取 Redis / 自定义类型读取数据库

组合并返回完整数据权限对象

认证失败时直接拒绝请求,功能权限不足时不能进入业务逻辑。查询用户信息时,系统再按照已保存的定义 ID 和 guid,从对应来源还原完整的数据权限对象。

🗣️ 面试时怎么介绍

可以这样概括这套设计:

系统使用 RBAC 管理用户、角色和资源,同时支持用户直接授权。认证使用 JWT,Redis 保存当前有效 Token 和功能权限。授权分为功能权限和数据权限:功能权限统一控制菜单、按钮和 API;数据权限支持从其他服务的 HTTP 接口定时同步,也支持直接配置 JSON。两种数据源都使用包含 guidname 的固定对象数组,分配时只把数据权限定义 ID 和对象 guid 存入数据库。查询用户时,再分别从 Redis 或数据库还原完整对象并组合返回。

这套设计的重点是把三个边界分开:

  • JWT 负责证明用户身份。
  • 功能权限负责控制用户能做什么。
  • 数据权限负责管理用户被分配的具体业务数据对象。

三者职责明确后,权限模型更容易扩展,也更容易在面试中说明设计原因和请求执行过程。