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.
- 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.
- 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
- 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
- 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
- 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.
- 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
- 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.