We mentioned

As we all know, session is a solution for authentication between users and servers implemented by Web applications on the server side. Currently, the Go standard package does not provide any support for session. In this article, I will explain the implementation principle of session and some common defense problems based on session security.

Of course, some people might look at it and say, aren’t most of the architectures now separated from the front and back? Yes, you can use JWT to solve your problems. But there are some all-in-one Web applications that require sessions, so I’m going to build a wheel. The wheel that oneself make where problem, more familiar than others, have bug, still need not beg others to fix bug, oneself fix good, ha ha ha ha, of course this a few words have a bit skin 😜.

demand

I think a good programmer should make a requirements analysis before writing a program, organize his thoughts, and then write the code.

  • Supports memory storage of session data
  • Support for distributionredisThe session storage
  • The session is automatically renewed if there is a heartbeat30Minutes (life cycle)
  • To provide defense:A middleman.Session hijacking.Session replaySuch attacks

The working principle of

Session is implemented based on cookies. Each session corresponds to a UUID which is also a sessionID. Create an associated data structure in the server. Then the browser saves the sessionid through the cookie. The next time the browser requests the sessionid, it will have the sessionid, and then obtains the session data through the sessionid.

Code implementation

It was easy to say, but it was a lot of crap to write, but I made it happen.

Cut the crap and get right to the code.

  1. dependencies

Session is an independent structure, GlobalManager is the overall session manager responsible for data persistence, garbage collection of expired sessions ♻️, storage is the storage interface, Because we want to implement two ways to store session data or later to add other persistent storage, we must need interface abstraction support, memory and Redis are the concrete implementation of storage.

  1. storageinterface
package sessionx

// session storage interface
type storage interface {
    Read(s *Session) error
    Create(s *Session) error
    Update(s *Session) error
    Remove(s *Session) error
}
Copy the code

Storage on 9 lines of code, is the specific session data operation action of the abstract, all parameters using the pointer to the structure of session, if the processing is abnormal that is returned.

Why is the function signature parameter used pointer type, this I want to read the understanding person should know this is why 😁

  1. memoryStoreThe structure of the body
type memoryStore struct {
    sync.Map
}
Copy the code

The memoryStore contains the sync.Map structure, which was originally used as a Map, but was later found to be less efficient than using sync.Mutex 🔐 for concurrent reads and writes. Sync. Map is used for K:V storage, i.e. session data corresponding to sessionID.

The specific methods of storage are as follows:

func (m *memoryStore) Read(s *Session) error {
    if ele, ok := m.Load(s.ID); ok {
      // There is a bug that this cannot be directly s = ele
      s.Data = ele.(*Session).Data
      return nil
    }
    // s = nil
    return fmt.Errorf("id `%s` not exist session data", s.ID)
}
Copy the code

When reading data, the persistent data is first read out and then assigned to the current session.

Note: fields in structs in go maps cannot be addressed directly, official issue github.com/golang/go/i…

A few other functions:

func (m *memoryStore) Create(s *Session) error {
      m.Store(s.ID, s)
      return nil
}

func (m *memoryStore) Remove(s *Session) error {
      m.Delete(s.ID)
      return nil
}

func (m *memoryStore) Update(s *Session) error {
      if ele, ok := m.Load(s.ID); ok {
        // Why exchange data because we are not sure whether the upper layer has changed the address
        ele.(*Session).Data = s.Data
        ele.(*Session).Expires = s.Expires
        //m.sessions[s.ID] = ele
        return nil
      }
      return fmt.Errorf("id `%s` updated session fail", s.ID)
}
Copy the code

This sentence code has nothing to say, write go can read understand.

Garbage collection:

func (m *memoryStore) gc(a) {
    // recycle your trash every 10 minutes
    for {
        time.Sleep(time.Minute * 10)
        m.Range(func(key, value interface{}) bool {
            if time.Now().UnixNano() >= value.(*Session).Expires.UnixNano() {
              m.Delete(key)
            }
            return true
        })
        runtime.GC()
        // log.Println("gc running..." )}}Copy the code

Compare session expiration time, delete session expiration, above is the implementation of memory storage.

  1. redisStoreThe structure of the body
type redisStore struct {
    sync.Mutex
    sessions *redis.Client
}

func (rs *redisStore) Read(s *Session) error {
    sid := fmt.Sprintf("%s:%s", mgr.cfg.RedisKeyPrefix, s.ID)
    bytes, err := rs.sessions.Get(ctx, sid).Bytes()
    iferr ! =nil {
      return err
    }
    iferr := rs.sessions.Expire(ctx, sid, mgr.cfg.TimeOut).Err(); err ! =nil {
      return err
    }
    iferr := decoder(bytes, s); err ! =nil {
      return err
    }
    // log.Println("redis read:", s)
    return nil
}

func (rs *redisStore) Create(s *Session) error {
    return rs.setValue(s)
}

func (rs *redisStore) Update(s *Session) error {
    return rs.setValue(s)
}

func (rs *redisStore) Remove(s *Session) error {
    return rs.sessions.Del(ctx, fmt.Sprintf("%s:%s", mgr.cfg.RedisKeyPrefix, s.ID)).Err()
}

func (rs *redisStore) setValue(s *Session) error {
    bytes, err := encoder(s)
    iferr ! =nil {
        return err
    }
    err = rs.sessions.Set(ctx, fmt.Sprintf("%s:%s", mgr.cfg.RedisKeyPrefix, s.ID), bytes, mgr.cfg.TimeOut).Err()
    return err
}
Copy the code

Encoding/GOB is the encoding/ goB of the local session data provided by the redis client. The encoding/ GOB of the local session data is serialized into binary and written to the Redis server for storage and deserialized when needed.

So the question comes up, someone might ask, does Redis have no concurrent problems?

👨💻 : Then I would definitely answer, I don’t know if you know about Redis before you ask this question.

Redis concurrency competition refers to the concurrency problem caused by multiple Redis clients at the same time set key. Redis is a NoSQL database with a single-threaded mechanism, so Redis itself does not have the concept of locking.

However, when multiple clients concurrently write the same key, the value of one key is 1, which was originally changed to 2,3,4, and finally to 4. However, because of the concurrent writing of keys, the order may be changed to 4,3, and 2, and finally to 2.

If you are deploying to multiple machines, use setnx(Key, value) to implement distributed locking. The library I am currently writing does not provide distributed locking. Please Google for details.

  1. managerThe structure of the body
type storeType uint8

const (
    // memoryStore store type
    M storeType = iota
    // redis store type
    R
    SessionKey = "session-id"
)

// manager for session manager
type manager struct {
    cfg   *Configs
    store storage
}

func New(t storeType, cfg *Configs) {
    switch t {
    case M:
        // init memory storage
        m := new(memoryStore)
        go m.gc()
        mgr = &manager{cfg: cfg, store: m}
    case R:
        // parameter verify
        validate := validator.New()
        iferr := validate.Struct(cfg); err ! =nil {
            panic(err.Error())
        }

        // init redis storage
        r := new(redisStore)
        r.sessions = redis.NewClient(&redis.Options{
            Addr:     cfg.RedisAddr,
            Password: cfg.RedisPassword, // no password set
            DB:       cfg.RedisDB,       // use default DB
            PoolSize: int(cfg.PoolSize), // connection pool size
        })

        // test connection
        timeout, cancelFunc := context.WithTimeout(context.Background(), 8*time.Second)
        defer cancelFunc()
        iferr := r.sessions.Ping(timeout).Err(); err ! =nil {
            panic(err.Error())
        }
        mgr = &manager{cfg: cfg, store: r}

    default:
      panic("not implement store type")}}Copy the code

The manager structure is just two fields, one to hold our global configuration information and one to instantiate the different persistent stores. The rest of the code is auxiliary code, so I won’t go into details.

  1. SessionThe structure of the body

This structure corresponds to the browser session structure. The design principle is that one ID corresponds to one session structure.

type Session struct {
    / / the session ID
    ID string
    // Session timeout duration
    Expires time.Time
    // Map to store data
    Data map[interface{}]interface{}
    _w   http.ResponseWriter
    // Each session corresponds to a cookie
    Cookie *http.Cookie
}
Copy the code

Specific operating functions:

// Get Retrieves the stored element data from the session via the key
func (s *Session) Get(key interface{}) (interface{}, error) {
    err := mgr.store.Read(s)
    iferr ! =nil {
        return nil, err
    }
    s.refreshCookie()
    if ele, ok := s.Data[key]; ok {
        return ele, nil
    }
    return nil, fmt.Errorf("key '%s' does not exist", key)
}

// Set Stores information in the session
func (s *Session) Set(key, v interface{}) error {

    lock["W"] (func(a) {
        if s.Data == nil {
          s.Data = make(map[interface{}]interface{}, 8)
        }
        s.Data[key] = v
    })

	  s.refreshCookie()
	  return mgr.store.Update(s)
}

// Remove an element stored in the session
func (s *Session) Remove(key interface{}) error {
	  s.refreshCookie()

    lock["R"] (func(a) {
        delete(s.Data, key)
    })

	  return mgr.store.Update(s)
}

// Clean up all data for this session
func (s *Session) Clean(a) error {
    s.refreshCookie()
    return mgr.store.Remove(s)
}
// Refresh cookie session resets session life cycle whenever there is an operation
func (s *Session) refreshCookie(a) {
    s.Expires = time.Now().Add(mgr.cfg.TimeOut)
    s.Cookie.Expires = s.Expires
    // We're not using Pointers here
    // Because here we support Redis if the Web server is restarted
    // Then the session data is cleared in memory
    // The deserialization address of the data read from Redis is different from that of the reboot
    // All direct data copies
    http.SetCookie(s._w, s.Cookie)
}
Copy the code

RefreshCookie () is used to refresh the browser cookie information, because when I designed it, only the browser has heartbeat, that is, when there is operation data, the manager defaults to the browser session is still alive, and automatically updates the cookie expiration time. This update process does not just refresh the cookie, but also update the data expiration time for persistence.

Handler method

// Handler Get session data from the Request
func Handler(w http.ResponseWriter, req *http.Request) *Session {
    // Fetch session from the request
    var session Session
    session._w = w
    cookie, err := req.Cookie(mgr.cfg.Cookie.Name)
    iferr ! =nil || cookie == nil || len(cookie.Value) <= 0 {
        return createSession(w, cookie, &session)
    }
    // The ID is 73 bits long after being encoded
    if len(cookie.Value) >= 73 {
        session.ID = cookie.Value
        ifmgr.store.Read(&session) ! =nil {
            return createSession(w, cookie, &session)
        }

        // Prevent redis session data from remaining after the Web server restarts
        // But the browser cookie is not updated
        // Refresh the cookie

        // There is a pointer consistency problem, so the operation is still a block of memory, so we need to copy the copy
        _ = session.copy(mgr.cfg.Cookie)
        session.Cookie.Value = session.ID
        session.Cookie.Expires = session.Expires
        http.SetCookie(w, session.Cookie)
	    }
      // Same address!!
      // log.Printf("mgr.cfg.Cookie pointer:%p \n", mgr.cfg.Cookie)
      // log.Printf("session.cookie pointer:%p \n", session.Cookie)
      return &session
}

func createSession(w http.ResponseWriter, cookie *http.Cookie, session *Session) *Session {
      // init session parameter
      session.ID = generateUUID()
      session.Expires = time.Now().Add(mgr.cfg.TimeOut)
      _ = mgr.store.Create(session)

      // Reset the configuration cookie template
      session.copy(mgr.cfg.Cookie)
      session.Cookie.Value = session.ID
      session.Cookie.Expires = session.Expires

      http.SetCookie(w, session.Cookie)
      return session
}
Copy the code

The Handler function reads the sessionID from the HTTP request, then reads the data from the persistence layer and instantiates a session structure.

Security defense Issues

First of all I still that sentence: do not know how to attack, how to do defense. Let’s talk about how this problem came about:

Man-in-the-middle attack (MITM attack for short) is an indirect intrusion attack. This attack mode uses various technical means to place a computer controlled by the intruder virtually between two communication computers in the network connection. This computer is called the Man in the middle.

In this process, normal users visit our website through the browser, but at this time, a hack uses ARP spoofing to hijack the traffic from the router to his computer, and then the hacker uses some special software to capture the traffic information of your network request. During this process, if you sessionID is stored in cookies, It is very likely to be extracted and processed by the hacker. If you log in at this time, the hacker will get your login credentials and then replay the login using your sessionID in order to access your account related data.

func (s *Session) MigrateSession(a) error {
    // Migrating to new memory prevents session consistency from causing security problems
    // the root cause of this problem is the same as the sessionid. If the user has a sessionid before logging in, the server can give the user a new sessionid after logging in, which can prevent the session fixation attack.
    s.ID = generateUUID()
    newSession, err := deepcopy.Anything(s)
    iferr ! =nil {
        return errors.New("migrate session make a deep copy from src into dst failed")
    }
    newSession.(*Session).ID = s.ID
    newSession.(*Session).Cookie.Value = s.ID
    newSession.(*Session).Expires = time.Now().Add(mgr.cfg.TimeOut)
    newSession.(*Session)._w = s._w
    newSession.(*Session).refreshCookie()
    // The new memory is persisted
    // log.Printf("old session pointer:%p \n", s)
    // log.Printf("new session pointer:%p \n", newSession.(*Session))
    //log.Println("MigrateSession:", newSession.(*Session))
    return mgr.store.Create(newSession.(*Session))
}
Copy the code

If you’ve written Java, you’ve probably used springBoot. The Session security policy in springBoot, if you’ve looked at the source code, has a migrateSession option that says create a new session after a successful login. And then copy the information from the old session into the new session.

I followed his strategy and also implemented it in my library. When the user accesses anonymously, it is a sessionID, and when the user logs in successfully, it is another sessionID, which can effectively avoid the session fixed attack.

MigrateSession (MigrateSession) : MigrateSession (MigrateSession) : MigrateSession (MigrateSession) : MigrateSession (MigrateSession) : MigrateSession (MigrateSession) : MigrateSession (MigrateSession)

Using the demonstration

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"

    sessionx "github.com/higker/sesssionx"
)

var (
    // Configuration information
    cfg = &sessionx.Configs{
          TimeOut:        time.Minute * 30,
          RedisAddr:      "127.0.0.1:6379",
          RedisDB:        0,
          RedisPassword:  "redis.nosql",
          RedisKeyPrefix: sessionx.SessionKey,
          PoolSize:       100,
          Cookie: &http.Cookie{
            Name:     sessionx.SessionKey,
            Path:     "/",
            Expires:  time.Now().Add(time.Minute * 30), // TimeOut
            Secure:   false,
            HttpOnly: true,}})func main(a) {
    // R indicates that redis stores CFG configuration information
	  sessionx.New(sessionx.R, cfg)

    http.HandleFunc("/set".func(writer http.ResponseWriter, request *http.Request) {
        session := sessionx.Handler(writer, request)
        session.Set("K", time.Now().Format("2006 01-02 15:04:05"))
        fmt.Fprintln(writer, "set time value succeed.")
    })

    http.HandleFunc("/get".func(writer http.ResponseWriter, request *http.Request) {
        session := sessionx.Handler(writer, request)
        v, err := session.Get("K")
        iferr ! =nil {
            fmt.Fprintln(writer, err.Error())
            return
        }
        fmt.Fprintln(writer, fmt.Sprintf("The stored value is : %s", v))
    })

    http.HandleFunc("/migrate".func(writer http.ResponseWriter, request *http.Request) {
        session := sessionx.Handler(writer, request)
        err := session.MigrateSession()
        iferr ! =nil {
            log.Println(err)
        }
        fmt.Fprintln(writer, session)
    })
    _ = http.ListenAndServe(": 8080".nil)}Copy the code

The small knot

It is recommended to use JWT to do authentication, but there are also integrated Web application session will not be eliminated so early, if there is a problem, welcome to pr, there are some codes are not listed, you can go to the warehouse.

A link to the

Code repository: github.com/higker/sess…