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

55
internal/model/session.go Normal file
View File

@@ -0,0 +1,55 @@
package model
import (
"time"
"gorm.io/gorm"
)
// Session 用户登录会话
//
// 用于支持令牌撤销:
// - access token 携带 SessionIDJWT sid中间件校验会话是否仍有效。
// - refresh token 同样携带 SessionID刷新时轮换并校验。
// - 登出/封禁时撤销会话,旧 token 在 access TTL 内可见但 refresh 立即失效。
//
// RefreshTokenHash 存储 refresh token 的 SHA256 哈希,便于按 refresh token 反查会话,
// 不直接存储 token 明文以降低泄露风险。
type Session struct {
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
UserID string `json:"user_id" gorm:"type:varchar(36);index;not null"`
RefreshTokenHash string `json:"-" gorm:"type:varchar(64);uniqueIndex;not null"`
UserAgent string `json:"user_agent" gorm:"type:varchar(255)"`
IP string `json:"ip" gorm:"type:varchar(45)"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at" gorm:"index"`
RevokedAt *time.Time `json:"revoked_at,omitempty" gorm:"index"`
LastUsedAt time.Time `json:"last_used_at"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
func (Session) TableName() string {
return "sessions"
}
// BeforeCreate 创建前生成 UUID。
func (s *Session) BeforeCreate(tx *gorm.DB) error {
SetUUIDIfEmpty(&s.ID)
return nil
}
// IsRevoked 会话是否已撤销。
func (s *Session) IsRevoked() bool {
return s != nil && s.RevokedAt != nil
}
// IsExpired 会话是否已过期(按 ExpiresAt
func (s *Session) IsExpired(now time.Time) bool {
return s != nil && !s.ExpiresAt.IsZero() && now.After(s.ExpiresAt)
}
// IsValid 会话是否仍有效(未撤销且未过期)。
func (s *Session) IsValid(now time.Time) bool {
return s != nil && !s.IsRevoked() && !s.IsExpired(now)
}