Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

The session of Beego

Web development is a very important issue is how to do a good job of the user’s browsing process control, because HTTP protocol is stateless, so the user’s every request is stateless, we do not know in the entire Web operation process which connection is related to the user, we should how to solve this problem? The classic solutions in the Web are cookie and session. Cookie is a client-side mechanism that stores user data on the client, while session is a server-side mechanism that uses a hash structure to store information. Each site visitor is assigned a unique identifier, called a sessionID, which can be stored in one of two ways: either through the URL or stored in cookies on the client side. Of course, you can also save sessions to a database, which is more secure, but less efficient.

  1. Session Management Design

We know that session management involves several factors

  • Global Session manager
  • Ensure that the sessionID is globally unique
  • Associate a session with each customer
  • Storage of sessions (can be stored in memory, files, databases, etc.)
  • Session Expiration Processing

I’ll walk you through my entire design approach to session management and the corresponding GO code examples.

  1. The Session manager

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
}
Copy the code

Go should do the same for the entire process, creating a global session manager in the main package.

// Then initialize it in the init function
var globalSessions *session.Manager
func init(a) {
    memory.InitSession()
    globalSessions ,_= session.NewManager("memory"."gosessionid".3600)}Copy the code

We know that a session is data stored on the server side, which can be stored in any way, such as memory, database, or file. Therefore, we abstract out a Provider interface to represent the underlying storage structure of the Session manager.

type Provider interface {
    SessionInit(sid string) (Session, error)
    SessionRead(sid string) (Session, error)
    SessionDestroy(sid string) error
    SessionGC(maxlifetime int64)}Copy the code
  • The SessionInit function initializes the Session and returns the new Session variable if the operation succeeds
  • The SSessionRead function returns the Session variable represented by sid. If it does not exist, a new Session variable is created and returned by calling the SessionInit function with sid as the argument
  • The SessionDestroy function destroys the Session variable corresponding to the SID
  • SessionGC deletes expired data based on maxLifeTime

So what does the Session interface need to do? Those of you who have experience in Web development know that Session processing basically consists of setting values, reading values, deleting values, and obtaining the current Session ID, so our Session interface also implements these four 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
}
Copy the code

The above design ideas come from database/ SQL /driver. Define interfaces first, and then implement the corresponding interfaces and register them in the specific session storage structure

The following is an implementation of the Register function used to Register the structure that stores sessions on demand.

var provides = make(map[string]Provider)

// Register makes a session provide available by the provided name.
// If Register is called twice with the same name or if driver is nil,
// it panics.
func Register(name string, provide Provider) {
    if provide == nil {
        panic("session: Register provide is nil")}if _, dup := provides[name]; dup {
        panic("session: Register called twice for provide " + name)
    }
    provides[name] = provide
}
Copy the code
  1. Globally unique Session ID

The Session ID is used to identify every user accessing the Web application, so it must be globally unique (GUID). The following code shows how to satisfy this requirement:

func (manager *Manager) sessionId(a) string {
    b := make([]byte.32)
    if_, err := io.ReadFull(rand.Reader, b); err ! =nil {
        return ""
    }
    return base64.URLEncoding.EncodeToString(b)
}
Copy the code
  1. The session to create

We need to assign or obtain a Session associated with each visiting user so that we can verify operations against the Session information later. The SessionStart function checks if a Session has been associated with the current user, and creates one if not.

//get Session
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)
    iferr ! =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
}
Copy the code

Let’s use the previous login operation to demonstrate session usage:

func login(w http.ResponseWriter, r *http.Request) {
    sess := globalSessions.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)}}Copy the code
  1. Action values: Set, read, and delete

The SessionStart function returns a variable that meets the Session interface. How can we use it to manipulate Session data?

The session.get (“uid”) code in the above example shows the basic operation of reading data, now let’s look at the detailed operation:

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"))}Copy the code

As you can see from the above example, the operations of a Session are similar to those of a key/value database :Set, Get, and Delete.

Because Session has the concept of expiration, so we define GC operation, when the access expiration time meets the trigger condition of GC will cause GC, but when we perform any Session operation, the Session entity will be updated, and the last access time will be changed. This way you don’t accidentally delete Session entities that are still in use during GC.

  1. Reset the session

When a user logs out of a Web application, we need to destroy the user’s session data. The above code demonstrates how to use the session reset operation. The following function implements this function:

//Destroy sessionid
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
    cookie, err := r.Cookie(manager.cookieName)
    iferr ! =nil || cookie.Value == "" {
        return
    } else {
        manager.lock.Lock()
        defermanager.lock.Unlock() manager.provider.SessionDestroy(cookie.Value) expiration := time.Now() cookie := http.Cookie{Name:  manager.cookieName, Path:"/", HttpOnly: true, Expires: expiration, http.SetCookie(w, &cookie)
    }
}
Copy the code
  1. The session is destroyed

Let’s take a look at how the Session manager manages destruction, as long as we start it when Main starts:

func init(a) {
    go globalSessions.GC()
}

func (manager *Manager) GC(a) {
    manager.lock.Lock()
    defer manager.lock.Unlock()
    manager.provider.SessionGC(manager.maxlifetime)
    time.AfterFunc(time.Duration(manager.maxlifetime), func(a) { manager.GC() })
}
Copy the code

We can see that the GC takes full advantage of the timer function in the Time package, calling the GC function after the maxLifeTime has expired, so that the session is available for the maxLifeTime period. Similar schemes can be used to count the number of online users and so on.