This commit is contained in:
2026-09-02 13:01:11 +03:00
parent 0fbba6378a
commit a27c644840
+155 -83
View File
@@ -6,99 +6,134 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"io" "io"
"maps"
"net/http" "net/http"
"net/url"
"sync" "sync"
"time" "time"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
var RDb = redis.NewClient(&redis.Options{ //var RDb = redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Addr: "localhost:6379",
Password: "123", // Password: "123",
DB: 0, // use default DB // DB: 0, // use default DB
}) //})
// var expires = time.Duration(96 * time.Hour)
var expires = int(96 * time.Hour)
var SessionManager = InitSession("sid", expires)
type SessionInterface interface { type SessionInterface interface {
Write() (error, bool) redisRead() error
redisWrite() error
UpdateTTL() error UpdateTTL() error
Read() (error, bool) Set(key string, value any) error
Set(key string, value interface{}) Get(key string) any
Get(key string) interface{}
ID() string ID() string
Delete(string) Delete(string) error
Destroy() error Destroy() error
} }
type Manager struct { type Manager struct {
redisDB *redis.Client
privateCookieName string privateCookieName string
lock sync.Mutex
maxLifeTTL time.Duration maxLifeTTL time.Duration
} secure bool
cache map[string]SessionInterface
func InitSession(cookieName string, ttl int) *Manager {
return &Manager{
privateCookieName: cookieName,
lock: sync.Mutex{},
maxLifeTTL: time.Duration(ttl),
}
} }
type Session struct { type Session struct {
sid string sid string
Storage *SessionStorage storage *sessionStorage
ttl time.Duration ttl time.Duration
manager *Manager
mutex sync.RWMutex
} }
type SessionStorage struct { type sessionStorage struct {
Values map[string]interface{} Values map[string]any
} }
func (m *Manager) createSession() (*Session, *http.Cookie) { func NewManager(client *redis.Client, cookieName string, ttl time.Duration, secure bool) *Manager {
v := make(map[string]interface{}, 0) return &Manager{
redisDB: client,
privateCookieName: cookieName,
maxLifeTTL: ttl,
secure: secure,
}
}
func (m *Manager) createSession() (SessionInterface, *http.Cookie, error) {
v := make(map[string]any)
session := &Session{ session := &Session{
sid: m.sessionID(), sid: m.sessionID(),
Storage: &SessionStorage{Values: v}, storage: &sessionStorage{Values: v},
ttl: m.maxLifeTTL, ttl: m.maxLifeTTL,
manager: m,
} }
session.Write() err := session.redisWrite()
if err != nil {
return nil, nil, err
}
return session, m.newCookie(session.sid), nil
}
cookie := &http.Cookie{ func (m *Manager) newCookie(sid string) *http.Cookie {
return &http.Cookie{
Name: m.privateCookieName, Name: m.privateCookieName,
Value: url.QueryEscape(session.sid), Value: sid,
Path: "/", Path: "/",
HttpOnly: true, HttpOnly: true,
Secure: m.secure,
MaxAge: int(m.maxLifeTTL / time.Second), MaxAge: int(m.maxLifeTTL / time.Second),
SameSite: http.SameSiteStrictMode,
} }
return session, cookie
} }
func (m *Manager) GetOrCreateSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) { func (m *Manager) GetOrCreateSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) {
m.lock.Lock()
defer m.lock.Unlock()
cookie, err := r.Cookie(m.privateCookieName) cookie, err := r.Cookie(m.privateCookieName)
if err != nil || cookie.Value == "" { if err != nil || cookie.Value == "" {
session, cookie := m.createSession() session, cookie, err := m.createSession()
http.SetCookie(w, cookie) if err != nil {
return session, nil return nil, err
} else { }
sid, _ := url.QueryUnescape(cookie.Value)
session := &Session{sid: sid}
if _, ok := session.Read(); ok {
return session, nil
} else {
session, cookie := m.createSession()
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
m.cache[session.ID()] = session
return session, nil return session, nil
} }
sid := cookie.Value
if cached, ok := m.cache[sid]; ok {
// refresh cache
if err = cached.redisRead(); err != nil { return nil, err }
if err = cached.UpdateTTL(); err != nil { return nil, err }
http.SetCookie(w, m.newCookie(cached.ID()))
return cached, nil
} }
session := &Session{sid: sid, manager: m}
err = session.redisRead()
if err == nil {
// refresh cookie on read
http.SetCookie(w, m.newCookie(session.sid))
if err := session.UpdateTTL(); err != nil {
newSession, cookie, err := m.createSession()
if err != nil {
return nil, err
}
http.SetCookie(w, cookie)
m.cache[newSession.ID()] = newSession
return newSession, nil
}
m.cache[session.ID()] = session
return session, nil
}
if err != redis.Nil {
return nil, err
}
newSession, cookie, err := m.createSession()
if err != nil {
return nil, err
}
http.SetCookie(w, cookie)
m.cache[newSession.ID()] = newSession
return newSession, nil
} }
func (m *Manager) sessionID() string { func (m *Manager) sessionID() string {
@@ -110,70 +145,107 @@ func (m *Manager) sessionID() string {
return base64.URLEncoding.EncodeToString(b) return base64.URLEncoding.EncodeToString(b)
} }
func (sm *Session) Read() (error, bool) { func (sm *Session) redisRead() error {
val, err := RDb.Get(context.Background(), sm.sid).Result() ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
val, err := sm.manager.redisDB.Get(ctx, "session:"+sm.sid).Result()
if err == redis.Nil { if err == redis.Nil {
return err, false cancel()
return err
} }
cancel()
st := &SessionStorage{} st := &sessionStorage{}
err = json.Unmarshal([]byte(val), st) err = json.Unmarshal([]byte(val), st)
if err != nil { if err != nil {
return err, false return err
} }
sm.mutex.Lock()
sm.storage = st
sm.mutex.Unlock()
sm.Storage = st ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ttl, err := RDb.TTL(context.Background(), sm.sid).Result() // session ttl mirrors redis ttl
ttl, err := sm.manager.redisDB.TTL(ctx, "session:"+sm.sid).Result()
if err != nil { if err != nil {
return err, false return err
} }
sm.mutex.Lock()
sm.ttl = ttl sm.ttl = ttl
sm.mutex.Unlock()
return nil, true return nil
} }
func (sm *Session) Write() (error, bool) { func (sm *Session) redisWrite() error {
val, err := json.Marshal(sm.Storage) shallow := make(map[string]any, len(sm.storage.Values))
if err != nil { maps.Copy(shallow, sm.storage.Values)
return err, false
}
err = RDb.Set(context.Background(), sm.sid, val, sm.ttl).Err() payload := &sessionStorage{Values: shallow}
val, err := json.Marshal(payload)
if err != nil { if err != nil {
return err, false return err
} }
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
return nil, true defer cancel()
// on successful redisWrite ttl resets to max time
err = sm.manager.redisDB.Set(ctx, "session:"+sm.sid, val, sm.manager.maxLifeTTL).Err()
return err
} }
func (sm *Session) UpdateTTL() error { func (sm *Session) UpdateTTL() error {
return RDb.Expire(context.Background(), sm.sid, time.Duration(expires)).Err() ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
} defer cancel()
ok, err := sm.manager.redisDB.Expire(ctx, "session:"+sm.sid, sm.manager.maxLifeTTL).Result()
func (sm *Session) Get(key string) interface{} { if err != nil {
if val, ok := sm.Storage.Values[key]; ok { return err
return val
} else {
return nil
} }
if !ok {
return redis.Nil
}
return nil
} }
func (sm *Session) ID() string { func (sm *Session) ID() string {
return sm.sid return sm.sid
} }
func (sm *Session) Set(key string, value interface{}) { func (sm *Session) Get(key string) any {
sm.Storage.Values[key] = value sm.mutex.RLock()
defer sm.mutex.RUnlock()
if val, ok := sm.storage.Values[key]; ok {
return val
} else {
return nil
}
} }
func (sm *Session) Delete(key string) { func (sm *Session) Set(key string, value any) error {
delete(sm.Storage.Values, key) sm.mutex.Lock()
defer sm.mutex.Unlock()
sm.storage.Values[key] = value
return sm.redisWrite()
}
func (sm *Session) Delete(key string) error {
sm.mutex.Lock()
defer sm.mutex.Unlock()
delete(sm.storage.Values, key)
return sm.redisWrite()
} }
func (sm *Session) Destroy() error { func (sm *Session) Destroy() error {
sm.mutex.Lock()
defer sm.mutex.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err := sm.manager.redisDB.Del(ctx, "session:"+sm.sid).Err()
if err == nil {
sm.sid = "" sm.sid = ""
sm.Storage = &SessionStorage{} sm.storage = &sessionStorage{Values: map[string]any{}}
return RDb.Del(context.Background(), sm.sid).Err() }
return err
} }