Back

/ 7 min read

Use middleware to implement distributed session solution

Introduction

The main content of this post is to introduce a bizdemo hertz_session. The link to the demo is here.

This demo is designed to help users quickly get started with the Session middleware and CSRF middleware of the Hertz framework, and to show the distributed Session solution based on Redis.

If you don’t know what Hertz is, then you can check out my previous articles which will help you get started with this Golang HTTP framework quickly.

The main features of the hertz_session :

  • Use thrift IDL to define HTTP interface
  • Use hz to generate code
  • Use hertz-contrib/sessions to store sessions
  • Use hertz-contrib/csrf to prevent Cross-Site Request Forgery attacks
  • Use Gorm and MySQL
  • Use AdminLTE as frontend page

Hertz

Hertz is an ultra-large-scale enterprise-level microservice HTTP framework, featuring high ease of use, easy expansion, and low latency etc.

Hertz uses the self-developed high-performance network library Netpoll by default. In some special scenarios, Hertz has certain advantages in QPS and latency compared to go net.

In internal practice, some typical services, such as services with a high proportion of frameworks, gateways and other services, after migrating Hertz, compared to the Gin framework, the resource usage is significantly reduced, CPU usage is reduced by 30%-60% with the size of the traffic.

For more details, see cloudwego/hertz.

How to get this demo

Use the following command to get hertz_session:

Terminal window
git clone https://github.com/cloudwego/hertz-examples.git
cd bizdemo/hertz_session

Project structure

ps
  • biz holds the main business logic code, including handler for HTTP requests, dal for database operations, and mw for middleware
  • idl stores thrift IDL
  • pkg for some utility methods, business constants, errmsg, template rendering, etc
  • static holds the frontend static files, all taken from AdminLTE
  • Other files in the root directory include the main startup file main.go, docker config files, etc

Use of middleware

Session middleware

The distributed session solution based on redis is to store the sessions of different servers in redis or redis cluster, which aims to solve the problem that the sessions of multiple servers are not synchronized in the case of distributed system.

1. We store the session in Redis by using Hertz’s session middleware, which is initialized as follows:

biz/mw/session.go
func InitSession(h *server.Hertz) {
store, err := redis.NewStore(consts.MaxIdleNum, consts.TCP, consts.RedisAddr, consts.RedisPasswd, []byte(consts.SessionSecretKey))
if err != nil {
panic(err)
}
h.Use(sessions.New(consts.HertzSession, store))
}
  • First connect to Redis using redis.NewStore by passing the address, password, etc
  • Use the Session middleware via the h.Use method and pass in the storage connection object that we just returned. The first parameter is the name of the Cookie, which we pass in as a defined constant

2. Once initialized, we can store the user’s information (username in this case) in the session after the user is authenticated and logged in. Here’s the core code for the Login Handler Session:

// biz/handler/user/user_service.go/Login
session := sessions.Default(c)
session.Set(consts.Username, req.Username)
_ = session.Save()
  • First get a Session object via sessions.Default
  • The username is stored in the Session using the Set method
  • Finally, we call Save to save the Session object

After the user is logged in, we will store a copy of the user’s information in the session object stored in Redis, so the user can visit the corresponding page without logging in.

3. In this case, there is only one home page, index.html, which will be checked if the user is logged in and redirected to the login page login.html if they are not logged in. The core code to check if they are logged in is as follows:

pkg/render/render.go
h.GET("/index.html", func(ctx context.Context, c *app.RequestContext) {
session := sessions.Default(c)
username := session.Get(consts.Username)
if username == nil {
c.HTML(http.StatusOK, "index.html", hutils.H{
"message": utils.BuildMsg(consts.PageErr),
})
c.Redirect(http.StatusMovedPermanently, []byte("/login.html"))
return
}
c.HTML(http.StatusOK, "index.html", hutils.H{
"message": utils.BuildMsg(username.(string)),
})
})

It’s exactly the same as Login in step 2, but instead of Set, it’s Get and without Save

4. Finally, we need to clean up the user’s session when the user logs out, the core code is as follows:

// biz/handler/user/user_service.go/Logout
session := sessions.Default(c)
session.Delete(consts.Username)
_ = session.Save()

Again, note that you need to Save otherwise the delete operation will be invalid.

Session middleware encapsulates most of the complex logic that needs to be considered, such as the storage of different user sessions in Redis, and we only need to call a simple interface to complete the corresponding business process.

CSRF middleware

Next up is the use of CSRF middleware. The following explanation of what CSRF attacks are is taken from Wikipedia:

Cross-site request forgery, also known as one-click attack or session riding and abbreviated as CSRF (sometimes pronounced sea-surf[1]) or XSRF, is a type of malicious exploit of a website or web application where unauthorized commands are submitted from a user that the web application trusts.[2] There are many ways in which a malicious website can transmit such commands; specially-crafted image tags, hidden forms, and JavaScript fetch or XMLHttpRequests, for example, can all work without the user’s interaction or even knowledge. Unlike cross-site scripting (XSS), which exploits the trust a user has for a particular site, CSRF exploits the trust that a site has in a user’s browser.[3] In a CSRF attack, an innocent end user is tricked by an attacker into submitting a web request that they did not intend. This may cause actions to be performed on the website that can include inadvertent client or server data leakage, change of session state, or manipulation of an end user’s account.

After reading the relevant information, I understand that malicious websites use the trust of some websites on the user’s browser, such as the use of cookies, to launch some attacks.

Here we use CSRF middleware to protect registration, login POST form submission and POST logout. It is worth noting that the logout is initially using the GET method, but the browser sometimes caches the GET request, so there is a “bug” that fails to delete the Session. This problem was solved by changing the request method to POST and adding CSRF protection, check out this PR for details.

In this demo, since the CSRF middleware is added later, it was not considered when defining IDL at the beginning, so there is only one GET request to log out after login. Since GET is considered to be a safe method, here we mainly use the CSRF middleware to protect the registration and login two POST form submissions.

1. Let’s also take a look the initialization and usage of CSRF as follows:

func InitCSRF(h *server.Hertz) {
h.Use(csrf.New(
csrf.WithSecret(consts.CSRFSecretKey),
csrf.WithKeyLookUp(consts.CSRFKeyLookUp),
csrf.WithNext(utils.IsLogout),
csrf.WithErrorFunc(func(ctx context.Context, c *app.RequestContext) {
c.String(http.StatusBadRequest, errors.New(consts.CSRFErr).Error())
c.Abort()
}),
))
}
  • Call h.Use using CSRF middleware
  • Define the token with csrf.WithSecret
  • Retrieve the CSRF Token from the post form using the csrf.WithKeyLookup definition, default to the request header
  • Use csrf.WithNext to skip the no-login case, i.e. no Cookie exists
  • Custom exception handling with csrf.WithErrorFunc

2. After initialization, we only need to submit the generated CSRF Token through the hidden field when submitting the form after login, and the middleware will automatically help us verify whether it is valid. If there is an error, the exception handling function just defined will be used, because this Demo uses the template rendering mode. The core code is as follows (in the case of registration, the same goes for login) :

pkg/render/render.go
h.GET("/register.html", func(ctx context.Context, c *app.RequestContext) {
if !utils.IsLogout(ctx, c) {
token = csrf.GetToken(c)
}
c.HTML(http.StatusOK, "register.html", hutils.H{
"message": utils.BuildMsg("Register a new membership"),
"token": utils.BuildMsg(token),
})
})

The core HTML template is as follows:

<div>
<input type="hidden" name="csrf" value="{[{ .token | BuildMsg }]}">
</div>
  • After verifying that the user is logged in, we can get the corresponding token and put it in the form to submit, and let the middleware handle the rest.

Run the demo

Execute the following command to run the demo:

Terminal window
docker-compose up
go run .

then visit localhost:8888/register.html to visit the register page, here are the sample pages:

registerpage loginpage Image description

Summary

That’s all for this article, I would be happy if you found this helpful. Feel free to leave a comment if you have any questions.

Reference List