1

使用 Go HTTP 框架 Hertz 进行 JWT 认证 - 白泽来了

 1 year ago
source link: https://www.cnblogs.com/YLTFY1998/p/16898513.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

534815a2d9d74778aa4f3a0623006f17~tplv-k3u1fbpfcp-zoom-1.image

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. 判断用户是否存在

用户登陆(认证)

服务器需要在用户第一次登陆的时候,验证用户账号和密码,并签发 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 操作 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": "[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"
}

授权访问 Ping

# 请求
curl --location --request GET 'localhost:8888/auth/ping' \
--header 'Authorization: Bearer ${token}'
# 响应
{
    "message": "username:admin"
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK