An Interest In:
Web News this Week
- March 20, 2024
- March 19, 2024
- March 18, 2024
- March 17, 2024
- March 16, 2024
- March 15, 2024
- March 14, 2024
Web Development JWT Practices
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. tree/main/bizdemo/hertz_jwt), 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.gitcd 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 CodeSourced from hz
Official Document
// idl/hello.thriftnamespace go hello.examplestruct 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 $GOPATHhz 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.
// registervar 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'"`}// loginvar 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
# requestcurl --location --request POST 'localhost:8888/register' \--header 'Content-Type: application/json' \--data-raw '{ "Username": "admin", "Email": "[email protected]", "Password": "admin"}'# response{ "code": 200, "message": "success"}
Login
# requestcurl --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
# requestcurl --location --request GET 'localhost:8888/auth/ping' \--header 'Authorization: Bearer ${token}'# response{ "message": "username:admin"}
References
- https://github.com/hertz-contrib/jwt
- https://www.cloudwego.io/docs/hertz/tutorials/basic-feature/middleware/jwt/
- https://github.com/cloudwego/hertz-examples/tree/main/bizdemo/hertz_jwt
- https://github.com/cloudwego/hertz
- https://dev.to/justlorain/high-performance-web-framework-tasting-database-operations-3m7
Original Link: https://dev.to/baize1998/web-development-jwt-practices-59cn
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To