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
55 lines
1.9 KiB
Go
55 lines
1.9 KiB
Go
package model
|
||
|
||
import (
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// Session 用户登录会话
|
||
//
|
||
// 用于支持令牌撤销:
|
||
// - access token 携带 SessionID(JWT 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)
|
||
} |