Files
2026-09-19 21:44:04 +08:00

221 lines
5.6 KiB
Go

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
}