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
- gorilla/sessions GitHub:github.com/gorilla/sessions
- 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~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。