feat: opaque session、安装/发帖 SSR 与最小 Admin 后台
浏览器登录改为 DB sessions(可吊销);敏感词与 OIDC PEM 入 settings; 落地安装向导、注册发帖与 /admin 仪表盘/板块/审核/设置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
129
services/session.go
Normal file
129
services/session.go
Normal file
@@ -0,0 +1,129 @@
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user