Setting up the session manager
In this chapter I’ll run through the basics of setting up and using the alexedwards/scs
package, but if you’re going to use it in a production application I recommend reading the documentation and API reference to familiarize yourself with the full range of features.
The first thing we need to do is create a sessions
table in our MySQL database to hold the session data for our users. Start by connecting to MySQL from your terminal window as the root
user and execute the following SQL statement to setup the sessions
table:
USE snippetbox; CREATE TABLE sessions ( token CHAR(43) PRIMARY KEY, data BLOB NOT NULL, expiry TIMESTAMP(6) NOT NULL ); CREATE INDEX sessions_expiry_idx ON sessions (expiry);
In this table:
The
token
field will contain a unique, randomly-generated, identifier for each session.The
data
field will contain the actual session data that you want to share between HTTP requests. This is stored as binary data in aBLOB
(binary large object) type.The
expiry
field will contain an expiry time for the session. Thescs
package will automatically delete expired sessions from thesessions
table so that it doesn’t grow too large.
The next thing we need to do is establish a session manager in our main.go
file and make it available to our handlers via the application
struct. The session manager holds the configuration settings for our sessions, and also provides some middleware and helper methods to handle the loading and saving of session data.
Open your main.go
file and update it as follows:
package main import ( "database/sql" "flag" "html/template" "log/slog" "net/http" "os" "time" // New import "snippetbox.alexedwards.net/internal/models" "github.com/alexedwards/scs/mysqlstore" // New import "github.com/alexedwards/scs/v2" // New import "github.com/go-playground/form/v4" _ "github.com/go-sql-driver/mysql" ) // Add a new sessionManager field to the application struct. type application struct { logger *slog.Logger snippets *models.SnippetModel templateCache map[string]*template.Template formDecoder *form.Decoder sessionManager *scs.SessionManager } func main() { addr := flag.String("addr", ":4000", "HTTP network address") dsn := flag.String("dsn", "web:pass@/snippetbox?parseTime=true", "MySQL data source name") flag.Parse() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) db, err := openDB(*dsn) if err != nil { logger.Error(err.Error()) os.Exit(1) } defer db.Close() templateCache, err := newTemplateCache() if err != nil { logger.Error(err.Error()) os.Exit(1) } formDecoder := form.NewDecoder() // Use the scs.New() function to initialize a new session manager. Then we // configure it to use our MySQL database as the session store, and set a // lifetime of 12 hours (so that sessions automatically expire 12 hours // after first being created). sessionManager := scs.New() sessionManager.Store = mysqlstore.New(db) sessionManager.Lifetime = 12 * time.Hour // And add the session manager to our application dependencies. app := &application{ logger: logger, snippets: &models.SnippetModel{DB: db}, templateCache: templateCache, formDecoder: formDecoder, sessionManager: sessionManager, } logger.Info("starting server", "addr", *addr) err = http.ListenAndServe(*addr, app.routes()) logger.Error(err.Error()) os.Exit(1) } ...
For the sessions to work, we also need to wrap our application routes with the middleware provided by the SessionManager.LoadAndSave()
method. This middleware automatically loads and saves session data with every HTTP request and response.
It’s important to note that we don’t need this middleware to act on all our application routes. Specifically, we don’t need it on the GET /static/
route, because all this does is serve static files and there is no need for any stateful behavior.
So, because of that, it doesn’t make sense to add the session middleware to our existing standard
middleware chain.
Instead, let’s create a new dynamic
middleware chain containing the middleware appropriate for our dynamic application routes only.
Open the routes.go
file and update it like so:
package main import ( "net/http" "github.com/justinas/alice" ) func (app *application) routes() http.Handler { mux := http.NewServeMux() // Leave the static files route unchanged. fileServer := http.FileServer(http.Dir("./ui/static/")) mux.Handle("GET /static/", http.StripPrefix("/static", fileServer)) // Create a new middleware chain containing the middleware specific to our // dynamic application routes. For now, this chain will only contain the // LoadAndSave session middleware but we'll add more to it later. dynamic := alice.New(app.sessionManager.LoadAndSave) // Update these routes to use the new dynamic middleware chain followed by // the appropriate handler function. Note that because the alice ThenFunc() // method returns a http.Handler (rather than a http.HandlerFunc) we also // need to switch to registering the route using the mux.Handle() method. mux.Handle("GET /{$}", dynamic.ThenFunc(app.home)) mux.Handle("GET /snippet/view/{id}", dynamic.ThenFunc(app.snippetView)) mux.Handle("GET /snippet/create", dynamic.ThenFunc(app.snippetCreate)) mux.Handle("POST /snippet/create", dynamic.ThenFunc(app.snippetCreatePost)) standard := alice.New(app.recoverPanic, app.logRequest, commonHeaders) return standard.Then(mux) }
If you run the application now you should find that it compiles all OK, and your application routes continue to work as normal.
Additional information
Without using alice
If you’re not using the justinas/alice
package to help manage your middleware chains, then you’d need to use the http.HandlerFunc()
adapter to convert your handler functions like app.home
to a http.Handler
, and then wrap that with session middleware instead. Like this:
mux := http.NewServeMux() mux.Handle("GET /{$}", app.sessionManager.LoadAndSave(http.HandlerFunc(app.home))) mux.Handle("GET /snippet/view/:id", app.sessionManager.LoadAndSave(http.HandlerFunc(app.snippetView))) // ... etc