feat: 完整游戏运维功能
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
const defaultAuthSessionTTL = 8 * time.Hour
|
||||
|
||||
func (svc *CoreService) issueAuthSession(user domain.User, message string) (domain.AuthSession, error) {
|
||||
token, err := randomToken()
|
||||
if err != nil {
|
||||
return domain.AuthSession{}, err
|
||||
}
|
||||
hash := tokenHash(token)
|
||||
stamp := svc.now()
|
||||
generation := 1
|
||||
existing, err := svc.store.AuthSessions().List(domain.AuthSessionFilter{UserID: user.ID})
|
||||
if err != nil {
|
||||
return domain.AuthSession{}, err
|
||||
}
|
||||
for _, session := range existing {
|
||||
if session.Generation >= generation {
|
||||
generation = session.Generation + 1
|
||||
}
|
||||
}
|
||||
record := domain.AuthSessionRecord{
|
||||
ID: "auth-session-" + hash[:24],
|
||||
UserID: user.ID,
|
||||
TokenHash: hash,
|
||||
Status: domain.AuthSessionStatusActive,
|
||||
Generation: generation,
|
||||
IssuedAt: stamp,
|
||||
ExpiresAt: stamp.Add(defaultAuthSessionTTL),
|
||||
LastSeenAt: stamp,
|
||||
}
|
||||
if err := validator.ValidateAuthSessionRecord(record); err != nil {
|
||||
return domain.AuthSession{}, err
|
||||
}
|
||||
if err := svc.store.AuthSessions().Create(record); err != nil {
|
||||
return domain.AuthSession{}, err
|
||||
}
|
||||
svc.authMu.Lock()
|
||||
svc.authSessions[token] = user.ID
|
||||
svc.authMu.Unlock()
|
||||
return domain.AuthSession{
|
||||
SessionID: token,
|
||||
User: domain.CopyUser(user),
|
||||
Status: "authenticated",
|
||||
Message: message,
|
||||
ExpiresAt: record.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) authenticatedSession(token string) (domain.AuthSessionRecord, error) {
|
||||
token = strings.TrimSpace(token)
|
||||
if token == "" {
|
||||
return domain.AuthSessionRecord{}, ErrUnauthorized
|
||||
}
|
||||
hash := tokenHash(token)
|
||||
sessions, err := svc.store.AuthSessions().List(domain.AuthSessionFilter{TokenHash: hash})
|
||||
if err != nil {
|
||||
return domain.AuthSessionRecord{}, err
|
||||
}
|
||||
if len(sessions) != 1 || subtle.ConstantTimeCompare([]byte(sessions[0].TokenHash), []byte(hash)) != 1 {
|
||||
return domain.AuthSessionRecord{}, ErrUnauthorized
|
||||
}
|
||||
session := sessions[0]
|
||||
stamp := svc.now()
|
||||
if session.Status != domain.AuthSessionStatusActive || !session.RevokedAt.IsZero() || !stamp.Before(session.ExpiresAt) {
|
||||
if session.Status == domain.AuthSessionStatusActive && !stamp.Before(session.ExpiresAt) {
|
||||
session.Status = domain.AuthSessionStatusRevoked
|
||||
session.RevokedAt = stamp
|
||||
_ = svc.store.AuthSessions().Update(session)
|
||||
}
|
||||
return domain.AuthSessionRecord{}, ErrUnauthorized
|
||||
}
|
||||
user, err := svc.store.Users().Get(session.UserID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repo.ErrNotFound) {
|
||||
return domain.AuthSessionRecord{}, ErrUnauthorized
|
||||
}
|
||||
return domain.AuthSessionRecord{}, err
|
||||
}
|
||||
if user.Status != domain.UserStatusActive {
|
||||
return domain.AuthSessionRecord{}, ErrUnauthorized
|
||||
}
|
||||
if session.LastSeenAt.IsZero() || stamp.Sub(session.LastSeenAt) >= time.Minute {
|
||||
session.LastSeenAt = stamp
|
||||
if err := svc.store.AuthSessions().Update(session); err != nil {
|
||||
return domain.AuthSessionRecord{}, err
|
||||
}
|
||||
}
|
||||
return domain.CopyAuthSessionRecord(session), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) revokeAuthSession(token string) error {
|
||||
session, err := svc.authenticatedSession(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stamp := svc.now()
|
||||
session.Status = domain.AuthSessionStatusRevoked
|
||||
session.RevokedAt = stamp
|
||||
session.LastSeenAt = stamp
|
||||
if err := validator.ValidateAuthSessionRecord(session); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.store.AuthSessions().Update(session); err != nil {
|
||||
return err
|
||||
}
|
||||
svc.authMu.Lock()
|
||||
delete(svc.authSessions, token)
|
||||
svc.authMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) RotateUserSession(token string) (domain.AuthSession, error) {
|
||||
session, err := svc.authenticatedSession(token)
|
||||
if err != nil {
|
||||
return domain.AuthSession{}, err
|
||||
}
|
||||
user, err := svc.store.Users().Get(session.UserID)
|
||||
if err != nil {
|
||||
return domain.AuthSession{}, err
|
||||
}
|
||||
if err := svc.revokeAuthSession(token); err != nil {
|
||||
return domain.AuthSession{}, err
|
||||
}
|
||||
return svc.issueAuthSession(user, "会话已安全轮换")
|
||||
}
|
||||
|
||||
func (svc *CoreService) revokeUserSessions(userID string) error {
|
||||
sessions, err := svc.store.AuthSessions().List(domain.AuthSessionFilter{UserID: userID, Status: domain.AuthSessionStatusActive})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stamp := svc.now()
|
||||
for _, session := range sessions {
|
||||
session.Status = domain.AuthSessionStatusRevoked
|
||||
session.RevokedAt = stamp
|
||||
session.LastSeenAt = stamp
|
||||
if err := validator.ValidateAuthSessionRecord(session); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.store.AuthSessions().Update(session); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tokenHash(token string) string {
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
Reference in New Issue
Block a user