Files
jiang13-forum/services/session.go
freefire fde5f628ec feat: opaque session、安装/发帖 SSR 与最小 Admin 后台
浏览器登录改为 DB sessions(可吊销);敏感词与 OIDC PEM 入 settings;
落地安装向导、注册发帖与 /admin 仪表盘/板块/审核/设置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 05:44:16 +08:00

130 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"crypto/rand"
"encoding/base64"
"errors"
"strings"
"sync"
"time"
"git.iioio.com/freefire/jiang13-forum/models"
)
const (
// SessionTTL 浏览器会话默认有效期
SessionTTL = 7 * 24 * time.Hour
// sessionTouchMin 滑动续期写库节流
sessionTouchMin = 2 * time.Minute
)
var (
ErrSessionInvalid = errors.New("会话无效或已过期")
sessionTouchCache sync.Map // sessionID -> time.Time
)
// CreateSession 为用户创建会话,返回 cookie 值
func CreateSession(userID uint, ip, userAgent string) (string, error) {
id, err := newSessionID()
if err != nil {
return "", err
}
now := time.Now()
ip = trimLen(ip, 45)
ua := trimLen(userAgent, 256)
rec := models.Session{
ID: id,
UserID: userID,
ExpiresAt: now.Add(SessionTTL),
CreatedAt: now,
LastSeenAt: now,
IP: ip,
UserAgent: ua,
}
if err := models.DB.Create(&rec).Error; err != nil {
return "", err
}
return id, nil
}
// ResolveSession 校验会话并返回用户;无效则删行并返回错误
func ResolveSession(sessionID string) (*models.User, *models.Session, error) {
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return nil, nil, ErrSessionInvalid
}
var sess models.Session
if err := models.DB.First(&sess, "id = ?", sessionID).Error; err != nil {
return nil, nil, ErrSessionInvalid
}
now := time.Now()
if now.After(sess.ExpiresAt) {
_ = models.DB.Delete(&models.Session{}, "id = ?", sessionID).Error
return nil, nil, ErrSessionInvalid
}
var user models.User
if err := models.DB.First(&user, sess.UserID).Error; err != nil {
_ = models.DB.Delete(&models.Session{}, "id = ?", sessionID).Error
return nil, nil, ErrSessionInvalid
}
touchSession(&sess, now)
return &user, &sess, nil
}
func touchSession(sess *models.Session, now time.Time) {
if v, ok := sessionTouchCache.Load(sess.ID); ok {
if t, ok := v.(time.Time); ok && now.Sub(t) < sessionTouchMin {
return
}
}
sessionTouchCache.Store(sess.ID, now)
half := SessionTTL / 2
remaining := sess.ExpiresAt.Sub(now)
updates := map[string]interface{}{"last_seen_at": now}
if remaining < half {
updates["expires_at"] = now.Add(SessionTTL)
sess.ExpiresAt = now.Add(SessionTTL)
}
sess.LastSeenAt = now
_ = models.DB.Model(&models.Session{}).Where("id = ?", sess.ID).Updates(updates).Error
}
// DeleteSession 登出当前会话
func DeleteSession(sessionID string) {
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return
}
_ = models.DB.Delete(&models.Session{}, "id = ?", sessionID).Error
sessionTouchCache.Delete(sessionID)
}
// RevokeUserSessions 吊销用户全部会话(禁言 / 改密)
func RevokeUserSessions(userID uint) {
if userID == 0 {
return
}
_ = models.DB.Where("user_id = ?", userID).Delete(&models.Session{}).Error
}
func newSessionID() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func trimLen(s string, n int) string {
s = strings.TrimSpace(s)
if len(s) > n {
return s[:n]
}
return s
}
// SessionCookieMaxAge Cookie MaxAge
func SessionCookieMaxAge() int {
return int(SessionTTL.Seconds())
}