前言
上一篇文章简单介绍了一个高性能的 go http 框架——hertz,本篇文章将围绕 hertz 开源仓库的一个 demo,讲述如何使用 hertz 完成 jwt 的认证与授权流程。
这里要说明的是,hertz-jwt 是 hertz 众多外部扩展组件之一,hertz 丰富的扩展生态为开发者带来了很大的便利,值得你在本文之外自行探索。

demo 介绍
- 使用命令行工具
hz生成代码 - 使用
jwt扩展完成登陆认证和授权访问 - 使用
gorm访问mysql数据库
demo 下载
git clone https://github.com/cloudwego/hertz-examples.git
cd bizdemo/hertz_jwt
demo 结构
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 分析
下方是这个 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{}
},
})
- authenticator:用于设置登录时认证用户信息的函数,demo 当中定义了一个
loginstruct结构接收用户登陆信息,并进行认证有效性。这个函数的返回值users[0]将为后续生成 jwt token 提供 payload 数据源。 - payloadfunc:它的入参就是
authenticator的返回值,此时负责解析users[0],并将用户名注入 token 的 payload 部分。
- key:指定了用于加密 jwt token 的密钥为
"secret key"。 - timeout:指定了 token 有效期为一个小时。
- maxrefresh:用于设置最大 token 刷新时间,允许客户端在
tokentime+maxrefresh内刷新 token 的有效时间,追加一个timeout的时长。
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:在登陆成功之后,jwt token 信息会随响应返回,你可以自定义这部分的具体内容,但注意不要改动函数签名,因为它与
loginhandler是强绑定的。
token 的校验
访问配置了 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,
})
},
})
- tokenlookup:用于设置 token 的获取源,可以选择
header、query、cookie、param,默认为header:authorization,同时存在是以左侧一个读取到的优先。当前 demo 将以header为数据源,因此在访问/ping接口时,需要你将 token 信息存放在 http header 当中。 - tokenheadname:用于设置从 header 中获取 token 时的前缀,默认为
"bearer"。
- httpstatusmessagefunc:用于设置 jwt 校验流程发生错误时响应所包含的错误信息,你可以自行包装这些内容。
- unauthorized:用于设置 jwt 验证流程失败的响应函数,当前 demo 返回了错误码和错误信息。
用户信息的提取
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:用于设置获取身份信息的函数,在 demo 中,此处提取 token 的负载,并配合
identitykey将用户名存入上下文信息。 - identitykey:用于设置检索身份的键,默认为
"identity"。 - ping:构造响应结果,从上下文信息中取出用户名信息并返回。
其他组件
代码生成
上述代码大部分是通过 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
更多 gorm 操作 mysql 的信息可以参考 gorm
demo 运行
- 运行 mysql docker 容器
cd bizdemo/hertz_jwt && docker-compose up
- 创建 mysql 数据库
连接 mysql 之后,执行 user.sql
- 运行 demo
cd bizdemo/hertz_jwt && go run main.go
api 请求
注册
# 请求
curl --location --request post 'localhost:8888/register' \
--header 'content-type: application/json' \
--data-raw '{
"username": "admin",
"email": "admin@test.com",
"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"
}
授权访问 ping
# 请求
curl --location --request get 'localhost:8888/auth/ping' \
--header 'authorization: bearer ${token}'
# 响应
{
"message": "username:admin"
}
参考文献
- 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
发表评论