使用 Go HTTP 框架 Hertz 进行 JWT 认证

IT技术2年前 (2022)发布 IT大王
0

前言

上一篇文章简单介绍了一个高性能的 Go HTTP 框架——Hertz,本篇文章将围绕 Hertz 开源仓库的一个 demo,讲述如何使用 Hertz 完成 JWT 的认证与授权流程。

这里要说明的是,hertz-jwt 是 Hertz 众多外部扩展组件之一,Hertz 丰富的扩展生态为开发者带来了很大的便利,值得你在本文之外自行探索。

使用 Go HTTP 框架 Hertz 进行 JWT 认证

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 完成持久化,因此在登陆之前,需要对用户进行注册,注册流程为:

  1. 获取用户名密码和邮箱
  2. 判断用户是否存在
  3. 创建用户

用户登陆(认证)

服务器需要在用户第一次登陆的时候,验证用户账号和密码,并签发 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 的获取源,可以选择 headerquerycookieparam,默认为 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
© 版权声明
好牛新坐标 广告
版权声明:
1、IT大王遵守相关法律法规,由于本站资源全部来源于网络程序/投稿,故资源量太大无法一一准确核实资源侵权的真实性;
2、出于传递信息之目的,故IT大王可能会误刊发损害或影响您的合法权益,请您积极与我们联系处理(所有内容不代表本站观点与立场);
3、因时间、精力有限,我们无法一一核实每一条消息的真实性,但我们会在发布之前尽最大努力来核实这些信息;
4、无论出于何种目的要求本站删除内容,您均需要提供根据国家版权局发布的示范格式
《要求删除或断开链接侵权网络内容的通知》:https://itdw.cn/ziliao/sfgs.pdf,
国家知识产权局《要求删除或断开链接侵权网络内容的通知》填写说明: http://www.ncac.gov.cn/chinacopyright/contents/12227/342400.shtml
未按照国家知识产权局格式通知一律不予处理;请按照此通知格式填写发至本站的邮箱 wl6@163.com

相关文章