【CloudWeGo】字节跳动 Golang 微服务框架 Hertz 集成 jwt 实践

322 阅读5分钟

hello,大家好,我是千羽。

前几天聊了持久层框架:gorm、gen 等等,今天探讨如何在 Hertz 中集成 JWT。

一、JWT:安全令牌的核心

JWT(JSON Web Token) 对于程序员应该是最常见不过了,主要的应用场景也身份认证,比如你在抖音点赞收藏等等都需要登录验证信息,同时,在微服务架构日益流行的今天,如何确保服务之间通信的安全是至关重要的问题。

JWT 作为一种开放标准(RFC 7519),在身份验证和授权方面有着独特的优势。它由头部(Header)、载荷(Payload)和签名(Signature)三部分组成。

头部包含了令牌的类型(JWT)和所使用的签名算法信息,如 HMAC SHA256 等。载荷则承载了用户相关的信息,像用户 ID、角色、权限等,这些信息经过编码后被封装在令牌内。而签名是通过对头部和载荷使用特定密钥和算法生成的,用于保证令牌的完整性和真实性。

在微服务通信中,JWT 可以作为一种轻量级的、自包含的认证和授权凭证,服务端可以轻松地验证令牌的有效性,从而决定是否允许请求的访问。

二、hertz_jwt 项目目录结构

以下是 Hertz 项目的基本目录结构,展示了如何集成 JWT

├── Makefile # 定义项目的自动化任务脚本。
├── biz
│   ├── dal
│   │   ├── init.go
│   │   └── mysql
│   │       ├── init.go # 连接信息
│   │       └── user.go # 包含对用户数据在 MySQL 中的操作。
│   ├── handler
│   │   ├── ping.go
│   │   └── register.go # 处理用户注册handler请求。
│   ├── model
│   │   ├── sql
│   │   │   └── user.sql  # 定义用户数据结构体和相关方法。
│   │   └── user.go
│   ├── mw
│   │   └── jwt.go # JWT 相关的中间件代码。
│   ├── router
│   │   └── register.go
│   └── utils
│       └── md5.go 
├── docker-compose.yml # 定义和运行多个 Docker 容器的配置文件。
├── go.mod
├── go.sum
├── main.go
├── readme.md
├── router.go
└── router_gen.go

三、如何运行

Step 1:使用 Docker 启动 MySQL容器

cd bizdemo/hertz_jwt && docker-compose up

docker运行mysql

Step 2:编译并运行项目

cd bizdemo/hertz_jwt && go run main.go

看到下面的日志👇,就说明启动成功啦

HERTZ: HTTP server listening on address=[::]:8888

四、API 接口调试

根据启动的日志,我们进行各个接口验证

1. /register 用户注册接口

请求url:http://localhost:8888/register

请求参数:

{
    "username": "hertz_jwt",
    "email": "1122@qq.com",
    "password": "password"
}

响应结果:

{
    "code": 200,
    "message": "success"
}

数据库验证:

2. /login 用户登录接口

请求:http://localhost:8888/login

请求参数:

{
    "account": "hertz_jwt",
    "password": "password"
}

响应结果

{
    "code": 200,
    "expire": "2024-11-10T21:57:05+08:00",
    "message": "success",
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzEyNDcwMjUsImlkZW50aXR5IjoiaGVydHpfand0Iiwib3JpZ19pYXQiOjE3MzEyNDM0MjV9.lOnyoa-VSz9yTbUdjxF9h2I-4iJNbHh2vfqSqRE7mYI"
}

3. /ping 接口

请求:http://localhost:8888/ping

需要在auth里头,选择Bear Token 把登录成功返回的token的一大串字符串复制进去就好

响应结果:

{
    "message": "username:hertz_jwt"
}

五、代码解析

5.1 /register 用户注册接口代码解析

Hertz 中集成 JWT 的步骤

(一)引入相关库

首先,在你的 Hertz 项目中,需要引入处理 JWT 的 Golang 库。有多种选择,比如 "github.com/golang-jwt/jwt/v4",这个库提供了丰富的功能来生成、解析和验证 JWT。

(二)生成 JWT 令牌

在用户认证成功的逻辑中,我们可以使用密钥和相关用户信息来生成 JWT。例如:

(三)在 Hertz 中间件中验证 JWT

创建一个 Hertz 中间件来拦截请求,并验证请求中携带的 JWT。

(四)注册中间件到 Hertz 路由

在 Hertz 的路由设置中,将 JwtAuthMiddleware 注册到需要认证的路由上。

5.1.1 路由 router.go

// 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)
}

5.1.2 Register handler

// Register user register handler
func Register(ctx context.Context, c *app.RequestContext) {
	// 定义一个结构体用于接收用户注册时传入的参数
	var registerStruct struct {
		Username string `form:"username" json:"username" query:"username" vd:"(len($) > 0 && len($) < 128); msg:'Illegal format'"`
		Email    string `form:"email" json:"email" query:"email" vd:"(len($) > 0 && len($) < 128) && email($); msg:'Illegal format'"`
		Password string `form:"password" json:"password" query:"password" vd:"(len($) > 0 && len($) < 128); msg:'Illegal format'"`
	}
	// 绑定请求中的参数到结构体并进行验证
	if err := c.BindAndValidate(&registerStruct); err != nil {
		c.JSON(http.StatusOK, utils.H{
			"message": err.Error(),
			"code":    http.StatusBadRequest,
		})
		return
	}
	// 根据用户名和邮箱查询用户是否已存在
	users, err := mysql.FindUserByNameOrEmail(registerStruct.Username, registerStruct.Email)
	if err != nil {
		c.JSON(http.StatusOK, utils.H{
			"message": err.Error(),
			"code":    http.StatusBadRequest,
		})
		return
	}
	// 如果查询到的用户数量不为 0,说明用户已存在
	if len(users) != 0 {
		c.JSON(http.StatusOK, utils.H{
			"message": "user already exists",
			"code":    http.StatusBadRequest,
		})
		return
	}
	// 创建新用户,对密码进行 MD5 加密后存储
	if err = mysql.CreateUsers([]*model.User{
		{
			UserName: registerStruct.Username,
			Email:    registerStruct.Email,
			Password: utils2.MD5(registerStruct.Password),
		},
	}); err != nil {
		// 如果创建用户过程中出现错误,返回错误信息和状态码
		c.JSON(http.StatusOK, utils.H{
			"message": err.Error(),
			"code":    http.StatusBadRequest,
		})
		return
	}
	// 注册成功,返回成功信息和状态码
	c.JSON(http.StatusOK, utils.H{
		"message": "success",
		"code":    http.StatusOK,
	})
}

mysql操作层实现

func CreateUsers(users []*model.User) error {
	return DB.Create(users).Error
}

func FindUserByNameOrEmail(userName, email string) ([]*model.User, error) {
	res := make([]*model.User, 0)
	if err := DB.Where(DB.Or("user_name = ?", userName).
		Or("email = ?", email)).
		Find(&res).Error; err != nil {
		return nil, err
	}
	return res, nil
}

func CheckUser(account, password string) ([]*model.User, error) {
	res := make([]*model.User, 0)
	if err := DB.Where(DB.Or("user_name = ?", account).
		Or("email = ?", account)).Where("password = ?", password).
		Find(&res).Error; err != nil {
		return nil, err
	}
	return res, nil
}

5.2 登录实现

通过JwtMiddleware登录相对简单一些,直接是一行代码搞定。

r.POST("/login", mw.JwtMiddleware.LoginHandler)

通过分析auth_jwt.go源码可以发现 首先检查是否设置了认证函数,如果没有则返回内部服务器错误。

然后调用认证函数进行用户认证,如果认证失败则返回未授权错误。接着创建 JWT 令牌,设置令牌的载荷和过期时间,并对令牌进行签名。

如果设置了发送 Cookie,则将令牌字符串设置到 Cookie 中。

最后调用登录响应函数,返回登录成功的状态码、令牌字符串和过期时间。

ping接口

最后的ping接口,和前面的例子有区别,这次是加了token鉴权处理的,需要拿到token进行才能请求成功。

// 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),
	})
}

分析一下c.Get()获取key方法实现


// Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false)
func (ctx *RequestContext) Get(key string) (value interface{}, exists bool) {
	ctx.mu.RLock()
	value, exists = ctx.Keys[key]
	ctx.mu.RUnlock()
	return
}

对请求上下文的互斥锁(mu)加读锁,以确保在读取Keys字典时的线程安全。

然后,在Keys字典中查找给定的键,并将找到的值和一个表示键是否存在的布尔值分别赋值给value和exists变量。最后,解读锁并返回找到的值和是否存在的标志。

这种情况下安全地获取存储在其中的特定键的值,适用于在处理请求过程中需要获取上下文特定数据的场景。

参考文献:

  1. www.cloudwego.io/zh/docs/her…