Web Development JWT Practices
Tags: beginners, go, opensource, webdev
Published: 2022-11-17 08:42:15

## Foreword

The [previous post](https://dev.to/baize1998/source-code-analysis-for-go-http-framework-hertz-4jgi) briefly introduced a high-performance Go HTTP framework - [Hertz](https://dev.to/baize1998/source-code-analysis-for-go-http-framework-hertz-4jgi). This article is based on a [demo](https://github.com/cloudwego/hertz-examples/tree/main/bizdemo/hertz_jwt) from the Hertz open-source repository, which describes how to use Hertz to complete the JWT authentication and authorization process.

It should be noted here that [hertz-jwt](https://github.com/hertz-contrib/jwt) is one of many external extensions to Hertz, it is forked from [gin-jwt](https://github.com/BaiZe1998/gin-jwt) and adapted to Hertz.

Hertz's rich extension ecosystem is a great convenience for developers and worth exploring beyond this article.

![image-20221114215243547](https://baize-blog-images.oss-cn-shanghai.aliyuncs.com/img/image-20221114215243547.png)

## Introduction

- Use `hz` to generate code
- Use `JWT` to complete login and authentication
- Use `Gorm` and `MySQL`

## Download

```shell
git clone https://github.com/cloudwego/hertz-examples.git
cd bizdemo/hertz_jwt
```

## Architecture

```shell
hertz_jwt
├── Makefile # Generate hertz scaffolding code using the hz command line tool
├── biz
│   ├── dal
│   │   ├── init.go 
│   │   └── mysql
│   │       ├── init.go # Initialising database connections
│   │       └── user.go # Database operations
│   ├── handler
│   │   ├── ping.go
│   │   └── register.go # Register handler
│   ├── model
│   │   ├── sql
│   │   │   └── user.sql
│   │   └── user.go # Defining the database model
│   ├── mw
│   │   └── jwt.go # Initialising the hertz-jwt middleware
│   ├── router
│   │   └── register.go
│   └── utils
│       └── md5.go # MD5
├── docker-compose.yml # MySQL container
├── go.mod
├── go.sum
├── main.go # Hertz startup functions
├── readme.md
├── router.go # Routing Registration
└── router_gen.go
```

## Analysis

The list of interfaces for this demo is as follows.

```go
// customizeRegister registers customize routers.
func customizedRegister(r *server.Hertz) {
    r.POST("/register", handler.Register)
    r.POST("/login", mw.JwtMiddleware.LoginHandler)
    auth := r.Group("/auth", mw.JwtMiddleware.MiddlewareFunc())
    auth.GET("/ping", handler.Ping)
}
```

### User Registration

The user data of the current demo is persisted through mysql by gorm, so before logging in, the user needs to be registered. The registration process is as follows.

1. get the username, password and email address
2. determine if the user exists
3. create the user

### User log-in (Authentication)

The server needs to validate the user account and password and sign a jwt token when the user logs in for the first time.

```go
JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
    Key:           []byte("secret key"),
    Timeout:       time.Hour,
    MaxRefresh:    time.Hour,
    Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
        var loginStruct struct {
            Account  string `form:"account" json:"account" query:"account" vd:"(len($) > 0 && len($) < 30); msg:'Illegal format'"`
            Password string `form:"password" json:"password" query:"password" vd:"(len($) > 0 && len($) < 30); msg:'Illegal format'"`
        }
        if err := c.BindAndValidate(&loginStruct); err != nil {
            return nil, err
        }
        users, err := mysql.CheckUser(loginStruct.Account, utils2.MD5(loginStruct.Password))
        if err != nil {
            return nil, err
        }
        if len(users) == 0 {
            return nil, errors.New("user already exists or wrong password")
        }

        return users[0], nil
    },
    PayloadFunc: func(data interface{}) jwt.MapClaims {
        if v, ok := data.(*model.User); ok {
            return jwt.MapClaims{
                jwt.IdentityKey: v,
            }
        }
        return jwt.MapClaims{}
    },
})
```

- Authenticator: A function that sets up the user information to be authenticated when logging in. The demo defines a `loginStruct` structure that receives the user login information and authenticates the validity. The return value of this function, `users[0]`, will provide the payload data source for the subsequent generation of the jwt token.
- PayloadFunc: Its input is the return value of `Authenticator`, which is responsible for parsing `users[0]` and injecting the username into the payload part of the token.

- Key: specifies the key used to encrypt the jwt token as `"secret key"`.
- Timeout: specifies that the token is valid for one hour.
- MaxRefresh: sets the maximum token refresh time, allowing the client to refresh the token within `TokenTime` + `MaxRefresh`, appending a `Timeout` duration.

### Return of Token

```go
JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
    LoginResponse: func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time) {
        c.JSON(http.StatusOK, utils.H{
            "code":    code,
            "token":   token,
            "expire":  expire.Format(time.RFC3339),
            "message": "success",
        })
    },
})
```

- LoginResponse: After a successful login, jwt token information is returned with the response, you can customize the content of this part, but be careful not to change the function signature as it is strongly bound to `LoginHandler`.

### Token Validation

When a client requests a route with a jwt middleware configured, the server verifies the jwt token it carries.

```go
JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
    TokenLookup:   "header: Authorization, query: token, cookie: jwt",
    TokenHeadName: "Bearer",
    HTTPStatusMessageFunc: func(e error, ctx context.Context, c *app.RequestContext) string {
        hlog.CtxErrorf(ctx, "jwt biz err = %+v", e.Error())
        return e.Error()
    },
    Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
        c.JSON(http.StatusOK, utils.H{
            "code":    code,
            "message": message,
        })
    },
})
```

- TokenLookup: This is used to set the source of the token, you can choose `header`, `query`, `cookie`, or `param`, the default is `header:Authorization`, the first one read from the left side takes precedence. The current demo will use `header` as the data source, so when accessing the `/ping` interface, you will need to store the token information in the HTTP Header.
- TokenHeadName: This is used to set the prefix used to retrieve the token from the header, the default is `"Bearer"`.
- HTTPStatusMessageFunc: This is used to set the error message that will be included in the response when an error occurs in the jwt validation process, you can wrap these yourself.
- Unauthorized: used to set the response function for a failed jwt validation process, the current demo returns the error code and error message.

### Extracting User Information

```go
JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
    IdentityKey: IdentityKey,
    IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
        claims := jwt.ExtractClaims(ctx, c)
        return &model.User{
            UserName: claims[IdentityKey].(string),
        }
    },
})

// Ping .
func Ping(ctx context.Context, c *app.RequestContext) {
	user, _ := c.Get(mw.IdentityKey)
	c.JSON(200, utils.H{
		"message": fmt.Sprintf("username:%v", user.(*model.User).UserName),
	})
}
```

- IdentityHandler: The function used to set the identity information to be retrieved. In the demo, here the payload of the token is extracted and the username is stored in the context information with the `IdentityKey`.
- IdentityKey: sets the key used to retrieve the identity, the default is `"identity"`.
- Ping: Constructs the response. Retrieves the username information from the context information and returns it.

## Other Components

### Code Generation

Most of the code above is scaffolded code generated through the `hz` command line tool, so developers don't need to spend a lot of time building a good code structure and just focus on writing the business.

```shell
hz new -mod github.com/cloudwego/hertz-examples/bizdemo/hertz_jwt
```

Most of the code above is scaffolded code generated through the `hz` command line tool, so developers don't need to spend a lot of time building a good code structure and just focus on writing the business.

Sample Code（Sourced from `hz` [Official Document](https://www.cloudwego.io/docs/hertz/tutorials/toolkit/toolkit/)）：

```go
// idl/hello.thrift
namespace go hello.example

struct HelloReq {
    1: string Name (api.query="name"); // api annotations to facilitate parameter binding
}

struct HelloResp {
    1: string RespBody;
}

service HelloService {
    HelloResp HelloMethod(1: HelloReq request) (api.get="/hello");
}

// execute under $GOPATH
hz new -idl idl/hello.thrift
```

### Parameter Binding

Hertz uses the open-source library [go-tagexpr](https://github.com/bytedance/go-tagexpr) for parameter binding and validation, which the current demo uses for user registration and login.

```go
// register
var registerStruct struct {
    // Binding and validation of parameters by declaring tags
    Username string `form:"username" json:"username" query:"username" vd:"(len($) > 0 && len($) < 128); msg:'Illegal format'"`
    Email    string `form:"email" json:"email" query:"email" vd:"(len($) > 0 && len($) < 128) && email($); msg:'Illegal format'"`
    Password string `form:"password" json:"password" query:"password" vd:"(len($) > 0 && len($) < 128); msg:'Illegal format'"`
}

// login
var loginStruct struct {
    // Binding and validation of parameters by declaring tags
    Account  string `form:"account" json:"account" query:"account" vd:"(len($) > 0 && len($) < 30); msg:'Illegal format'"`
    Password string `form:"password" json:"password" query:"password" vd:"(len($) > 0 && len($) < 30); msg:'Illegal format'"`
}
if err := c.BindAndValidate(&loginStruct); err != nil {
    return nil, err
}
```

- vd: used to validate the data format, e.g. string length checks (128), email type checks (email)
- form: binds the body content of the request. content-type -> `multipart/form-data` or `application/x-www-form-urlencoded`, binds the key-value of the form
- json: bind the body content of the request content-type -> `application/json`, bind the json parameters
- query: binds the query parameter of the request

The parameter bindings need to be in accordance with the priority.

```shell
path > form > query > cookie > header > json > raw_body
```

Further usage can be found in [Documentation](https://www.cloudwego.io/docs/hertz/tutorials/basic-feature/binding-and-validate/)

### Gorm

For more information on Gorm's operation of MySQL, please refer to [Gorm](https://gorm.io/)

## Run

- Running the mysql container

```shell
cd bizdemo/hertz_jwt && docker-compose up
```

- Creating a mysql database

After connecting to mysql, execute user.sql

- run the demo

```shell
cd bizdemo/hertz_jwt && go run main.go
```

## API Requests

### Register

```shell
# request
curl --location --request POST 'localhost:8888/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "Username": "admin",
    "Email": "admin@test.com",
    "Password": "admin"
}'
# response
{
    "code": 200,
    "message": "success"
}
```

### Login

```shell
# request
curl --location --request POST 'localhost:8888/login' \
--header 'Content-Type: application/json' \
--data-raw '{
    "Account": "admin",
    "Password": "admin"
}'
# response
{
    "code": 200,
    "expire": "2022-11-16T11:05:24+08:00",
    "message": "success",
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Njg1Njc5MjQsImlkIjoyLCJvcmlnX2lhdCI6MTY2ODU2NDMyNH0.qzbDJLQv4se6dOHN51p21Rp3DjV1Lf131l_5k4cK6Wk"
}
```

### Routing Access

```shell
# request
curl --location --request GET 'localhost:8888/auth/ping' \
--header 'Authorization: Bearer ${token}'
# response
{
    "message": "username:admin"
}
```

## References

- https://github.com/cloudwego/hertz-examples/tree/main/bizdemo/hertz_jwt
- https://github.com/hertz-contrib/jwt
- https://www.cloudwego.io/docs/hertz/tutorials/basic-feature/middleware/jwt/
- https://github.com/cloudwego/hertz
- https://dev.to/justlorain/high-performance-web-framework-tasting-database-operations-3m7