rework
This commit is contained in:
+158
-86
@@ -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)
|
|
||||||
return session, nil
|
|
||||||
}
|
}
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
m.cache[session.ID()] = session
|
||||||
|
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.sid = ""
|
sm.mutex.Lock()
|
||||||
sm.Storage = &SessionStorage{}
|
defer sm.mutex.Unlock()
|
||||||
return RDb.Del(context.Background(), sm.sid).Err()
|
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.storage = &sessionStorage{Values: map[string]any{}}
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user