SCS
Session management for Go 1.7+
Features
- Automatic loading and saving of session data via middleware.
- Fast and very memory-efficient performance. See the benchmarks.
- Choice of PostgreSQL, MySQL, Redis, encrypted cookie and in-memory storage engines. Custom storage engines are also supported.
- Type-safe and sensible API. Designed to be safe for concurrent use.
- Supports OWASP good-practices, including absolute and idle session timeouts and easy regeneration of session tokens.
Installation
SCS is broken up into small single-purpose packages for ease of use. You should install the session
package and your choice of storage engine from the following table:
For example:
$ go get github.com/alexedwards/scs/session
$ go get github.com/alexedwards/scs/engine/memstore
Or (recommended) use use gvt to vendor the packages you need. For example:
$ gvt fetch github.com/alexedwards/scs/session
$ gvt fetch github.com/alexedwards/scs/engine/memstore
Examples
Basic use
Working with SCS is straightforward: use the session.Manage
function to initialise a new session management middleware, then wrap your handlers or router with it.
package main
import (
"io"
"net/http"
"github.com/alexedwards/scs/engine/memstore"
"github.com/alexedwards/scs/session"
)
func main() {
engine := memstore.New(0)
sessionManager := session.Manage(engine)
mux := http.NewServeMux()
mux.HandleFunc("/put", putHandler)
mux.HandleFunc("/get", getHandler)
http.ListenAndServe(":4000", sessionManager(mux))
}
func putHandler(w http.ResponseWriter, r *http.Request) {
err := session.PutString(r, "message", "Hello from a session!")
if err != nil {
http.Error(w, err.Error(), 500)
}
}
func getHandler(w http.ResponseWriter, r *http.Request) {
msg, err := session.GetString(r, "message")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
io.WriteString(w, msg)
}
Setting options
The session.Manage
function accepts a range of functional options. You can specify any mixture of options, or none at all if you're happy with the defaults.
You can control how and when a session expires:
sessionManager := session.Manage(engine,
session.IdleTimeout(30*time.Minute),
session.Lifetime(3*time.Hour),
session.Persist(true),
)
You can control how the session cookie behaves:
sessionManager := session.Manage(engine,
session.Domain("example.org"),
session.HttpOnly(false),
session.Path("/account"),
session.Secure(true),
)
And also set a custom error handler:
sessionManager := session.Manage(engine,
session.ErrorFunc(ServerError),
)
…
func ServerError(w http.ResponseWriter, r *http.Request, err error) {
log.Println(err.Error())
http.Error(w, "Sorry, the application encountered an error", 500)
}
Storing data
SCS comes with built-in functions for storing and retreiving various types of data:
-
PutBool
, GetBool
, PopBool
– for use with bool
types
-
PutBytes
, GetBytes
, PopBytes
– for use with byte slice []byte
types
-
PutFloat
, GetFloat
, PopFloat
– for use with float64
types
-
PutInt
, GetInt
, PopInt
– for use with int
types
-
PutInt64
, GetInt64
, PopInt64
– for use with int64
types
-
PutString
, GetString
, PopString
– for use with string
types
-
PutTime
, GetTime
, PopTime
– for use with time.Time
types
-
Keys
– returns a alphabetically-sorted slice of all key names.
Custom types
Custom types can be stored and retreived using the PutObject
and GetObject
helpers.
Behind the scenes SCS uses gob encoding to store custom data types. For this to work properly:
- Your custom type must first be registered with the
encoding/gob
package. - The fields of your custom types must be exported.
The GetObject
function is computationally expensive, compared with the other built-in getters. Use it sparingly if performance is a major concern.
package main
import (
"encoding/gob"
"fmt"
"net/http"
"github.com/alexedwards/scs/engine/memstore"
"github.com/alexedwards/scs/session"
)
type User struct {
Name string
Email string
}
func main() {
gob.Register(User{})
engine := memstore.New(0)
sessionManager := session.Manage(engine)
mux := http.NewServeMux()
mux.HandleFunc("/put", putHandler)
mux.HandleFunc("/get", getHandler)
http.ListenAndServe(":4000", sessionManager(mux))
}
func putHandler(w http.ResponseWriter, r *http.Request) {
user := &User{"Alice", "alice@example.com"}
err := session.PutObject(r, "user", user)
if err != nil {
http.Error(w, err.Error(), 500)
}
}
func getHandler(w http.ResponseWriter, r *http.Request) {
user := &User{}
err := session.GetObject(r, "user", user)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
fmt.Fprintf(w, "Name: %s, Email: %s", user.Name, user.Email)
}
Flash data
The PopString
function (and similar helpers for other data types) provide one-time 'read and remove' operations on session data. This is useful for implementing flash-message style functions, such as displaying a one-time notification message after processing a form.
func putHandler(w http.ResponseWriter, r *http.Request) {
err := session.PutString(r, "flashMessage", "This will be a one-time message!")
if err != nil {
http.Error(w, err.Error(), 500)
}
}
func popHandler(w http.ResponseWriter, r *http.Request) {
msg, err := session.PopString(r, "flashMessage")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
io.WriteString(w, msg)
}
Preventing session fixation
To help prevent session fixation attacks you should renew the session token after any privilege level change.
SCS provides a RegenerateToken
helper, which should be called before making any changes to the session data that affect user privileges (such as login or logout operations).
RegenerateToken
creates a new session token (while retaining the session data), deletes the old session token from the storage engine, and sends the new session token to the client.
func loginHandler(w http.ResponseWriter, r *http.Request) {
userID := 123
err := session.RegenerateToken(r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = session.PutInt(r, "userID", userID)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
Destroying data and sessions
There are four different functions for deleting session data:
- Remove - Deletes a single key and corresponding value from the session data.
- Clear - Deletes all keys and values in the session data.
- Destroy - Deletes all keys and values in the session data and removes the session from the storage engine. The client is instructed to delete the session cookie.
- Renew - Establishes a new, empty session. The old session is deleted from the storage engine. This is essentially a a concurrency-safe amalgamation of the
RegenerateToken
and Clear
functions.
Custom storage engines
session.Engine
defines the interface for custom storage engines. Any object that implements this interface can be used as a storage engine when setting up the session manager middleware.
type Engine interface {
Delete(token string) (err error)
Find(token string) (b []byte, found bool, err error)
Save(token string, b []byte, expiry time.Time) (err error)
}
Benchmarks
Performance of SCS is heavily influenced by the choice of storage engine. The following benchmarks simulate a HTTP request during which an existing session is loaded, an integer value is retreived, modified and the session is saved.
BenchmarkSCSMemstore-8 200000 8463 ns/op 3644 B/op 49 allocs/op
BenchmarkSCSCookies-8 100000 20675 ns/op 7518 B/op 83 allocs/op
BenchmarkSCSRedis-8 30000 43636 ns/op 3229 B/op 64 allocs/op
BenchmarkSCSPostgres-8 500 3787304 ns/op 5584 B/op 96 allocs/op
BenchmarkSCSMySQL-8 300 5511906 ns/op 4382 B/op 73 allocs/op
BenchmarkSCSBoltstore-8 300 4086699 ns/op 12331 B/op 117 allocs/op
These benchmarks can be run from the benchmark_test.go
file.
Comparisons
Trying to compare against other packages is difficult. Not only is real-world usage tough to simulate with simple benchmarks, things like community support and quality of tests are probably more important than raw performance in the long-term.
That said, SCS stacks up pretty well. For the benchmarked operations it used around a quarter of the memory that Gorilla Sessions did and operated between 1.5 and 3 times faster depending on the storage engine.
BenchmarkGorillaCookies-8 20000 63678 ns/op 16987 B/op 296 allocs/op
BenchmarkGorillaRedis-8 10000 109229 ns/op 17877 B/op 336 allocs/op
BenchmarkGorillaPostgres-8 300 5460733 ns/op 24498 B/op 485 allocs/op
A big part of this performance difference is due to SCS's 'on-demand' use of Gob decoding. Accordingly, for operations which do need to call GetObject
the performance difference is significantly less pronounced.
BenchmarkSCSObjectCookies-8 30000 60773 ns/op 17700 B/op 300 allocs/op
BenchmarkSCSObjectRedis-8 10000 104259 ns/op 13883 B/op 293 allocs/op
BenchmarkSCSObjectPostgres-8 500 3926530 ns/op 15124 B/op 313 allocs/op
BenchmarkGorillaObjectCookies-8 20000 67899 ns/op 19302 B/op 320 allocs/op
BenchmarkGorillaObjectRedis-8 10000 123880 ns/op 18976 B/op 360 allocs/op
BenchmarkGorillaObjectPostgres-8 300 4073790 ns/op 26589 B/op 509 allocs/op
The code for all the above benchmarks is available in this gist.
Notes
Full godoc documentation: https://godoc.org/github.com/alexedwards/scs.