feat(auth): implement session-based token management with revocation support
All checks were successful
Build Backend / build (push) Successful in 2m19s
Build Backend / build-docker (push) Successful in 46s

Add Session model, SessionService, and SessionRepository to track user login
sessions and enable token revocation on auth-critical events.

- Introduce explicit TokenType (access/refresh) in JWT claims to prevent
  refresh token misuse via access token endpoints
- Add SessionID field to JWT claims, enabling stateless JWT validation
  against revoked sessions
- Replace legacy Auth middleware with RequireAuth/OptionalAuth pipeline
  that validates token type, account status, and session validity
- Implement session revocation on password change, reset, user ban/inactive,
  and explicit logout
- Add Principal cache with active invalidation for banned/role-changed users
- Fix IDOR vulnerability: GetMessagesByCursor now validates currentUserID
  is conversation participant via GetParticipantStrict
- Add group member visibility checks: announcements, group info, member list
  now require group membership
- Simplify Casbin policy: remove g grouping, use r.sub == p.sub matcher
  with globMatch; user_roles table is single source of truth for roles
- Add migration logic to clean legacy casbin g rules and migrate old p rules
  from path-style to admin/<domain> resource naming
This commit is contained in:
lafay
2026-07-05 18:28:08 +08:00
parent d240485d0e
commit eb931bf1c6
41 changed files with 2544 additions and 360 deletions

View File

@@ -7,6 +7,7 @@ import (
"with_you/internal/cache"
"with_you/internal/config"
"with_you/internal/pkg/auth"
"with_you/internal/repository"
"github.com/casbin/casbin/v3"
@@ -88,9 +89,11 @@ func (s *casbinService) Enforce(ctx context.Context, sub, obj, act string) (bool
return true, nil
}
// 检查缓存
// 缓存键与策略维度对齐sub/obj/act 直接对应 p, role, resource, action。
// user_roles 表为用户-角色真源,由 EnforceForUser 解析后再调本方法;
// 这里 sub 已经是角色名,避免再走 g(user, role) 分组model 中 matcher 已不再依赖 g
if s.cache != nil {
cacheKey := fmt.Sprintf("casbin:check:%s:%s:%s", sub, obj, act)
cacheKey := fmt.Sprintf("casbin:policy:%s:%s:%s", sub, obj, act)
if cached, ok := s.cache.Get(cacheKey); ok {
if allowed, ok := cached.(bool); ok {
return allowed, nil
@@ -106,7 +109,7 @@ func (s *casbinService) Enforce(ctx context.Context, sub, obj, act string) (bool
// 缓存结果
if s.cache != nil {
cacheKey := fmt.Sprintf("casbin:check:%s:%s:%s", sub, obj, act)
cacheKey := fmt.Sprintf("casbin:policy:%s:%s:%s", sub, obj, act)
s.cache.Set(cacheKey, allowed, s.cacheTTL)
}
@@ -140,33 +143,24 @@ func (s *casbinService) EnforceForUser(ctx context.Context, userID, obj, act str
}
func (s *casbinService) AddRoleForUser(ctx context.Context, userID, role string) error {
// 添加到数据库
// 真源策略user_roles 表为唯一真源Casbin 不再承载 g, user, role 分组策略。
// 减少双写漂移与缓存失效盲区EnforceForUser 内部先查 DB 角色再对每个角色调 Enforce(role,...)。
if err := s.roleRepo.AddRoleForUser(ctx, userID, role); err != nil {
return err
}
// 添加到 Casbin
if _, err := s.enforcer.AddRoleForUser(userID, role); err != nil {
return err
}
// 清除缓存
return s.clearUserCache(ctx, userID)
// 失效该用户的角色缓存,以及与该角色相关的策略缓存。
s.clearUserRoleCache(ctx, userID)
return s.clearPolicyCache(ctx)
}
func (s *casbinService) DeleteRoleForUser(ctx context.Context, userID, role string) error {
// 从数据库删除
if err := s.roleRepo.DeleteRoleForUser(ctx, userID, role); err != nil {
return err
}
// 从 Casbin 删除
if _, err := s.enforcer.DeleteRoleForUser(userID, role); err != nil {
return err
}
// 清除缓存
return s.clearUserCache(ctx, userID)
s.clearUserRoleCache(ctx, userID)
return s.clearPolicyCache(ctx)
}
func (s *casbinService) GetRolesForUser(ctx context.Context, userID string) ([]string, error) {
@@ -219,20 +213,39 @@ func (s *casbinService) LoadPolicy(ctx context.Context) error {
func (s *casbinService) ClearCache(ctx context.Context) error {
if s.cache != nil {
// 清除所有 casbin 相关缓存
// 清除所有 casbin 相关缓存:策略缓存 + 用户角色缓存。
s.cache.DeleteByPrefix("casbin:")
}
return nil
}
func (s *casbinService) clearUserCache(ctx context.Context, userID string) error {
// clearPolicyCache 失效所有策略缓存casbin:policy:*)。
//
// 策略变更或角色变更都应调用,确保下次 Enforce 重新查 Casbin。
func (s *casbinService) clearPolicyCache(ctx context.Context) error {
if s.cache != nil {
cacheKey := fmt.Sprintf("casbin:user:roles:%s", userID)
s.cache.Delete(cacheKey)
s.cache.DeleteByPrefix("casbin:policy:")
}
return nil
}
// clearUserRoleCache 仅失效某个用户的角色缓存casbin:user:roles:{userID})。
//
// 用于 AddRoleForUser/DeleteRoleForUser只刷新该用户的角色不波及策略缓存除非同时调 clearPolicyCache
// 同时失效认证管道的 Principal 缓存,让 RequireAuth 下次重新加载该用户的角色。
func (s *casbinService) clearUserRoleCache(ctx context.Context, userID string) error {
if s.cache != nil {
s.cache.Delete(fmt.Sprintf("casbin:user:roles:%s", userID))
s.cache.Delete(auth.PrincipalCacheKey(userID))
}
return nil
}
// clearUserCache Deprecated 兼容旧调用,内部转调 clearUserRoleCache。
func (s *casbinService) clearUserCache(ctx context.Context, userID string) error {
return s.clearUserRoleCache(ctx, userID)
}
func (s *casbinService) GetPermissionsForRole(ctx context.Context, role string) ([][]string, error) {
return s.enforcer.GetPermissionsForUser(role)
}