上一篇文章简单介绍了一个高性能的 Go HTTP 框架——Hertz,本篇文章将围绕 Hertz 开源仓库的一个 demo,讲述如何使用 Hertz 完成 JWT 的认证与授权流程。
这里要说明的是,hertz-jwt 是 Hertz 众多外部扩展组件之一,Hertz 丰富的扩展生态为开发者带来了很大的便利,值得你在本文之外自行探索。
hz
生成代码JWT
扩展完成登陆认证和授权访问Gorm
访问 MySQL
数据库git clone https://github.com/cloudwego/hertz-examples.git
cd bizdemo/hertz_jwt
hertz_jwt
├── Makefile # 使用 hz 命令行工具生成 hertz 脚手架代码
├── biz
│ ├── dal
│ │ ├── init.go
│ │ └── mysql
│ │ ├── init.go # 初始化数据库连接
│ │ └── user.go # 数据库操作
│ ├── handler
│ │ ├── ping.go
│ │ └── register.go # 用户注册 handler
│ ├── model
│ │ ├── sql
│ │ │ └── user.sql
│ │ └── user.go # 定义数据库模型
│ ├── mw
│ │ └── jwt.go # 初始化 hertz-jwt 中间件
│ ├── router
│ │ └── register.go
│ └── utils
│ └── md5.go # md5 加密
├── docker-compose.yml # mysql 容器环境支持
├── go.mod
├── go.sum
├── main.go # hertz 服务入口
├── readme.md
├── router.go # 路由注册
└── router_gen.go
下方是这个 demo 的接口列表。
// 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)
}
对应 /register
接口,当前 demo 的用户数据通过 gorm 操作 mysql 完成持久化,因此在登陆之前,需要对用户进行注册,注册流程为:
服务器需要在用户第一次登陆的时候,验证用户账号和密码,并签发 jwt token。
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{}
},
})
loginStruct
结构接收用户登陆信息,并进行认证有效性。这个函数的返回值 users[0]
将为后续生成 jwt token 提供 payload 数据源。Authenticator
的返回值,此时负责解析 users[0]
,并将用户名注入 token 的 payload 部分。"secret key"
。TokenTime
+ MaxRefresh
内刷新 token 的有效时间,追加一个 Timeout
的时长。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",
})
},
})
LoginHandler
是强绑定的。访问配置了 jwt 中间件的路由时,会经过 jwt token 的校验流程。
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,
})
},
})
header
、query
、cookie
、param
,默认为 header:Authorization
,同时存在是以左侧一个读取到的优先。当前 demo 将以 header
为数据源,因此在访问 /ping
接口时,需要你将 token 信息存放在 HTTP Header 当中。"Bearer"
。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),
})
}
IdentityKey
将用户名存入上下文信息。"identity"
。上述代码大部分是通过 hz
命令行工具生成的脚手架代码,开发者无需花费大量时间在构建一个良好的代码结构上,专注于业务的编写即可。
hz new -mod github.com/cloudwego/hertz-examples/bizdemo/hertz_jwt
更进一步,在使用代码生成命令时,指定 IDL 文件,可以一并生成通信实体、路由注册代码。
示例代码(源自 hz
官方文档):
// idl/hello.thrift
namespace go hello.example
struct HelloReq {
1: string Name (api.query="name"); // 添加 api 注解为方便进行参数绑定
}
struct HelloResp {
1: string RespBody;
}
service HelloService {
HelloResp HelloMethod(1: HelloReq request) (api.get="/hello");
}
// 在 GOPATH 下执行
hz new -idl idl/hello.thrift
hertz 使用开源库 go-tagexpr 进行参数的绑定及验证,demo 中也频繁使用了这个特性。
var loginStruct struct {
// 通过声明 tag 进行参数绑定和验证
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
}
更多操作可以参考文档
更多 Gorm 操作 MySQL 的信息可以参考 Gorm
cd bizdemo/hertz_jwt && docker-compose up
连接 mysql 之后,执行 user.sql
cd bizdemo/hertz_jwt && go run main.go
# 请求
curl --location --request POST 'localhost:8888/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"Username": "admin",
"Email": "[email protected]",
"Password": "admin"
}'
# 响应
{
"code": 200,
"message": "success"
}
# 请求
curl --location --request POST 'localhost:8888/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"Account": "admin",
"Password": "admin"
}'
# 响应
{
"code": 200,
"expire": "2022-11-16T11:05:24+08:00",
"message": "success",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Njg1Njc5MjQsImlkIjoyLCJvcmlnX2lhdCI6MTY2ODU2NDMyNH0.qzbDJLQv4se6dOHN51p21Rp3DjV1Lf131l_5k4cK6Wk"
}
# 请求
curl --location --request GET 'localhost:8888/auth/ping' \
--header 'Authorization: Bearer ${token}'
# 响应
{
"message": "username:admin"
}
感兴趣可以关注公众号 「白泽talk」,白泽目前也打算打造一个氛围良好的行业交流群,文章的更新也会提前预告,欢迎加入:622383022。