================================================================================ EasyGoAdmin 后端开发者文档 · 03 认证授权与多租户 ================================================================================ 代码位置:internal/logic/auth.go、index.go、role.go、menu.go、tenant.go、 user.go、online_user.go、data_scope.go(数据权限);utility/jwt; utility/captcha;utility/crypto -------------------------------------------------------------------------------- 1. JWT 双令牌(utility/jwt/jwt.go) -------------------------------------------------------------------------------- 算法 HS256;密钥 jwt.secret;签发者 jwt.issuer(默认 easygoadmin)。 Claims(golang-jwt/v5 + RegisteredClaims): user_id / tenant_id / username / realname / typ(access|refresh) RegisteredClaims:jti(随机)、iss、iat、exp(exp 必填,Parse 强校验) 有效期配置: 访问令牌 jwt.accessTokenExpire 默认 7200s(2h) 刷新令牌 代码读取 jwt.refreshExpire,默认 604800s(7d) (config.yaml 中的 refreshTokenExpire 为同值说明项,运行时以 jwt.refreshExpire 为准,未配置走 7 天默认值) Sign(ctx, tokenType, userID, tenantID, username, realname) → (token, claims, err) Parse(ctx, tokenStr) → 校验 HMAC 方法(WithValidMethods HS256)、签名、 过期、签发者;失败统一返回 401 "Token无效或已失效"。 ExpireDuration(ctx, tokenType) → 有效期(黑名单 TTL 计算用)。 黑名单:easygoadmin:token:blacklist:{jti} = "1",TTL = 令牌剩余有效期。 写入时机:登出、refresh 轮换旧令牌、RemoveToken 指定撤销、在线用户强退 (访问+刷新一并拉黑)。 -------------------------------------------------------------------------------- 2. 图形验证码(utility/captcha) -------------------------------------------------------------------------------- GET /api/captcha → Captcha 生成(utility/captcha.Generate): * 6 位字符(captcha.length,4~8),字符集排除易混淆字符 * 缓存 easygoadmin:captcha:{uuid},TTL captcha.expire(300s) * 渲染 PNG(TTF 字体 resource/static/fonts/Vera.ttf + 干扰线/噪点), 字体缺失降级 SVG * 返回 {key: uuid, captcha: "data:image/...;base64,..."}(前端直接展示) Verify(ctx, key, code):一次性校验(命中即删除);配置 captcha.bypassCode 可免校验(仅开发调试,生产留空)。 -------------------------------------------------------------------------------- 3. 登录闭环(logic/auth.go Login) -------------------------------------------------------------------------------- POST /api/login(公开),入参 {username, password, tenantCode?, code?, key?} 流程(每一步失败均写登录日志 log_type=1 status=1): 1) 提供 key 时强制验证码校验(captcha.Verify) 2) findUser:username 全局查找;带 tenantCode 先按 code 定位租户再过滤; 多租户同名 → 优先平台租户(tenant_id=0),仍歧义 → 提示携带租户编码 3) 账号状态 status==1 校验(1002) 4) 租户状态与过期校验(mustLoadTenant,60s 缓存;1006/1005) 5) 密码:crypto.VerifyPassword(bcryptHash, password, salt) —— bcrypt(password+salt) 双重加盐;IsBcrypt 识别旧 md5 数据(登录会拒绝, 需重置密码);salt 为空兼容直接比对 6) 签发双令牌(access + refresh,claims 含 user/tenant/username/realname) 7) 写登录日志成功(log_type=1 status=0,含 ip/location(os 归属地)/os/browser/ consume_time;code 参数脱敏为 ***) 8) MarkOnline 登记在线会话(见第 7 节) 返回(OAuth2 惯例命名,勿改驼峰): {access_token, refresh_token, expires_in(秒), token_type:"Bearer"} 登录日志字段:username/log_type/source(0 后台)/method/request_method/url/param/ result(成功|失败)/ip/location/os/browser/consume_time/status/error。 3.1 登录时序图 Client Server/auth Redis DB │ GET /api/captcha │ │ │ │────────────────────▶│ Generate ─────────▶│ SET captcha:uuid│ │◀── {key, 图片base64} │ │ │ │ POST /api/login {username,password,code,key,tenantCode?} │────────────────────▶│ Verify ───────────▶│ GET+DEL captcha:uuid │ │ findUser ──────────────────────────▶│ user(+tenant) │ │ status==1? mustLoadTenant ────────▶│ tenant(60s缓存) │ │ bcrypt(password+salt) 比对 │ │ │ Sign(access) + Sign(refresh) │ │ │ writeLoginLog(成功/失败) ──────────▶│ login_log │ │ MarkOnline ───────▶│ ZAdd online:jti:set │ │ │ SET online:info:{jti} │◀── {access_token, refresh_token, expires_in, token_type} │ 3.2 令牌生命周期状态图 Sign(access 2h) Refresh 轮换 / Logout / 强退 ────────────────▶ 有效 ────────────────────────────────────────▶ 黑名单 ▲ │ JWT中间件每请求: Parse + 黑名单Exists │ │ ▼ ▼ │ 到期(exp) ──前端──▶ POST /api/oauth2/token 自然过期(TTL │ (refresh_token 模式) = 剩余有效期)自动清理 └── 旧 refresh 已拉黑则 1008;否则旧 refresh 入黑名单 + 签发新对 3.3 RBAC 授权链路图 请求 ──▶ JWT中间件 ──▶ LoadIdentity(userID) │ user ─────────┤ dept_id → DeptId[](数据权限用) user_role ────┤ role.code → Roles[] │ role.data_scope → DataScope[](多角色取最宽松) role_menu ────┤ menu(type=1, ┤ permission → Perms[] status=0, │ hide=0) ────┘ ▼ 缓存 user:identity:{uid} JSON TTL 300s ▼ Bind 包装层 HasPerm("sys:user:page"): permission=="" → 通过 │ Roles 含 ROLE_SUPER_ADMIN → 通过 │ Perms 精确匹配 → 通过 │ 否则 403 数据范围经 ctx(WithDataScope/WithDeptID)随行, 在 applyFilters 末环过滤(见第 6 节) 缓存失效:角色保存权限/改数据范围 / 用户分配角色/改部门 等主动 Del,过期兜底 -------------------------------------------------------------------------------- 4. 令牌刷新与撤销 -------------------------------------------------------------------------------- POST /api/oauth2/token(公开)Refresh: * grant_type=password 或无 refresh_token → 兼容密码模式,复用 Login * refresh_token 模式: - Parse 校验 + typ 必须为 refresh(1008) - 旧 refresh 已在黑名单 → 1008(防重放) - 旧 refresh 入黑名单(轮换) - 签发新 access+refresh,MarkOnline 刷新在线登记 - 返回同登录格式四字段 GET /api/oauth2/check/token 登录可用;JWT 中间件通过即 {valid:true, userId} DELETE /api/oauth2/remove/:token 登录可用;解析指定 token 的 jti 入黑名单 GET /api/logout 当前 jti 入黑名单 + MarkOffline + 登录日志(log_type=2) -------------------------------------------------------------------------------- 5. RBAC 权限模型 -------------------------------------------------------------------------------- 表关系:user —(user_role)— role —(role_menu)— menu(type=1 权限节点) 身份加载 LoadIdentity(JWT 中间件调用,缓存 300s,键 user:identity:{uid}): 1) user 行回查 dept_id → DeptId(数据权限过滤用;单列查询) 2) user_role → roleIds → role.code 集合(Roles) 同一查询顺带读 role.data_scope:多角色取最宽松(数值 min, 1 最宽 4 最严;缺列值按"全部"参与比较)→ DataScope 3) role_menu → menuIds → menu 中 type=1 且 status=0 且 hide=0 的 permission 非空去重集合(Perms) 4) 超管/平台:DataScope 强制"全部"(过滤层另有 dataScopeBypass 双保险) 权限判定 consts.HasPerm(ctx, "sys:user:page"): permission 为空 → 通过(仅登录即可) Roles 含 ROLE_SUPER_ADMIN → 通过(超管全放行) Perms 精确匹配 → 通过;否则 403。 前端识别超管通配权限 *:*:*(getUser 返回)放开全部按钮权限。 权限点声明:接口 gmeta permission 标签;生成器自动创建 sys:{module小驼峰}:{page|list|detail|add|update|delete|batchDelete|status|import|export} 节点并挂到模块菜单下。 角色管理扩展接口: GET /api/role/menu/list/:roleId 查询角色已勾选菜单(sys:role:getPermission) POST /api/role/menu/save 重写 role_menu + 清理相关用户身份缓存 (sys:role:savePermission) 菜单树 GetUserMenuTree:user→role→role_menu→menu 组装(index.GetMenus 消费); 超管返回全量菜单。 缓存失效:角色权限变更/改数据范围(roleSpec.AfterUpdate → roleClearIdentityCache)/ 用户分配角色/用户改部门(user.Update → AssignRoles → clearIdentityCache)后清理 user:identity:{uid} (及 user:perms/user:roles 键),最迟 300s 自然过期兜底。 6. 角色数据权限(行级,role.data_scope,logic/data_scope.go) 与租户隔离(第 8 节,硬规则)独立叠加的第二层行级过滤,四档: 档位 含义 过滤条件(作用于挂接模块的列表行) ---- -------------- ----------------------------------------- 1 全部 不过滤(租户隔离之内全量) 2 本部门及以下 dept_id IN (自身部门 + 全部子孙部门ID) 3 本部门 dept_id = 自身部门ID 4 仅本人 id = 自身用户ID 模块接线(一行声明): userSpec = ModuleSpec[...]{ ..., DataScope: ApplyDataScope("dept_id") } ApplyDataScope(deptColumn) 返回 DataScopeFunc 闭包,挂进 applyFilters 末环(Like→Eq→Keyword→Filter→DataScope),Page/List/导出自动生效。 目前仅 user 模块接线(全库仅 goframe_user 有 dept_id 列); 其他模块需要时声明同款闭包即可,无部门归属列的表可按需扩展 按创建人过滤的变体。 安全语义(与直觉不同的点): * 多角色取最宽松(并集直觉),合并发生在 LoadIdentity 缓存层, 热路径零额外查询; * 超管/平台完全绕过(IsPlatform 或 Roles 含 ROLE_SUPER_ADMIN, dataScopeBypass 双判断); * 未分配部门(dept_id=0)的用户挂档 2/3 时降级为"仅本人" (防 dept_id=0 意外放大可见面); * 仅作用于 Page/List/导出(applyFilters 链);Detail/Update/Delete 不过滤,由 permission 节点与租户隔离兜底(边界见 17 章第 8 节); * 部门子树 deptSubtreeIds:dao.Dept.Ctx 拉全租户部门行(id,parent_id) 后 BFS(树小直查,无缓存;成热点可加短时缓存)。 配置链路: role 表 data_scope 列(默认 1)→ 角色表单"数据范围"下拉(字典 role_data_scope 驱动,值 1/2/3/4)→ roleToData 归一(缺省回 1) → 角色 Update 后 AfterUpdate 清该角色全部用户身份缓存 → 生效。 -------------------------------------------------------------------------------- 7. 个人中心(logic/index.go) -------------------------------------------------------------------------------- GET /api/index/getUser 当前用户信息(超管返回通配权限 *:*:*) GET /api/index/getMenus 当前用户菜单树(无参版,前端 common 菜单) GET /api/index/getMenus/:id 同上(system/menu 页面调用) PUT /api/index/updatePassword 旧密码校验(1012)+ bcrypt 新密码(事务外预计算) PUT /api/index/updateProfile 更新个人信息(realname/avatar/mobile/email 等) -------------------------------------------------------------------------------- 8. 多租户 -------------------------------------------------------------------------------- 7.1 数据模型 goframe_tenant:code(T+4位ID 规则由 Account 生成)、status(1 正常/2 禁用)、 expire_time(nil=永不过期)、max_user_num 等。 9 张租户级表带 tenant_id 列(见 01 章 5.3)。 7.2 读侧隔离 dao.Ctx(hooks.ApplyCtx):非平台上下文自动追加 tenant_id IN (0, 自身); 超管(ctx=0)不加过滤。写侧禁止手写该条件。 7.3 写侧归属 insertHook:租户用户插入未显式指定时自动填自身(防逃逸);超管未指定时填 tenant.defaultId(默认 1),显式 0 可建平台共享行,显式具体值从显式值。 业务取归属租户统一 consts.GetTenantID(ctx)。 7.4 请求级校验 Tenant 中间件(见 02 章)每请求校验 status/expire(60s 缓存); 登录路径 mustLoadTenant 同缓存复用。 7.5 租户开户(tenant.Account,POST /api/tenant/account) 创建租户 + 初始管理员:事务内建租户(code 生成)+ 建管理员用户 + 分配角色;max_user_num 上限校验(1007)在 user.Add 生效 (checkTenantUserMax)。事务纪律:bcrypt 等 CPU 计算在事务外预计算, 事务内不做 HTTP/文件 IO。 7.6 其他 tenant 用户列表:GET /api/tenant/users/:tenantId 租户状态变更后缓存 tenant:info:{id} 需清理(tenant 模块 clearCache 钩子)。 -------------------------------------------------------------------------------- 9. 用户模块要点(logic/user.go) -------------------------------------------------------------------------------- * 手写唯一校验:username/mobile 按“行现租户”检查(uk_tenant_username / uk_tenant_mobile),Update 时以被改行的 tenant_id 为准(预检范围必须等于 唯一索引范围,详见 module_spec 章唯一校验说明)。 * Add:租户用户数上限校验;密码 bcrypt(事务外);genUserCode 生成用户编码; AssignRoles 写 user_role。 * Update:AssignRoles 同步角色 + clearIdentityCache;城市编码经 ResolveCityCodes 存库、详情经 ResolveCityNames 还原。 * ResetPwd(PUT /api/user/resetPwd,sys:user:resetPwd):重置密码(bcrypt)。 * Import(POST /api/user/import):Excel(excelize)解析 → 逐行校验 → 批量插入(含角色/城市/密码默认);Export:ChunkQuery 分批防 OOM, excel.WriteSheet 落临时文件返回 URL。 * 删除守卫:userDeleteGuard / 关联清理(user_role)。 -------------------------------------------------------------------------------- 10. 在线登记(logic/online_user.go MarkOnline/MarkOffline) -------------------------------------------------------------------------------- 登录/刷新成功后登记: zset easygoadmin:online:jti:set score=access 过期时间戳 详情键 easygoadmin:online:info:{jti} = JSON(含 userId、username、 realname、ip、location、os、browser、loginTime、access/refresh jti 与 起止时间戳、令牌原文(详情查看用)、tokenType、expiresIn),TTL=access 剩余有效期+60s。 登出/强退 MarkOffline:ZRem + Del。 在线用户模块的分页/详情/踢出见 06 章。 -------------------------------------------------------------------------------- 11. 安全清单 -------------------------------------------------------------------------------- * 密码:bcrypt(password+salt),Cost=DefaultCost;旧 md5 数据被 IsBcrypt 拒绝 * 令牌:HS256 + jti 黑名单 + refresh 轮换(旧 refresh 拉黑防重放) * 强退:访问+刷新令牌一并拉黑(防被踢会话经 refresh 复活) * 限流:认证入口独立更严阈值(authLimit=10/分钟/IP) * 脱敏:操作日志 param 中 password/salt/access_token/refresh_token → *** * 代理:仅受信代理的 XFF 生效(api.proxyIps) * 会话固定:登录信息全部由服务端签发 claims 承载,无 cookie 会话