做统一认证登录
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user