Web Development JWT Practices
baize
Posted on November 17, 2022
Foreword
The previous post briefly introduced a high-performance Go HTTP framework - Hertz. This article is based on a demo 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 is one of many external extensions to Hertz, it is forked from gin-jwt and adapted to Hertz.
Hertz's rich extension ecosystem is a great convenience for developers and worth exploring beyond this article.
Introduction
- Use
hz
to generate code - Use
JWT
to complete login and authentication - Use
Gorm
andMySQL
Download
git clone https://github.com/cloudwego/hertz-examples.git
cd bizdemo/hertz_jwt
Architecture
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.
// 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.
- get the username, password and email address
- determine if the user exists
- 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.
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 parsingusers[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 aTimeout
duration.
Return of Token
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.
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
, orparam
, the default isheader:Authorization
, the first one read from the left side takes precedence. The current demo will useheader
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
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.
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):
// 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 for parameter binding and validation, which the current demo uses for user registration and login.
// 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
orapplication/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.
path > form > query > cookie > header > json > raw_body
Further usage can be found in Documentation
Gorm
For more information on Gorm's operation of MySQL, please refer to Gorm
Run
- Running the mysql container
cd bizdemo/hertz_jwt && docker-compose up
- Creating a mysql database
After connecting to mysql, execute user.sql
- run the demo
cd bizdemo/hertz_jwt && go run main.go
API Requests
Register
# 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
# 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
# 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
Posted on November 17, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.