认证鉴权流程说明
2026/7/23大约 4 分钟
认证鉴权流程说明
一、登录流程
1.1 用户名密码登录 (POST /api/login)
SysLoginController.LoginAsync()
│
├─ 验证账号密码
├─ 查询用户角色: roleService.SelectUserRoleListByUserId()
├─ 查询用户权限: permissionService.GetMenuPermission()
│
├─ 构建 LoginUser:
│ ├─ UserId, UserName, DeptId, NickName ← 从用户表
│ ├─ Roles, RoleKeys ← 从角色表
│ ├─ Permissions ← 从权限表 (完整列表, 如 system:user:list)
│ ├─ ScopeType ← 登录时预计算 (取所有角色中最宽松的权限)
│ └─ TenantId ← 请求头 tenantId
│
└─ 生成 JWT:
JwtUtil.GenerateJwtToken(AddClaims(loginUser))
│
└─ AddClaims 写入 JWT Claims:
├─ ClaimTypes.PrimarySid = UserId
├─ ClaimTypes.Name = UserName
├─ ClaimTypes.GroupSid = DeptId
├─ ClaimTypes.PrimaryGroupSid = TenantId
├─ ClaimTypes.UserData = JSON(LoginUser) ← 完整用户信息
└─ "sensitivePerms" = p:* 开头的敏感权限 (完整Permissions不入JWT, 太大)
注意: 登录时
Permissions只存在 LoginUser 对象上,未写入 Redis 缓存。登录后前端会立刻调GetUserInfo完成写入。
1.2 获取用户信息 (GET /api/getUserInfo)
SysLoginController.GetUserInfo()
│
└─ 写入 Redis 缓存:
├─ CacheService.SetUserPerms(UserPermKEY + userId, permissions) ← 权限列表入缓存
└─ CacheService.SetDataScopeDeptIds(userId, deptIds) ← 数据权限部门ID入缓存
二、中间件管道
管道顺序 (Program.cs)
请求 →
1. GlobalExceptionMiddleware 全局异常拦截
2. UseRequestLocalization 国际化
3. UseForwardedHeaders 反向代理IP转发
4. 内联中间件 EnableBuffering + Query token→Header
5. UseStaticFiles 静态文件
6. UseRouting 路由匹配
7. UseCors 跨域
────────────────────── 认证/授权区 ──────────────────────
8. TenantResolveMiddleware 租户一致性校验 ← User 可能还未挂载,自行解析JWT
9. UseAuthentication() 标准认证中间件
10. JwtAuthMiddleware JWT 解析 + Token刷新 + 设置 context.User ★
11. TenantFeatureMiddleware 租户套餐功能开关拦截
12. UseAuthorization() 标准授权中间件
─────────────────────────────────────────────────────────
13. UseResponseCaching
14. 业务中间件 (限流、SignalR、Swagger...)
→ Controller Action
各中间件职责
| # | 中间件 | 职责 | 跳过条件 |
|---|---|---|---|
| 8 | TenantResolveMiddleware | 校验 header.tenantId 与 token 中 tenantId 一致性;缺失时自动补全 | IsTenantEnabled=false 或 [AllowAnonymous] |
| 10 | JwtAuthMiddleware ★ | 解析 JWT → LoginUser;临近过期时刷新 Token;挂载 context.User;存入 context.Items | 静态资源、白名单、[AllowAnonymous] |
| 11 | TenantFeatureMiddleware | 按租户套餐拦截无权限的菜单/功能 | — |
三、JwtAuthMiddleware 详解
JwtAuthMiddleware.InvokeAsync()
│
├─ 静态资源 (含 ".") → 跳过
├─ 白名单路径 → 跳过
├─ [AllowAnonymous] → 跳过
│
├─ 解析 JWT: JwtUtil.GetLoginUser(context)
│ └─ 从 Authorization Header 取 token → ValidateJwtToken → 反序列化 LoginUser
│
├─ Token 为空 → 返回 401
│
├─ Token 临近过期 (≤ RefreshTokenTime 分钟, 默认5) → 刷新
│ └─ 防并发: CacheHelper 锁
│ └─ 响应头: X-Refresh-Token = 新Token
│
├─ ★ 存入 Items: context.Items["__CurrentUser"] = loginUser
│ └─ 后续所有读取都从 Items 取, 避免重复解析 JWT
│
├─ 挂载 context.User:
│ └─ new ClaimsIdentity(AddClaims(loginUser), "jwt")
│
└─ await _next(context)
四、用户信息获取 (多入口统一到 Items)
本次重构后,所有用户信息读取统一收敛到 JwtAuthMiddleware 写入 HttpContext.Items["__CurrentUser"] 的 LoginUser 对象,业务代码无需自行解析 JWT。
推荐用法(走 Items 缓存,零重复解析)
// Controller 中获取
long userId = HttpContext.GetUId(); // 用户ID
string userName = HttpContext.GetName(); // 用户名
long deptId = HttpContext.GetDeptId(); // 部门ID
// Service 层获取(通过 App.HttpContext,等价于上面)
string userName = App.HttpContext.GetName();
long userId = App.HttpContext.GetUId();
// 获取完整登录信息(含 Permissions,首次从 Redis 补全并回写 Items)
LoginUser info = HttpContext.GetCurrentUser(); // Controller 内
LoginUser info = App.HttpContext.GetCurrentUser(); // Service 内
GetUId / GetName / GetDeptId内部均委托GetCurrentUser(),整个请求只解析一次 JWT, 结果缓存到Items,同请求内多次调用零开销。GetCurrentUser()在Permissions为空时 会从 Redis (UserPermKEY + userId) 补全,无需业务层手动处理。
获取租户 / 主库信息
string tenantId = App.GetCurrentTenantId(); // 当前租户ID
string mainDb = App.MainDbConfigId; // 主库 ConfigId
架构图
JwtAuthMiddleware (唯一JWT解析入口)
│
└─ context.Items["__CurrentUser"] = loginUser ← 一次解析, 全请求复用
│
├── GetCurrentUser() [HttpContextExtension] 权限+用户信息
├── ActionPermissionFilter [MVC Filter] 权限校验
├── DataPermi.GetLoginUser [SqlSugar层] 数据权限计算 (已废弃)
└── DataScopeExtensions [Repository层] 数据权限过滤
五、ActionPermissionFilter 权限校验
OnActionExecutionAsync() [每个带 [Permission("xxx")] 的 Action 都执行]
│
├─ 从 Items 获取 LoginUser
│
├─ SaaS 租户隔离: 非主租户禁止访问平台级菜单 (前缀 p:*)
│
├─ 获取权限列表:
│ ├─ 先查 Redis: CacheService.GetUserPerms(UserPermKEY + userId)
│ └─ 未命中则查 DB: permissionService.GetMenuPermission() → 回写 Redis
│
├─ 权限判断优先级:
│ 1. 拥有 *:*:* (管理员权限) → 放行
│ 2. 拥有 admin 角色 → 放行
│ 3. 权限列表包含当前 Action 所需权限 → 放行
│ 4. RolePermi 匹配 → 放行
│ └─ 都不满足 → 返回 403
│
└─ 演示模式: 屏蔽增删改等危险操作
六、数据权限过滤 (DataScopeExtensions / DataPermi)
BaseRepository.Queryable<T>()
│
└─ TryApplyScope<T>() ← 自动附加 WHERE 条件
│
├─ SysUser: WHERE UserId=@uid OR DeptId IN (用户可见部门)
├─ SysDept: WHERE DeptId IN (用户可见部门)
├─ SysRole: WHERE RoleId IN (用户拥有的角色)
├─ LoginLog: WHERE UserName=@uname
└─ OnlineLog: WHERE UserId=@uid
权限等级优先级: All(管理员) > DeptList(多部门) > Dept(本部门) > Self(仅本人)
七、LoginUser.Permissions 数据流图
┌─────────────────────────────────────────────────────────────────┐
│ 登录 LoginAsync │
│ permissions ← DB │
│ loginUser.Permissions = permissions │
│ GenerateJwtToken(AddClaims(loginUser)) │
│ └─ JWT 只含 p:* 敏感权限, 完整 Permissions 不入 JWT │
└───────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────────┐
│ GetUserInfo (前端登录后立即调用) │
│ CacheService.SetUserPerms(UserPermKEY+userId, permissions) │
│ └─ 完整权限列表写入 Redis │
└───────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────────┐
│ JwtAuthMiddleware (每个请求) │
│ JwtUtil.GetLoginUser() → LoginUser (Permissions = []) │
│ context.Items["__CurrentUser"] = loginUser │
└───────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
GetCurrentUser ActionPermissionFilter DataScopeExtensions
Items中LoginUser Items中LoginUser Items中LoginUser
Permissions为空 Permissions为空 不需要Permissions
→ Redis补全 → Redis/DB补全
→ 回写Items对象 → 本地变量,不回写Items
