#6.2 How to use session in Go You learned that session is a solution of user verification between client and server in section 6.1, and for now there is no support for session from Go standard library, so we're going to implement our version of session manager in Go.
##Create session The basic principle of session is that server maintains a kind of information for every single client, and client rely on an unique session id to access the information. When users visit the web application, server will crated a new session as following three steps as needed:
- Create unique session id
- Open up data storage space: normally we save session in memory, but you will lose all session data once the system interrupt accidentally, it causes serious problems if your web application is for electronic commerce. In order to solve this problem, you can save your session data in database or file system, it makes data be persistence and easy to share with other applications, though it needs more IO pay expenses.
- Send unique session id to clients.
The key step of above steps is to send unique session id to clients. In consideration of HTTP, you can either use respond line, header or body; therefore, we have cookie and URL rewrite two ways to send session id to clients.
- Cookie: Server can easily use Set-cookie in header to save session id to clients, and clients will carry this cookies in future requests; we often set 0 as expired time of cookie that contains session information, which means the cookie will be saved in memory and deleted after users close browsers.
- URL rewrite: append session id as arguments in URL in all pages, this way seems like messy, but it's the best choice if client disabled the cookie feature.
##Use Go to manage session After we talked about the constructive process of session, you should have a overview of it, but how can we use it in dynamic pages? Let's combine life cycle of session to implement a Go session manager.
###Session management design Here is the list of factors of session management:
- Global session manager.
- Keep session id unique.
- Have one session for every single user.
- Session storage, in memory, file or database.
- Deal with expired session.
I'll show you a complete example of Go session manager and mentality of designing.
###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
}
Create a global session manager in main() function:
var globalSessions *session.Manager
//然后在init函数中初始化
func init() {
globalSessions = NewManager("memory","gosessionid",3600)
}
We know that we can save session in many ways, including memory, file system or database, so we need to define a interface Provider
in order to represent 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 initialization of session, it returns new session variable if it succeed.SessionRead
returns session variable that is represented by corresponding sid, it creates a new session variable and return if it does not exist.SessionDestory
deletes session variable by corresponding sid.SessionGC
deletes expired session variables according tomaxLifeTime
.
So what methods should our session interface have? If you have web development experience, you should know that there are only four operations for session, which are set value, get value, delete value and get current session id, so our session interface should have four method for 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
}
The mentality of designing is from database/sql/driver
that define the interface first and register specific structure when we want to use. The following code is the internal implementation of session register function.
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, 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
}
###Unique session id Session id is for identifying users of web applications, so it has to 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)
}
###Create session
We need to allocate or get corresponding session in order to verify user operations. Function SessionStart
is for checking if any session related to current user, create a new one if no related 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)
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 session for log in 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)
}
}
###Operation value: set, get and delete
Function SessionStart
returns a variable that implemented session interface, so how can we use it?
You saw session.Get("uid")
in above example for basic operation, now let's see a 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, operate session is very like key/value pattern database in operation Set, Get and Delete, etc.
Because session has concept of expired, so we defined GC to update session latest modify time, then GC will not delete session that is expired but still using.
###Reset session We know that web application has log out operation, and we need to delete corresponding session, we've already used reset operation in above example, let's see the code of 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)
}
}
###Delete session Let's see how to let session manager delete session, we need to start GC in 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 GC makes full use of the timer function in package time
, it automatically calls GC when timeout, ensure that all session are usable during maxLifeTime
, similar solution can be used to count online users.
##Summary
So far we implemented a session manager to manage global session in the web application, defined the Provider
interface for storage implementation of Session
. In next section, we are going to talk about how to implement Provider
for more session storage structures, and you can reference in the future development.
##Links
- Directory
- Previous section: Session and cookies
- Next section: Session storage