package auth import ( "crypto/rand" "encoding/hex" "time" "server/models" ) // 默认会话配置(租户未配置 yz_auth_tenant_auth_config 时使用) const ( DefaultSessionTTL = 7200 // 会话有效期(秒) DefaultMaxSession = 1 // 默认 1 号 1 机 ) // TenantSessionPolicy 租户会话策略 type TenantSessionPolicy struct { SessionTTL int MaxSession int KickStrategy int8 } // GetTenantSessionPolicy 读取租户登录策略,未配置时返回默认值 func GetTenantSessionPolicy(tid uint64) TenantSessionPolicy { policy := TenantSessionPolicy{ SessionTTL: DefaultSessionTTL, MaxSession: DefaultMaxSession, KickStrategy: models.KickStrategyKickOld, } var cfg models.AuthTenantAuthConfig if err := models.Orm.QueryTable(new(models.AuthTenantAuthConfig)). Filter("tid", tid).One(&cfg); err != nil { return policy } if cfg.SessionTTL > 0 { policy.SessionTTL = cfg.SessionTTL } if cfg.MaxSession > 0 { policy.MaxSession = cfg.MaxSession } if cfg.KickStrategy == models.KickStrategyReject { policy.KickStrategy = models.KickStrategyReject } return policy } // SessionInfo 创建会话的入参 type SessionInfo struct { IdentityID uint64 Tid uint64 ClientID string DeviceID string DeviceName string IP string UserAgent string LoginType string Amr string } // CreateSession 创建会话并执行并发控制。 // // 并发策略(租户可配): // - KickStrategyKickOld(默认):超出上限时踢掉最旧的会话(1号1机) // - KickStrategyReject:超出上限时拒绝新登录 func CreateSession(info SessionInfo) (*models.AuthSession, error) { policy := GetTenantSessionPolicy(info.Tid) // 已占用的活跃会话 var actives []models.AuthSession if _, err := models.Orm.QueryTable(new(models.AuthSession)). Filter("identity_id", info.IdentityID). Filter("revoked", 0). OrderBy("login_at"). All(&actives); err != nil { return nil, err } // 过期会话先作废,不计入占用 now := time.Now() valid := make([]models.AuthSession, 0, len(actives)) for _, s := range actives { if s.ExpiresAt.Before(now) { _ = RevokeSession(s.Sid, models.RevokeReasonExpired) continue } valid = append(valid, s) } if len(valid) >= policy.MaxSession { if policy.KickStrategy == models.KickStrategyReject { return nil, ErrSessionLimitExceeded } // 踢掉最旧的,直到腾出名额 kick := len(valid) - policy.MaxSession + 1 for i := 0; i < kick && i < len(valid); i++ { _ = RevokeSession(valid[i].Sid, models.RevokeReasonKicked) } } sid, err := randomToken(32) if err != nil { return nil, err } ttl := time.Duration(policy.SessionTTL) * time.Second s := &models.AuthSession{ Sid: sid, IdentityID: info.IdentityID, Tid: info.Tid, ClientID: info.ClientID, LoginType: orDefault(info.LoginType, "password"), LoginAt: now, LastAccessAt: now, ExpiresAt: now.Add(ttl), } if info.DeviceID != "" { s.DeviceID = &info.DeviceID } if info.DeviceName != "" { s.DeviceName = &info.DeviceName } if info.IP != "" { s.IP = &info.IP } if info.UserAgent != "" { s.UserAgent = &info.UserAgent } if info.Amr != "" { s.Amr = &info.Amr } if _, err := models.Orm.Insert(s); err != nil { return nil, err } return s, nil } // GetSession 查询有效会话(已吊销或已过期返回错误) func GetSession(sid string) (*models.AuthSession, error) { var s models.AuthSession if err := models.Orm.QueryTable(new(models.AuthSession)).Filter("sid", sid).One(&s); err != nil { return nil, err } if s.Revoked != 0 { return nil, ErrSessionRevoked } if s.ExpiresAt.Before(time.Now()) { return nil, ErrSessionExpired } return &s, nil } // TouchSession 更新会话最近访问时间(建议每 5~10 分钟一次,避免高频写库) func TouchSession(sid string) error { last := time.Now() _, err := models.Orm.QueryTable(new(models.AuthSession)). Filter("sid", sid). Update(map[string]interface{}{"last_access_at": last}) return err } // SwitchSessionTenant 切换当前会话所属企业(免密切换) func SwitchSessionTenant(sid string, tid uint64) error { _, err := models.Orm.QueryTable(new(models.AuthSession)). Filter("sid", sid). Update(map[string]interface{}{"tid": tid}) return err } // RevokeSession 吊销单个会话 func RevokeSession(sid, reason string) error { now := time.Now() _, err := models.Orm.QueryTable(new(models.AuthSession)). Filter("sid", sid). Update(map[string]interface{}{ "revoked": 1, "revoke_reason": reason, "revoke_at": now, }) return err } // RevokeAllSessions 吊销该身份的全部会话(改密、管理员下线等场景) func RevokeAllSessions(identityID uint64, reason string) error { now := time.Now() _, err := models.Orm.QueryTable(new(models.AuthSession)). Filter("identity_id", identityID). Filter("revoked", 0). Update(map[string]interface{}{ "revoked": 1, "revoke_reason": reason, "revoke_at": now, }) return err } // ListActiveSessions 在线设备列表 func ListActiveSessions(identityID uint64) ([]models.AuthSession, error) { var list []models.AuthSession _, err := models.Orm.QueryTable(new(models.AuthSession)). Filter("identity_id", identityID). Filter("revoked", 0). OrderBy("-login_at"). All(&list) return list, err } // randomToken 生成 URL 安全的随机串(用于 sid / refresh token 明文) func randomToken(n int) (string, error) { buf := make([]byte, n) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil } func orDefault(v, def string) string { if v == "" { return def } return v }