Managing Sessions in Go Web Applications
Session management is crucial for maintaining user state in web applications. This guide covers how to implement and manage sessions effectively in Go.
Basic Session Management
Using gorilla/sessions
package main
import (
"github.com/gorilla/sessions"
"net/http"
)
// Create store
var store = sessions.NewCookieStore([]byte("secret-key"))
func sessionHandler(w http.ResponseWriter, r *http.Request) {
// Get session
session, err := store.Get(r, "session-name")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Set value
session.Values["user_id"] = "123"
// Save session
err = session.Save(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
Custom Session Manager
type Session struct {
ID string
UserID string
CreatedAt time.Time
ExpiresAt time.Time
Data map[string]interface{}
}
type SessionManager struct {
store map[string]*Session
mu sync.RWMutex
maxAge time.Duration
}
func NewSessionManager(maxAge time.Duration) *SessionManager {
return &SessionManager{
store: make(map[string]*Session),
maxAge: maxAge,
}
}
func (sm *SessionManager) CreateSession() *Session {
sm.mu.Lock()
defer sm.mu.Unlock()
session := &Session{
ID: generateID(),
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(sm.maxAge),
Data: make(map[string]interface{}),
}
sm.store[session.ID] = session
return session
}
Storage Backends
Redis Session Store
type RedisStore struct {
client *redis.Client
prefix string
}
func NewRedisStore(client *redis.Client) *RedisStore {
return &RedisStore{
client: client,
prefix: "session:",
}
}
func (rs *RedisStore) Save(session *Session) error {
data, err := json.Marshal(session)
if err != nil {
return err
}
key := rs.prefix + session.ID
expiration := time.Until(session.ExpiresAt)
return rs.client.Set(context.Background(),
key, data, expiration).Err()
}
func (rs *RedisStore) Get(id string) (*Session, error) {
key := rs.prefix + id
data, err := rs.client.Get(context.Background(), key).Bytes()
if err != nil {
if err == redis.Nil {
return nil, nil
}
return nil, err
}
var session Session
err = json.Unmarshal(data, &session)
return &session, err
}
Database Session Store
type DBStore struct {
db *sql.DB
}
func NewDBStore(db *sql.DB) *DBStore {
return &DBStore{db: db}
}
func (ds *DBStore) Save(session *Session) error {
query := `
INSERT INTO sessions (id, user_id, data, expires_at)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO UPDATE
SET data = $3, expires_at = $4
`
data, err := json.Marshal(session.Data)
if err != nil {
return err
}
_, err = ds.db.Exec(query,
session.ID,
session.UserID,
data,
session.ExpiresAt,
)
return err
}
Session Middleware
Basic Session Middleware
func SessionMiddleware(manager *SessionManager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get session ID from cookie
cookie, err := r.Cookie("session_id")
if err != nil {
// Create new session
session := manager.CreateSession()
cookie = &http.Cookie{
Name: "session_id",
Value: session.ID,
Expires: session.ExpiresAt,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
}
http.SetCookie(w, cookie)
}
// Get session
session, err := manager.Get(cookie.Value)
if err != nil {
http.Error(w, "Invalid session", http.StatusUnauthorized)
return
}
// Add session to context
ctx := context.WithValue(r.Context(), "session", session)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
Secure Session Middleware
type SecureSessionManager struct {
store SessionStore
encryption *Encryption
}
func (sm *SecureSessionManager) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get encrypted session ID
cookie, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "No session", http.StatusUnauthorized)
return
}
// Decrypt session ID
sessionID, err := sm.encryption.Decrypt(cookie.Value)
if err != nil {
http.Error(w, "Invalid session", http.StatusUnauthorized)
return
}
// Get and validate session
session, err := sm.store.Get(sessionID)
if err != nil || session.IsExpired() {
http.Error(w, "Invalid session", http.StatusUnauthorized)
return
}
// Add session to context
ctx := context.WithValue(r.Context(), "session", session)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Best Practices
1. Session Security
type SessionConfig struct {
Secret []byte
MaxAge time.Duration
Secure bool
HttpOnly bool
SameSite http.SameSite
Domain string
Path string
}
func NewSecureSession(config SessionConfig) *Session {
return &Session{
ID: generateSecureID(),
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(config.MaxAge),
Cookie: &http.Cookie{
Name: "session_id",
Value: "", // Set later
MaxAge: int(config.MaxAge.Seconds()),
Secure: config.Secure,
HttpOnly: config.HttpOnly,
SameSite: config.SameSite,
Domain: config.Domain,
Path: config.Path,
},
}
}
func generateSecureID() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return base64.URLEncoding.EncodeToString(b)
}
2. Session Cleanup
type SessionCleaner struct {
store SessionStore
ticker *time.Ticker
done chan bool
}
func NewSessionCleaner(store SessionStore, interval time.Duration) *SessionCleaner {
return &SessionCleaner{
store: store,
ticker: time.NewTicker(interval),
done: make(chan bool),
}
}
func (sc *SessionCleaner) Start() {
go func() {
for {
select {
case <-sc.ticker.C:
sc.cleanup()
case <-sc.done:
sc.ticker.Stop()
return
}
}
}()
}
func (sc *SessionCleaner) cleanup() {
if err := sc.store.DeleteExpired(); err != nil {
log.Printf("Session cleanup error: %v", err)
}
}
3. Session Data Validation
type SessionData struct {
UserID string `json:"user_id"`
Username string `json:"username"`
LastSeen time.Time `json:"last_seen"`
UserAgent string `json:"user_agent"`
}
func (sd *SessionData) Validate() error {
if sd.UserID == "" {
return errors.New("user_id is required")
}
if sd.Username == "" {
return errors.New("username is required")
}
if sd.LastSeen.IsZero() {
return errors.New("last_seen is required")
}
return nil
}
func (s *Session) SetData(data *SessionData) error {
if err := data.Validate(); err != nil {
return err
}
s.Data = data
return nil
}
Common Patterns
1. Session Authentication
func AuthMiddleware(sm *SessionManager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*Session)
// Check if user is authenticated
if session.Data["authenticated"] != true {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Update last seen
session.Data["last_seen"] = time.Now()
if err := sm.Save(session); err != nil {
http.Error(w, "Session error", http.StatusInternalServerError)
return
}
next.ServeHTTP(w, r)
})
}
}
2. Session Rate Limiting
type SessionLimiter struct {
store SessionStore
maxReqs int
window time.Duration
}
func (sl *SessionLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*Session)
// Get request history
reqs, ok := session.Data["requests"].([]time.Time)
if !ok {
reqs = make([]time.Time, 0)
}
// Remove old requests
now := time.Now()
windowStart := now.Add(-sl.window)
for i, t := range reqs {
if t.After(windowStart) {
reqs = reqs[i:]
break
}
}
// Check rate limit
if len(reqs) >= sl.maxReqs {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
// Add new request
reqs = append(reqs, now)
session.Data["requests"] = reqs
next.ServeHTTP(w, r)
})
}
Next Steps
- Learn about Authentication
- Explore REST APIs
- Study GraphQL