1

Introduction

In the previous article "Go a library of secure , we introduced cookies. At the same time, it is mentioned that cookies have two disadvantages, one is that the data should not be too large, and the other is security. Session is a server-side storage solution that can store a large amount of data and does not need to be transmitted to the client, thus solving these two problems. But the session needs an ID that can uniquely identify the user. This ID is generally stored in a cookie and sent to the client for storage, and sent to the server with each request. Cookie and session are usually used together.

gorilla/sessions is the session management library in the gorilla web development kit. It provides session based on cookie and local file system. At the same time, the expansion interface is reserved, and other backends can be used to store session data.

This article first introduces sessions , and then uses third-party extensions to introduce how to maintain login status among multiple Web server instances.

Quick to use

The code in this article uses Go Modules.

Create a directory and initialize:

$ mkdir gorilla/sessions && cd gorilla/sessions
$ go mod init github.com/darjun/go-daily-lib/gorilla/sessions

Install the gorilla/sessions library:

$ go get -u github.com/valyala/gorilla/sessions

Now we realize the function of storing some information through session on the server side:

package main

import (
  "fmt"
  "github.com/gorilla/mux"
  "github.com/gorilla/sessions"
  "log"
  "net/http"
  "os"
)

var (
  store = sessions.NewFilesystemStore("./", securecookie.GenerateRandomKey(32), securecookie.GenerateRandomKey(32))
)

func set(w http.ResponseWriter, r *http.Request) {
  session, _ := store.Get(r, "user")
  session.Values["name"] = "dj"
  session.Values["age"] = 18
  err := sessions.Save(r, w)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }
  fmt.Fprintln(w, "Hello World")
}

func read(w http.ResponseWriter, r *http.Request) {
  session, _ := store.Get(r, "user")
  fmt.Fprintf(w, "name:%s age:%d\n", session.Values["name"], session.Values["age"])
}

func main() {
  r := mux.NewRouter()
  r.HandleFunc("/set", set)
  r.HandleFunc("/read", read)
  log.Fatal(http.ListenAndServe(":8080", r))
}

The logic of the whole program is relatively clear, and the processing functions for setting and reading are hung under the paths /set and /read The focus is on the variable store . We call the session.NewFilesystemStore() method to create an *sessions.FilesystemStore type 0610348110ce22, which will store the content of our session in the file system (that is, on the local disk). We need to NewFilesytemStore() method. The first parameter specifies the local disk path for session storage. The subsequent parameters specify hashKey and blockKey (can be omitted) in turn. The former is used for authentication and the latter is used for encryption. We can use securecookie generate a sufficiently random key. For details, see the previous article introducing securecookie .

sessions Store for all session storage:

type Store interface {
  Get(r *http.Request, name string) (*Session, error)
  New(r *http.Request, name string) (*Session, error)
  Save(r *http.Request, w http.ResponseWriter, s *Session) error
}

Implementing this interface can customize the location and format of our storage session.

In the set processing function, we call store.Get(r, "user") get user , if the session does not exist, create a new one. sessions library supports creating multiple sessions for the same user. The second parameter of the store.Get() The obtained *Session is as follows:

type Session struct {
  ID string
  Values  map[interface{}]interface{}
  Options *Options
  IsNew   bool
  store   Store
  name    string
}

The data is stored directly in the Session.Values field, which is a field of type map[interface{}]interface{} , which can store almost any type of data (the reason why I want to say almost here, because I also need to consider the limitation of serialization to storage, some data types cannot be serialized Save for byte stream, such as chan ).

In the set processing function, we directly manipulate the Values field, and finally we call store.Save(r, w, session) to save the session data to the corresponding storage.

In the get processing function, similarly we first call store.Get(r, "user") get the *Session object, and then read the name and age values inside.

run:

$ go run main.go

First visit localhost:8080/set and check the cookie through the browser’s developer tools Application

We found that the name of the session will be sent to the client as the cookie name, and the session ID will be saved as the value of the cookie.

Then we visit localhost:8080/read and read the data saved in the session:

In addition, I said earlier that the FilesystemStore is stored on the local hard disk. In the local directory where the program is run, we see files beginning with session. The part after the file name session is the session ID:

cookie storage

In addition to the default local file system as storage, sessions also supports cookie as storage, that is, session data is directly transmitted between the client and the server through the cookie. The creation of cookie storage is similar to the creation of file system storage:

var store = sessions.NewCookieStore(securecookie.GenerateRandomKey(32), securecookie.GenerateRandomKey(32))

sessions.NewCookieStore() method is hashKey for verification, and the second parameter is blockKey for encryption, sessions.NewFilesystemStore() same as 0610348110d0c5.

The other part of the code does not need to be modified at all, and the result of running the program is consistent with the above. The session data is stored in a cookie and is sent from the client to the server with each request. This method is actually the cookie usage introduced in the previous article.

Log in status

Before we introduced gorilla/mux , we introduced the use of cookies to save the login status. At that time, the user name and password were directly stored in the cookie after a simple Base64 encoding, basically in a "naked" state. It is easy to steal usernames and passwords intentionally. Now we store the key user information in the session, and only a session ID is stored in the cookie.

First, we design 3 pages, the login page, the main page, and the secret page that can only be accessed by authorization. The login page only needs the user name & password input box and login button:

// login.tpl
<form action="/login" method="post">
  <label>Username:</label>
  <input name="username"><br>
  <label>Password:</label>
  <input name="password" type="password"><br>
  <button type="submit">登录</button>
</form>

The login request needs to perform different operations according to different methods. The GET method represents the page requesting the login, and the POST method represents the execution of the login operation. We use handlers.MethodHandler this middleware to handle requests for different methods of the same path:

r.Handle("/login", handlers.MethodHandler{
  "GET":  http.HandlerFunc(Login),
  "POST": http.HandlerFunc(DoLogin),
})

Login processing function is very simple, just show the page:

func Login(w http.ResponseWriter, r *http.Request) {
  ptTemplate.ExecuteTemplate(w, "login.tpl", nil)
}

Here I use the Go standard library html/template template library to load and manage the templates of each page:

var (
  ptTemplate *template.Template
)

func init() {
  template.Must(template.New("").ParseGlob("./tpls/*.tpl"))
}

DoLogin processing function needs to verify the login request, then create a User object, save it in the session, and then redirect to the main page:

func DoLogin(w http.ResponseWriter, r *http.Request) {
  r.ParseForm()
  username := r.Form.Get("username")
  password := r.Form.Get("password")
  if username != "darjun" || password != "handsome" {
    http.Redirect(w, r, "/login", http.StatusFound)
    return
  }

  SaveSessionUser(w, r, &User{Username: username})
  http.Redirect(w, r, "/", http.StatusFound)
}

The following is the processing of the main page, we can take out the saved User object from the session, and display different pages according to whether there is a User

// home.tpl
{% if . %}
<p>Hi, {% .Username %}</p><br>
<a href="/secret">Goto secret?</a>
{% else %}
<p>Hi, stranger</p><br>
<a href="/login">Goto login?</a>
{% end %}

HomeHandler code for 0610348110d27a is as follows:

func HomeHandler(w http.ResponseWriter, r *http.Request) {
  u := GetSessionUser(r)
  ptTemplate.ExecuteTemplate(w, "home.tpl", u)
}

Finally, the secret page:

// secret.tpl
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
Inventore a cumque sunt pariatur nihil doloremque tempore,
consectetur ipsum sapiente id excepturi enim velit,
quis nisi esse doloribus aliquid. Incidunt, dolore.
</p>
<p>You have visited this page {% .Count %} times.</p>

Shows how many times the page has been visited.

SecretHandler as follows:

func SecretHandler(w http.ResponseWriter, r *http.Request) {
  u := GetSessionUser(r)
  if u == nil {
    http.Redirect(w, r, "/login", http.StatusFound)
    return
  }
  u.Count++
  SaveSessionUser(w, r, u)
  ptTemplate.ExecuteTemplate(w, "secret.tpl", u)
}

If there is no session, redirect to the login page. Otherwise, the page is displayed. Here, every time the secret page is successfully accessed, the counter will be incremented and stored in the session.

One thing to note in the above code is that because the serialization of session content uses encoding/gob in the standard library, it does not support direct serialization of the structure. I encapsulated two functions to User object into JSON, and then save it in the session. And remove the string from the session to deserialize it into a User object:

func GetSessionUser(r *http.Request) *User {
  session, _ := store.Get(r, "user")
  s, ok := session.Values["user"]
  if !ok {
    return nil
  }
  u := &User{}
  json.Unmarshal([]byte(s.(string)), u)
  return u
}

func SaveSessionUser(w http.ResponseWriter, r *http.Request, u *User) {
  session, _ := store.Get(r, "user")
  data, _ := json.Marshal(u)
  session.Values["user"] = string(data)
  store.Save(r, w, session)
}

Now run our program, first visit localhost:8080 , because there is no login, it shows welcome strangers, go to login:

Click to log in, jump to the login interface, enter the user name and password:

Click login to jump to the homepage. At this time, because the login status is recorded, welcome darjun will be displayed:

Click to go to the secret link:

I kept refreshing the page and found that the number of visits has been accumulating.

localhost:8080/secret directly when you are not logged in, you will be redirected to the login interface directly.

The above program has a shortcoming, after the program restarts, you need to log in again. Because we re-randomize hashKey and blockKey every time we start, we only need to fix these two values to realize restart and save login status.

The login authentication function is very suitable for processing in middleware. The previous article has already introduced how to write middleware, so I won't go into details here.

Third-party back-end storage

Storing the session in the local file system is not conducive to horizontal expansion. Generally, for a slightly larger website, the web server will deploy many instances, and the request is forwarded to a back-end instance for processing through a reverse proxy such as Nginx. There is no guarantee that subsequent requests and previous requests will be processed in the same instance, so the session generally needs to be stored in a public place, such as redis.

sessions provides an extension interface to facilitate the use of other back-end storage session content. There are already many third-party backend extensions on GitHub. For a detailed list, see the GitHub homepage of the sessions

We only introduce the back-end storage based on redis, other extensions can be studied by yourself if you are interested. First install the extension:

$ go get gopkg.in/boj/redistore.v1

Create an instance of redistore:

store, _ = redistore.NewRediStore(10, "tcp", ":6379", "", []byte("redis-key"))

The parameters are:

  • size : the maximum number of idle connections;
  • network : connection type, generally TCP;
  • addr : network address + port;
  • password : redis password, if not enabled, fill in the blanks;
  • keyPairs : hashKey and blockKey (can be omitted) in sequence, so I won't repeat them.

In order to verify, we open multiple servers, so the port is passed in via command line parameters, using the standard library flag :

port = flag.Int("port", 8080, "port to listen")

func init() {
  flag.Parse()
}

func main() {
  // ...
  log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}

In order to run the server, we need to start a redis-server first. I won’t say much about the installation of redis. Under windows, it is recommended to use chocolatey to install. Chocolatey is similar to Ubutnu's apt-get and Mac's brew. It is very convenient and highly recommended.

In order to demonstrate the effect of reverse proxy, that is, one address can randomly access multiple web servers deployed, we open 3 web servers. Terminal 1:

$ go build 
$ ./redis -port 8080

Terminal 2:

$ ./redis -port 8081

Terminal 3:

$ ./redis -port 8082

You can use nginx as a reverse proxy, install nginx, configure:

upstream mysvr {
  server localhost:8080;
  server localhost:8081;
  server localhost:8082;
}

server {
  listen       80;
  server_name  localhost;

  location / {
    proxy_pass http://mysvr;
  }
}

This means that localhost randomly forwarded to the 3 servers in the group mysvr

$ nginx -c nginx.conf

Everything is ready, now use the browser to visit localhost , and find through the console log that server3 processed the request:

Click to log in, server1 processed the request to display the page:

Click login, server3 processed the POST type login request:

After the login is successful, the request redirected to the main interface is processed by server1 again:

Click on the private link, and the request to display the page is processed by server2:

Although the server processed each time is different, the login status is always kept. Because we use redis to save the session.

Note that I use a random server to process each time, and the result of your operation may not be the same.

Summarize

session in order to solve the problem of storing a large amount of user data and security. sessions library provides a simple and flexible method for processing sessions in Go Web development. It depends on less, can be plug-and-played, and is very convenient.

If you find a fun and easy-to-use Go language library, welcome to submit an issue on the Go Daily Library GitHub😄

refer to

  1. gorilla/sessions GitHub:github.com/gorilla/sessions
  2. Go daily one library GitHub: https://github.com/darjun/go-daily-lib

I

My blog: https://darjun.github.io

Welcome to follow my WeChat public account [GoUpUp], learn together and make progress together~


darjun
2.9k 声望358 粉丝