前言
上一篇文章简单介绍了一个高性能的 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" }