221 lines
5.6 KiB
Go
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
|
|
}
|