userDetailsLoader.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. package loaders
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "math"
  8. "perms-system-server/internal/consts"
  9. "perms-system-server/internal/model"
  10. "perms-system-server/internal/model/productmember"
  11. "github.com/zeromicro/go-zero/core/logx"
  12. "github.com/zeromicro/go-zero/core/stores/redis"
  13. "github.com/zeromicro/go-zero/core/stores/sqlx"
  14. "golang.org/x/sync/singleflight"
  15. )
  16. const (
  17. defaultCacheTTL = 300 // 5 分钟
  18. // negativeCacheTTL 控制"用户不存在/已删除"的短期负缓存窗口;必须显著短于 defaultCacheTTL,避免
  19. // 刚刚 createUser 的合法用户被误判为不存在,但又要足够长到能吸收一波由离职用户残留 token 带来的
  20. // 无效流量(审计 M-3 所说的 DB DoS 放大路径)。
  21. negativeCacheTTL = 30 // 30s
  22. // negativeCacheMarker 是写入 Redis 的哨兵字符串;选用非合法 JSON,确保任何升级带来的 schema
  23. // 变动都不会把它误解析为真实 UserDetails。
  24. negativeCacheMarker = "_NOT_FOUND_"
  25. )
  26. // -------- UserDetails 及子结构 --------
  27. // UserDetails 用户完整信息,包含用户、部门、产品、成员、角色、权限等所有有效字段。
  28. // 由 UserDetailsLoader 一次性加载,可用于中间件 context 注入、login/userInfo 响应、Claims 构造等。
  29. type UserDetails struct {
  30. // 用户基本信息 (sys_user)
  31. UserId int64 `json:"userId"`
  32. Username string `json:"username"`
  33. Nickname string `json:"nickname"`
  34. Avatar string `json:"avatar"`
  35. Email string `json:"email"`
  36. Phone string `json:"phone"`
  37. Remark string `json:"remark"`
  38. IsSuperAdmin bool `json:"isSuperAdmin"`
  39. IsSuperAdminRaw int64 `json:"isSuperAdminRaw"`
  40. MustChangePassword bool `json:"mustChangePassword"`
  41. MustChangePwdRaw int64 `json:"mustChangePwdRaw"`
  42. Status int64 `json:"status"`
  43. TokenVersion int64 `json:"tokenVersion"`
  44. // 部门信息 (sys_dept)
  45. DeptId int64 `json:"deptId"`
  46. DeptName string `json:"deptName"`
  47. DeptPath string `json:"deptPath"`
  48. DeptType string `json:"deptType"`
  49. DeptStatus int64 `json:"deptStatus"`
  50. // 产品上下文 (sys_product)
  51. ProductCode string `json:"productCode"`
  52. ProductName string `json:"productName"`
  53. ProductStatus int64 `json:"productStatus"`
  54. // 成员信息 (sys_product_member)
  55. MemberType string `json:"memberType"`
  56. // 角色列表 (sys_role,当前产品下已启用的角色)
  57. Roles []RoleInfo `json:"roles"`
  58. // 权限列表 (计算后的权限 code 集合)
  59. Perms []string `json:"perms"`
  60. // 当前产品下最小 permsLevel(无角色时为 math.MaxInt64)
  61. MinPermsLevel int64 `json:"minPermsLevel"`
  62. }
  63. // RoleInfo 角色摘要信息。
  64. type RoleInfo struct {
  65. Id int64 `json:"id"`
  66. Name string `json:"name"`
  67. Remark string `json:"remark"`
  68. PermsLevel int64 `json:"permsLevel"`
  69. }
  70. // -------- UserDetailsLoader --------
  71. // UserDetailsLoader 负责加载、缓存、失效用户详细信息。
  72. // 优先从 Redis 读取完整 UserDetails,miss 时查 DB 并回填。
  73. type UserDetailsLoader struct {
  74. rds *redis.Redis
  75. keyPrefix string
  76. ttl int
  77. models *model.Models
  78. sf singleflight.Group
  79. }
  80. func NewUserDetailsLoader(rds *redis.Redis, keyPrefix string, models *model.Models) *UserDetailsLoader {
  81. return &UserDetailsLoader{
  82. rds: rds,
  83. keyPrefix: keyPrefix,
  84. ttl: defaultCacheTTL,
  85. models: models,
  86. }
  87. }
  88. func (l *UserDetailsLoader) cacheKey(userId int64, productCode string) string {
  89. return fmt.Sprintf("%s:ud:%d:%s", l.keyPrefix, userId, productCode)
  90. }
  91. func (l *UserDetailsLoader) userIndexKey(userId int64) string {
  92. return fmt.Sprintf("%s:ud:idx:u:%d", l.keyPrefix, userId)
  93. }
  94. func (l *UserDetailsLoader) productIndexKey(productCode string) string {
  95. return fmt.Sprintf("%s:ud:idx:p:%s", l.keyPrefix, productCode)
  96. }
  97. // Load 根据 userId 和 productCode 加载完整的 UserDetails。
  98. func (l *UserDetailsLoader) Load(ctx context.Context, userId int64, productCode string) *UserDetails {
  99. key := l.cacheKey(userId, productCode)
  100. if val, err := l.rds.GetCtx(ctx, key); err == nil && val != "" {
  101. // 命中负缓存:该 userId/productCode 最近查询确认为不存在;直接返回空 UserDetails,
  102. // 避免离职/伪造账号的残余 token 持续压垮 DB(见审计 M-3)。
  103. if val == negativeCacheMarker {
  104. return &UserDetails{UserId: userId, ProductCode: productCode}
  105. }
  106. var ud UserDetails
  107. if err := json.Unmarshal([]byte(val), &ud); err == nil {
  108. return &ud
  109. }
  110. }
  111. v, sfErr, _ := l.sf.Do(key, func() (interface{}, error) {
  112. ud, err := l.loadFromDB(ctx, userId, productCode)
  113. if err != nil {
  114. return nil, err
  115. }
  116. if ud.Username == "" {
  117. // 写短 TTL 的负缓存哨兵;不走 registerCacheKey:负缓存短窗口自然过期即可,
  118. // 也避免 Clean/CleanByProduct 路径误当成真实 UserDetails key 进去全量扫。
  119. if err := l.rds.SetexCtx(ctx, key, negativeCacheMarker, negativeCacheTTL); err != nil {
  120. logx.WithContext(ctx).Errorf("set user details negative cache failed: %v", err)
  121. }
  122. return nil, nil
  123. }
  124. if val, err := json.Marshal(ud); err == nil {
  125. if err := l.rds.SetexCtx(ctx, key, string(val), l.ttl); err != nil {
  126. logx.WithContext(ctx).Errorf("set user details cache failed: %v", err)
  127. }
  128. l.registerCacheKey(ctx, key, userId, productCode)
  129. }
  130. return ud, nil
  131. })
  132. if sfErr != nil {
  133. logx.WithContext(ctx).Errorf("load user details from DB failed: %v", sfErr)
  134. }
  135. ud, ok := v.(*UserDetails)
  136. if !ok || ud == nil {
  137. return &UserDetails{UserId: userId, ProductCode: productCode}
  138. }
  139. return ud
  140. }
  141. // Del 删除指定用户在指定产品下的缓存。
  142. func (l *UserDetailsLoader) Del(ctx context.Context, userId int64, productCode string) {
  143. key := l.cacheKey(userId, productCode)
  144. if _, err := l.rds.DelCtx(ctx, key); err != nil {
  145. logx.WithContext(ctx).Errorf("del user details cache [%s] failed: %v", key, err)
  146. }
  147. l.unregisterCacheKey(ctx, key, userId, productCode)
  148. }
  149. // Clean 清除指定用户所有产品下的缓存。
  150. func (l *UserDetailsLoader) Clean(ctx context.Context, userId int64) {
  151. idxKey := l.userIndexKey(userId)
  152. l.cleanByIndex(ctx, idxKey)
  153. }
  154. // CleanByProduct 清除指定产品下所有用户的缓存。
  155. func (l *UserDetailsLoader) CleanByProduct(ctx context.Context, productCode string) {
  156. idxKey := l.productIndexKey(productCode)
  157. l.cleanByIndex(ctx, idxKey)
  158. }
  159. // BatchDel 批量删除多个用户在指定产品下的缓存。
  160. func (l *UserDetailsLoader) BatchDel(ctx context.Context, userIds []int64, productCode string) {
  161. if len(userIds) == 0 {
  162. return
  163. }
  164. keys := make([]string, 0, len(userIds))
  165. for _, uid := range userIds {
  166. keys = append(keys, l.cacheKey(uid, productCode))
  167. }
  168. if _, err := l.rds.DelCtx(ctx, keys...); err != nil {
  169. logx.WithContext(ctx).Errorf("batch del user details cache failed: %v", err)
  170. }
  171. for i, uid := range userIds {
  172. l.unregisterCacheKey(ctx, keys[i], uid, productCode)
  173. }
  174. }
  175. func (l *UserDetailsLoader) cleanByIndex(ctx context.Context, indexKey string) {
  176. keys, err := l.rds.SmembersCtx(ctx, indexKey)
  177. if err != nil {
  178. logx.WithContext(ctx).Errorf("smembers [%s] failed: %v", indexKey, err)
  179. return
  180. }
  181. if len(keys) > 0 {
  182. if _, err := l.rds.DelCtx(ctx, keys...); err != nil {
  183. logx.WithContext(ctx).Errorf("del cached keys failed: %v", err)
  184. }
  185. }
  186. if _, err := l.rds.DelCtx(ctx, indexKey); err != nil {
  187. logx.WithContext(ctx).Errorf("del index key [%s] failed: %v", indexKey, err)
  188. }
  189. }
  190. func (l *UserDetailsLoader) registerCacheKey(ctx context.Context, cacheKey string, userId int64, productCode string) {
  191. uIdxKey := l.userIndexKey(userId)
  192. if _, err := l.rds.SaddCtx(ctx, uIdxKey, cacheKey); err != nil {
  193. logx.WithContext(ctx).Errorf("sadd user index failed: %v", err)
  194. }
  195. if err := l.rds.ExpireCtx(ctx, uIdxKey, l.ttl+60); err != nil {
  196. logx.WithContext(ctx).Errorf("expire user index failed: %v", err)
  197. }
  198. if productCode != "" {
  199. pIdxKey := l.productIndexKey(productCode)
  200. if _, err := l.rds.SaddCtx(ctx, pIdxKey, cacheKey); err != nil {
  201. logx.WithContext(ctx).Errorf("sadd product index failed: %v", err)
  202. }
  203. if err := l.rds.ExpireCtx(ctx, pIdxKey, l.ttl+60); err != nil {
  204. logx.WithContext(ctx).Errorf("expire product index failed: %v", err)
  205. }
  206. }
  207. }
  208. func (l *UserDetailsLoader) unregisterCacheKey(ctx context.Context, cacheKey string, userId int64, productCode string) {
  209. if _, err := l.rds.SremCtx(ctx, l.userIndexKey(userId), cacheKey); err != nil {
  210. logx.WithContext(ctx).Errorf("srem user index failed: %v", err)
  211. }
  212. if productCode != "" {
  213. if _, err := l.rds.SremCtx(ctx, l.productIndexKey(productCode), cacheKey); err != nil {
  214. logx.WithContext(ctx).Errorf("srem product index failed: %v", err)
  215. }
  216. }
  217. }
  218. // -------- 内部加载逻辑 --------
  219. func (l *UserDetailsLoader) loadFromDB(ctx context.Context, userId int64, productCode string) (*UserDetails, error) {
  220. ud := &UserDetails{
  221. UserId: userId,
  222. ProductCode: productCode,
  223. MinPermsLevel: math.MaxInt64,
  224. }
  225. if err := l.loadUser(ctx, ud); err != nil {
  226. return ud, err
  227. }
  228. if ud.Username == "" {
  229. return ud, nil
  230. }
  231. l.loadDept(ctx, ud)
  232. l.loadProduct(ctx, ud)
  233. l.loadMembership(ctx, ud)
  234. l.loadRoles(ctx, ud)
  235. l.loadPerms(ctx, ud)
  236. return ud, nil
  237. }
  238. func (l *UserDetailsLoader) loadUser(ctx context.Context, ud *UserDetails) error {
  239. u, err := l.models.SysUserModel.FindOne(ctx, ud.UserId)
  240. if err != nil {
  241. if errors.Is(err, sqlx.ErrNotFound) {
  242. return nil
  243. }
  244. logx.WithContext(ctx).Errorf("userDetailsLoader: query user %d failed: %v", ud.UserId, err)
  245. return err
  246. }
  247. ud.Username = u.Username
  248. ud.Nickname = u.Nickname
  249. ud.Avatar = u.Avatar.String
  250. ud.Email = u.Email
  251. ud.Phone = u.Phone
  252. ud.Remark = u.Remark
  253. ud.DeptId = u.DeptId
  254. ud.IsSuperAdminRaw = u.IsSuperAdmin
  255. ud.IsSuperAdmin = u.IsSuperAdmin == consts.IsSuperAdminYes
  256. ud.MustChangePwdRaw = u.MustChangePassword
  257. ud.MustChangePassword = u.MustChangePassword == consts.MustChangePasswordYes
  258. ud.Status = u.Status
  259. ud.TokenVersion = u.TokenVersion
  260. return nil
  261. }
  262. func (l *UserDetailsLoader) loadDept(ctx context.Context, ud *UserDetails) {
  263. if ud.DeptId == 0 {
  264. return
  265. }
  266. d, err := l.models.SysDeptModel.FindOne(ctx, ud.DeptId)
  267. if err != nil {
  268. logx.WithContext(ctx).Errorf("userDetailsLoader: query dept %d failed: %v", ud.DeptId, err)
  269. return
  270. }
  271. ud.DeptName = d.Name
  272. ud.DeptPath = d.Path
  273. ud.DeptType = d.DeptType
  274. ud.DeptStatus = d.Status
  275. }
  276. func (l *UserDetailsLoader) loadProduct(ctx context.Context, ud *UserDetails) {
  277. if ud.ProductCode == "" {
  278. return
  279. }
  280. p, err := l.models.SysProductModel.FindOneByCode(ctx, ud.ProductCode)
  281. if err != nil {
  282. logx.WithContext(ctx).Errorf("userDetailsLoader: query product %s failed: %v", ud.ProductCode, err)
  283. return
  284. }
  285. ud.ProductName = p.Name
  286. ud.ProductStatus = p.Status
  287. }
  288. func (l *UserDetailsLoader) loadMembership(ctx context.Context, ud *UserDetails) {
  289. if ud.IsSuperAdmin {
  290. ud.MemberType = consts.MemberTypeSuperAdmin
  291. }
  292. if ud.ProductCode == "" {
  293. return
  294. }
  295. if ud.IsSuperAdmin {
  296. return
  297. }
  298. member, err := l.models.SysProductMemberModel.FindOneByProductCodeUserId(ctx, ud.ProductCode, ud.UserId)
  299. if err != nil {
  300. if err != productmember.ErrNotFound {
  301. logx.WithContext(ctx).Errorf("userDetailsLoader: query member failed: %v", err)
  302. }
  303. return
  304. }
  305. if member.Status != consts.StatusEnabled {
  306. return
  307. }
  308. ud.MemberType = member.MemberType
  309. }
  310. func (l *UserDetailsLoader) loadRoles(ctx context.Context, ud *UserDetails) {
  311. if ud.ProductCode == "" {
  312. return
  313. }
  314. roleIds, err := l.models.SysUserRoleModel.FindRoleIdsByUserIdForProduct(ctx, ud.UserId, ud.ProductCode)
  315. if err != nil || len(roleIds) == 0 {
  316. return
  317. }
  318. roles, err := l.models.SysRoleModel.FindByIds(ctx, roleIds)
  319. if err != nil {
  320. logx.WithContext(ctx).Errorf("userDetailsLoader: query roles failed: %v", err)
  321. return
  322. }
  323. ud.Roles = make([]RoleInfo, 0)
  324. minLevel := int64(math.MaxInt64)
  325. for _, r := range roles {
  326. if r.Status == consts.StatusEnabled {
  327. ud.Roles = append(ud.Roles, RoleInfo{
  328. Id: r.Id,
  329. Name: r.Name,
  330. Remark: r.Remark,
  331. PermsLevel: r.PermsLevel,
  332. })
  333. if r.PermsLevel < minLevel {
  334. minLevel = r.PermsLevel
  335. }
  336. }
  337. }
  338. if minLevel < math.MaxInt64 {
  339. ud.MinPermsLevel = minLevel
  340. }
  341. }
  342. func (l *UserDetailsLoader) loadPerms(ctx context.Context, ud *UserDetails) {
  343. if ud.ProductCode == "" {
  344. return
  345. }
  346. if ud.ProductStatus != consts.StatusEnabled {
  347. ud.Perms = nil
  348. return
  349. }
  350. if !ud.IsSuperAdmin && ud.MemberType == "" {
  351. ud.Perms = nil
  352. return
  353. }
  354. // 超管 / ADMIN / DEVELOPER / 研发部门的有效成员 → 全量权限
  355. if ud.IsSuperAdmin ||
  356. ud.MemberType == consts.MemberTypeAdmin ||
  357. ud.MemberType == consts.MemberTypeDeveloper ||
  358. (ud.MemberType != "" && ud.DeptType == consts.DeptTypeDev && ud.DeptStatus == consts.StatusEnabled) {
  359. codes, err := l.models.SysPermModel.FindAllCodesByProductCode(ctx, ud.ProductCode)
  360. if err != nil {
  361. logx.WithContext(ctx).Errorf("userDetailsLoader: query all perms failed: %v", err)
  362. }
  363. ud.Perms = codes
  364. return
  365. }
  366. // 普通成员:角色权限 + 用户附加权限 - 用户拒绝权限
  367. rolePermIds := make([]int64, 0)
  368. if len(ud.Roles) > 0 {
  369. roleIds := make([]int64, 0, len(ud.Roles))
  370. for _, r := range ud.Roles {
  371. roleIds = append(roleIds, r.Id)
  372. }
  373. ids, err := l.models.SysRolePermModel.FindPermIdsByRoleIds(ctx, roleIds)
  374. if err == nil {
  375. rolePermIds = ids
  376. }
  377. }
  378. allowIds, err := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(ctx, ud.UserId, consts.PermEffectAllow, ud.ProductCode)
  379. if err != nil {
  380. logx.WithContext(ctx).Errorf("userDetailsLoader: load allow perms failed: %v", err)
  381. return
  382. }
  383. denyIds, err := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(ctx, ud.UserId, consts.PermEffectDeny, ud.ProductCode)
  384. if err != nil {
  385. logx.WithContext(ctx).Errorf("userDetailsLoader: load deny perms failed: %v", err)
  386. }
  387. denySet := make(map[int64]bool, len(denyIds))
  388. for _, id := range denyIds {
  389. denySet[id] = true
  390. }
  391. permIdSet := make(map[int64]bool)
  392. for _, id := range rolePermIds {
  393. if !denySet[id] {
  394. permIdSet[id] = true
  395. }
  396. }
  397. for _, id := range allowIds {
  398. if !denySet[id] {
  399. permIdSet[id] = true
  400. }
  401. }
  402. finalIds := make([]int64, 0, len(permIdSet))
  403. for id := range permIdSet {
  404. finalIds = append(finalIds, id)
  405. }
  406. if len(finalIds) > 0 {
  407. perms, err := l.models.SysPermModel.FindByIds(ctx, finalIds)
  408. if err == nil {
  409. codes := make([]string, 0, len(perms))
  410. for _, p := range perms {
  411. if p.Status == consts.StatusEnabled {
  412. codes = append(codes, p.Code)
  413. }
  414. }
  415. ud.Perms = codes
  416. }
  417. }
  418. }