The basic principle behind Session is that the server maintains the information of each client, and the client relies on a unique Session ID to access the information.
When a user accesses a Web application, the server creates a new Session using the following three steps as needed:
- Create a unique Session ID
- Open the data storage space: Usually we save Session in memory, but if the system breaks down unexpectedly, you will lose all Session data. This can be a very serious problem if Web applications deal with sensitive data, such as e-commerce. To solve this problem, you can save Session data in a database or file system. This makes data persistence more reliable and easy to share with other applications, but the trade-off is that reading and writing these sessions requires more server-side IO.
- 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 standard HTTP responses, you can do this using response lines, titles or body text; therefore, we have two ways to send Session IDs to clients: through cookie s or URL rewriting.
- Cookie: The server can easily send the Session ID to the client using Set-cookie in the response header, and the client can use the cookie for future requests; we often set the expiration time of the cookie containing Session information to 0, which means that the cookie will be saved in memory and deleted only after the user closes the browser.
- URL rewriting: Attach Session ID as a parameter to the URLs of all pages. This may seem confusing, but if a customer disables cookie s in a browser, it's the best choice.
Use Go to manage Session
Session Management Design
- Global Session Management.
- Keep Session ID unique.
- Prepare a Session for each user.
- Session is stored in memory, file or database.
- Deal with expired Session s.
Next, a complete example is given to illustrate how to implement the above design.
Global Session Management
Define the Global Session Manager:
// Manager Session Management type Manager struct { cookieName string lock sync.Mutex provider Provider maxLifeTime int64 } // GetManager Gets Session Manager func GetManager(providerName string, cookieName string, maxLifeTime int64) (*Manager, error) { provider, ok := providers[providerName] if !ok { return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", providerName) } return &Manager{ cookieName: cookieName, maxLifeTime: maxLifeTime, provider: provider, }, nil }
Create a global Session manager in the main() function:
var appSession *Manager // Initialize session manager func init() { appSession, _ = GetManager("memory", "sessionid", 3600) go appSession.SessionGC() }
We know that we can save Session in a variety of ways, including memory, file system or direct access to the database. We need to define a Provider interface to represent the underlying structure of the Session Manager:
// Provider interface 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 Session and returns the new Session if successful.
- SessionRead returns the Session represented by the corresponding sid. Create a new Session and return it if it does not yet exist.
- Session Destroy gives a sid and deletes the corresponding Session.
- SessionGC deletes expired Session variables based on maxLifeTime. So what should our Session interface do? If you have any Web development experience, you should know that Session has only four operations: setting values, getting values, deleting values, and getting the current Session ID. Therefore, our Session interface should have four ways to perform these operations.
// Session interface type Session interface { Set(key, value interface{}) error // Setting Session Get(key interface{}) interface{} // Get Session Del(key interface{}) error // Delete Session SID() string // Current Session ID }
This design originates from database/sql/driver, which first defines interfaces and then registers specific structures when we want to use them. The following code is an internal implementation of the Session register function.
var providers = make(map[string]Provider) // RegisterProvider Register Session Register func RegisterProvider(name string, provider Provider) { if provider == nil { panic("session: Register provider is nil") } if _, p := providers[name]; p { panic("session: Register provider is existed") } providers[name] = provider }
Keep Session ID unique
Session ID s are used to identify users of Web applications, so they must be unique. The following code shows how to achieve this goal:
// Generate SID generates a unique Session ID func (m *Manager) GenerateSID() string { b := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, b); err != nil { return "" } return base64.URLEncoding.EncodeToString(b) }
Create Session
We need to assign or obtain existing Sessions to verify user actions. The Session Start function is used to check the existence of any Session associated with the current user and create a new Session when no Session is found.
// Session Start starts Session functionality func (m *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) { m.lock.Lock() defer m.lock.Unlock() cookie, err := r.Cookie(m.cookieName) if err != nil || cookie.Value == "" { sid := m.GenerateSID() session, _ := m.provider.SessionInit(sid) newCookie := http.Cookie{ Name: m.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(m.maxLifeTime), } http.SetCookie(w, &newCookie) } else { sid, _ := url.QueryUnescape(cookie.Value) session, _ := m.provider.SessionRead(sid) } return }
The following is an example of using Session for login operations.
func login(w http.ResponseWriter, r *http.Request) { sess := appSession.SessionStart(w, r) r.ParseForm() if r.Method == "GET" { t, _ := template.ParseFiles("login.html") 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) } }
Session-related operations
The SessionStart function returns variables that implement the Session interface. How do we use it? You saw session.Get("uid") in the example above for basic operations. Now let's look at a more detailed example.
func count(w http.ResponseWriter, r *http.Request) { sess := appSession.SessionStart(w, r) createtime := sess.Get("createtime") if createtime == nil { sess.Set("createtime", time.Now().Unix()) } else if (createtime.(int64) + 360) < (time.Now().Unix()) { appSession.SessionDestroy(w, r) sess = appSession.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.html") w.Header().Set("Content-Type", "text/html") t.Execute(w, sess.Get("countnum")) }
As you can see, operating on Session requires only the use of key/value modes in Set, Get, and Delete operations. Because Session has the concept of expiration time, we define GC to update Session's latest modification time. In this way, the GC will not delete the Session that has expired but is still in use.
Cancellation of Session
We know that Web applications have logout operations. When the user logs out, we need to delete the corresponding Session. We have already used the reset operation in the example above - now let's look at the body of the function.
// Session Destory cancels Session func (m *Manager) SessionDestory(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(m.cookieName) if err != nil || cookie.Value == "" { return } m.lock.Lock() defer m.lock.Unlock() m.provider.SessionDestroy(cookie.Value) expiredTime := time.Now() newCookie := http.Cookie{ Name: m.cookieName, Path: "/", HttpOnly: true, Expires: expiredTime, MaxAge: -1, } http.SetCookie(w, &newCookie) }
Delete Session
Let's see how Session Manager can delete Session. We need to start GC in the main() function:
func init() { go appSession.SessionGC() } // SessionGC Session Garbage Recycling func (m *Manager) SessionGC() { m.lock.Lock() defer m.lock.Unlock() m.provider.SessionGC(m.maxLifeTime) time.AfterFunc(time.Duration(m.maxLifeTime), func() { m.SessionGC() }) }
We see that GC takes full advantage of the timer function in the timepack.
It automatically calls GC when the Session timeout occurs, ensuring that all Sessions are available during maxLifeTime.
Similar solutions can be used to calculate online users.