In section 6.1, we learned that sessions are one solution for verifying users, and that for now, the Go standard library does not have baked-in support for sessions or session handling. So, we're going to implement our own version of a session manager in Go.
The basic principle behind sessions is that a server maintains information for every single client, and clients rely on unique session ids to access this information. When users visit the web application, the server will create a new session with the following three steps, as needed:
- Create a unique session id
- Open up a data storage space: normally we save sessions in memory, but you will lose all session data if the system is accidentally interrupted. This can be a very serious issue if web application deals with sensitive data, like in electronic commerce for instance. In order to solve this problem, you can instead save your session data in a database or file system. This makes data persistence more reliable and easy to share with other applications, although the tradeoff is that more server-side IO is needed to read and write these sessions.
- Send the unique session id to the client.
The key step here is to send the unique session id to the client. In the context of a standard HTTP response, you can either use the response line, header or body to accomplish this; therefore, we have two ways to send session ids to clients: by cookies or URL rewrites.
- Cookies: the server can easily use
Set-cookie
inside of a response header to save a session id to a client, and a client can then this cookie for future requests; we often set the expiry time for cookies containing session information to 0, which means the cookie will be saved in memory and only deleted after users have close their browsers. - URL rewrite: append the session id as arguments in the URL for all pages. This way seems messy, but it's the best choice if clients have disabled cookies in their browsers.
We've talked about constructing sessions, and you should now have a general overview of it, but how can we use sessions on dynamic pages? Let's take a closer look at the life cycle of a session so we can continue implementing our Go session manager.
Here is a list of some of the key considerations in session management design.
- Global session manager.
- Keep session id unique.
- Have one session for every user.
- Session storage in memory, file or database.
- Deal with expired sessions.
Next, we'll examine a complete example of a Go session manager and the rationale behind some of its design decisions.
Define a global session manager:
type Manager struct {
cookieName string //private cookiename
lock sync.Mutex // protects session
provider Provider
maxlifetime int64
}
func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
provider, ok := provides[provideName]
if !ok {
return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
}
return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil
}
Create a global session manager in the main()
function:
var globalSessions *session.Manager
// Then, initialize the session manager
func init() {
globalSessions = NewManager("memory","gosessionid",3600)
}
We know that we can save sessions in many ways including in memory, the file system or directly into the database. We need to define a Provider
interface in order to represent the underlying structure of our session manager:
type Provider interface {
SessionInit(sid string) (Session, error)
SessionRead(sid string) (Session, error)
SessionDestroy(sid string) error
SessionGC(maxLifeTime int64)
}
SessionInit
implements the initialization of a session, and returns new a session if it succeeds.SessionRead
returns a session represented by the corresponding sid. Creates a new session and returns it if it does not already exist.SessionDestroy
given an sid, deletes the corresponding session.SessionGC
deletes expired session variables according tomaxLifeTime
.
So what methods should our session interface have? If you have any experience in web development, you should know that there are only four operations for sessions: set value, get value, delete value and get current session id. So, our session interface should have four methods to perform these operations.
type Session interface {
Set(key, value interface{}) error //set session value
Get(key interface{}) interface{} //get session value
Delete(key interface{}) error //delete session value
SessionID() string //back current sessionID
}
This design takes its roots from the database/sql/driver
, which defines the interface first, then registers specific structures when we want to use it. The following code is the internal implementation of a session register function.
var provides = make(map[string]Provider)
// Register makes a session provider available by the provided name.
// If a Register is called twice with the same name or if the driver is nil,
// it panics.
func Register(name string, provider Provider) {
if provider == nil {
panic("session: Register provide is nil")
}
if _, dup := provides[name]; dup {
panic("session: Register called twice for provide " + name)
}
provides[name] = provider
}
Session ids are for identifying users of web applications, so they must be unique. The following code shows how to achieve this goal:
func (manager *Manager) sessionId() string {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return ""
}
return base64.URLEncoding.EncodeToString(b)
}
We need to allocate or get an existing session in order to validate user operations. The SessionStart
function is for checking if any there are any sessions related to the current user, creating a new session non are found.
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
manager.lock.Lock()
defer manager.lock.Unlock()
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
sid := manager.sessionId()
session, _ = manager.provider.SessionInit(sid)
cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
http.SetCookie(w, &cookie)
} else {
sid, _ := url.QueryUnescape(cookie.Value)
session, _ = manager.provider.SessionRead(sid)
}
return
}
Here is an example that uses sessions for a login operation.
func login(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
r.ParseForm()
if r.Method == "GET" {
t, _ := template.ParseFiles("login.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("username"))
} else {
sess.Set("username", r.Form["username"])
http.Redirect(w, r, "/", 302)
}
}
The SessionStart
function returns a variable that implements a session interface. How do we use it?
You saw session.Get("uid")
in the above example for a basic operation. Now let's examine a more detailed example.
func count(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
createtime := sess.Get("createtime")
if createtime == nil {
sess.Set("createtime", time.Now().Unix())
} else if (createtime.(int64) + 360) < (time.Now().Unix()) {
globalSessions.SessionDestroy(w, r)
sess = globalSessions.SessionStart(w, r)
}
ct := sess.Get("countnum")
if ct == nil {
sess.Set("countnum", 1)
} else {
sess.Set("countnum", (ct.(int) + 1))
}
t, _ := template.ParseFiles("count.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("countnum"))
}
As you can see, operating on sessions simply involves using the key/value pattern in the Set, Get and Delete operations.
Because sessions have the concept of an expiry time, we define the GC to update the session's latest modify time. This way, the GC will not delete sessions that have expired but are still being used.
We know that web application have a logout operation. When users logout, we need to delete the corresponding session. We've already used the reset operation in above example -now let's take a look at the function body.
//Destroy sessionid
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
return
} else {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionDestroy(cookie.Value)
expiration := time.Now()
cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
http.SetCookie(w, &cookie)
}
}
Let's see how to let the session manager delete a session. We need to start the GC in the main()
function:
func init() {
go globalSessions.GC()
}
func (manager *Manager) GC() {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionGC(manager.maxlifetime)
time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() })
}
We see that the GC makes full use of the timer function in the time
package. It automatically calls GC when the session times out, ensuring that all sessions are usable during maxLifeTime
. A similar solution can be used to count online users.
So far, we implemented a session manager to manage global sessions in the web application and defined the Provider
interface as the storage implementation of Session
. In the next section, we are going to talk about how to implement Provider
for additional session storage structures, which you will be able to reference in the future.
- Directory
- Previous section: Session and cookies
- Next section: Session storage