1. 简介
Hertz 网关是一个微服务网关,由CloudWeGo团队开发,于2021年正式发布。它基于Go语言编写,支持HTTP/gRPC/Thrift协议,具备高并发、高性能、高可用等特性,可以部署在Kubernetes环境中。Hertz 网关具有动态路由、限流、熔断、黑白名单、认证鉴权等功能,并且支持可插拔的扩展机制。
2. 先上脚手架
- 安装依赖
go install github.com/cloudwego/hertz/cmd/hz@latest
- 搭建脚手架
hz new
- 包的结构目录
3. 包结构解释
- handle包: 处理接口的处理器
- router.go:路由配置器
- router_gen.go: 路由注册(将router.go中所有的路由都给注册)
- main.go:主方法,开启服务,配置中间件,注册路由,并监听服务
4. 简单构建一个接口
- 处理器中编写函数结构为fun(ctx context.Context, c *app.RequestContext)
func Ping(ctx context.Context, c *app.RequestContext) {
c.JSON(consts.StatusOK, utils.H{
"message": "pong",
})
}
- 路由绑定
func customizedRegister(r *server.Hertz) {
r.GET("/ping", handler.Ping)
// your code ...
}
5. 参数的获取和校验
- 参数获取
func HelloPerson(ctx context.Context, c *app.RequestContext) {
name := c.Query("name")
age := c.Param("age")
// 请求体,自行转换成json
body, _ := c.Body()
c.JSON(200, utils.H{
"age": age,
"name": name,
})
}
- 参数校验
func PersonBind(ctx context.Context, c *app.RequestContext) {
type person struct {
Age int `path:"age" json:"age" ` // 从路径中获取参数
Name string `query:"name" json:"name" vd:"$!='Hertz'"` // 从query中获取参数
City string `json:"city"` // 从body中获取参数
}
var p person
// 参数校验
err := c.BindAndValidate(&p)
// 参数校验出错就在全局上下文中加一个错误信息
if err != nil {
fmt.Printf("%v", err.Error())
_ = c.Error(errors.WithStack(err))
return
}
c.JSON(200, utils.H{
"person": p,
})
}
标签 | 说明 | 示例 |
required | 必填字段 | Name string vd:"required"` |
min 和 max | 最小和最大长度 | Name string vd:"min=3,max=20"` |
regexp | 正则表达式匹配 | Name string vd:"regexp=^[a-zA-Z0-9]*$"` |
min 和 max | 数值范围 | Age int vd:"min=0,max=100"` |
自定义验证函数 | 使用自定义的验证函数 | Name string vd:"myCustomValidator"` |
6. 中间件的使用
- 使用中间件(tip:中间件的先后顺序和use的顺序密切相关)
func main() {
h := server.Default(
// 修改监听的端口
server.WithHostPorts("127.0.0.1:8080"),
)
// TODO 跨域报错
// 跨域的中间件
mw.Cors(h)
// 启动jtw的中间件
mw.Jwt(h)
// use方法启动中间件
h.Use(mw.MyMiddleware)
h.Use(mw.LoggingMiddleware)
// 注册路由
register(h)
// 开启监听
h.Spin()
}
- 全局异常处理器中间件
func loggingMiddleware(ctx context.Context, c *app.RequestContext) {
c.Next(ctx)
if len(c.Errors) == 0 {
// 没有收集到异常直接返回
fmt.Println("retun")
return
}
hertzErr := c.Errors[0]
// 获取errors包装的err
err := hertzErr.Unwrap()
// 打印异常堆栈
logger.CtxErrorf(ctx, "%+v", err)
// 获取原始err
err = errors.Unwrap(err)
// todo 进行错误代码进行判断
c.JSON(500, utils.H{
"code": consts.StatusOK,
"message": err.Error(),
})
}
- jwt中间件
// Jwt jwt校验
func Jwt(h *server.Hertz) {
// the jwt middleware
authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
Realm: "test zone",
Key: []byte("secret key"),
Timeout: time.Hour,
MaxRefresh: time.Hour,
IdentityKey: identityKey,
// 登录成功之后的生成jwt的token
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(*User); ok {
return jwt.MapClaims{
identityKey: v.UserName,
}
}
return jwt.MapClaims{}
},
// TODO token校验出现一些问题,我存放在请求头里面但是还是有问题
IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
claims := jwt.ExtractClaims(ctx, c)
return &User{
UserName: claims[identityKey].(string),
}
},
// 用于校验登录的,用户名和密码
Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
var loginVals login
if err := c.BindAndValidate(&loginVals); err != nil {
return "", jwt.ErrMissingLoginValues
}
userID := loginVals.Username
password := loginVals.Password
if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
return &User{
UserName: userID,
LastName: "Hertz",
FirstName: "CloudWeGo",
}, nil
}
return nil, jwt.ErrFailedAuthentication
},
// 执行权限的校验
Authorizator: func(data interface{}, ctx context.Context, c *app.RequestContext) bool {
if v, ok := data.(*User); ok && v.UserName == "admin" {
return true
}
return false
},
// 未认证的时候返回的数据
Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
c.JSON(code, map[string]interface{}{
"code": code,
"message": "请先进行登录",
})
},
})
if err != nil {
log.Fatal("JWT Error:" + err.Error())
}
// When you use jwt.New(), the function is already automatically called for checking,
// which means you don't need to call it again.
errInit := authMiddleware.MiddlewareInit()
if errInit != nil {
log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error())
}
h.POST("/login", authMiddleware.LoginHandler)
// 配置404页面
h.NoRoute(authMiddleware.MiddlewareFunc(), func(ctx context.Context, c *app.RequestContext) {
claims := jwt.ExtractClaims(ctx, c)
log.Printf("NoRoute claims: %#v\n", claims)
c.JSON(404, map[string]string{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
})
auth := h.Group("/auth")
// Refresh time can be longer than token timeout
auth.GET("/refresh_token", authMiddleware.RefreshHandler)
auth.Use(authMiddleware.MiddlewareFunc())
{
auth.GET("/ping", PingHandler)
}
}
Hertz代码生成工具(重要,重要,重要!!!!)
ⅰ. 创建一个空项目,项目下面放一个包idl
ⅱ. 编写模版文件(hello.thrift)
namespace go hello.ledger
struct HelloReq{
1: required string Id (api.query="id");
2: optional i32 age;
3: required list<string> hobbies
}
struct HelloResp{
1:string RespBody;
}
service HelloService{
HelloResp HelloMethod(1:HelloReq request)(api.get="/hello")
}
struct HelloReq2{
1: required string Id (api.query="id");
2: optional i32 age;
3: required list<string> hobbies
}
struct HelloResp2{
1:string RespBody;
}
service HelloService2{
HelloResp HelloMethod2(1:HelloReq request)(api.get="/hello2")
}
ⅲ. 执行命令
hz new -module ledger -idl idl/hello.thrift
ⅳ. 新增接口之后使用的是跟新命令
hz update -module ledger -idl idl/hello.thrift
注意点:
- 修改handler的代码,然后修改thrift源文件,之后使用命令重新生成结构体,不会被覆盖重写
- 注册路由要在使用中间件之后,不然中间件失效
.thrift文件的编写
- namespace go (hello.ledger) 生成包名
- struct 表示结构体
-
- required 表示参数必须携带(生成的结构体类型不为指针)
- optional 表示参数可选(生成的结构体的字段类型为指针)
- (api.query="id") query查询的参数映射为id
- list<string> 切片参数
- i32 具体标明int参数
- service 表示方法
-
- HelloResp 响应内容
- HelloMethod 方法名称
- (1:HelloReq request) 第一个参数
- (api.get="/hello") get请求,路径是/hello
namespace go hello.ledger
struct HelloReq{
1: required string Id (api.query="id");
2: optional i32 age;
3: required list<string> hobbies
}
struct HelloResp{
1:string RespBody;
}
service HelloService{
HelloResp HelloMethod(1:HelloReq request)(api.get="/hello")
}
struct HelloReq2{
1: required string Id (api.query="id");
2: optional i32 age;
3: required list<string> hobbies
}
struct HelloResp2{
1:string RespBody;
}
service HelloService2{
HelloResp HelloMethod2(1:HelloReq request)(api.get="/hello2")
}
hello_service.go分析
- HelloMethod 方法
-
- context.Context,携带参数上下文(在函数之间传递截止日期、取消信号和其他请求范围值的方法)和
- *app.RequestContext,包含与当前请求相关的信息
- var req ledger.HelloReq 声明一个包下面的请求参数变量
-
- c.BindAndValidate(&req) 校验并赋值
- resp := new(ledger.HelloResp) 构造一个响应的参数
-
- 赋值
- c.JSON(consts.StatusOK, resp) 写入json 的响应格式,之后返回resp
func HelloMethod(ctx context.Context, c *app.RequestContext) {
var req ledger.HelloReq
err := c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(ledger.HelloResp)
resp.RespBody = "id: " + req.Id +
" age: " + strconv.FormatInt(int64(*req.Age), 10) +
" hobbies: " + strings.Join(req.Hobbies, ",")
c.JSON(consts.StatusOK, resp)
}
hello.go(router)
- 路由映射的地方
func Register(r *server.Hertz) {
root := r.Group("/", rootMw()...)
root.GET("/hello", append(_hellomethodMw(), ledger.HelloMethod)...)
root.GET("/hello2", append(_hellomethod2Mw(), ledger.HelloMethod2)...)
}
middleware.go
- rootMw 全局的中间件配置
- _hellomethodMw 对HelloMethod方法单独的中间件
- _hellomethod2Mw 对HelloMethod2方法单独的中间件
func rootMw() []app.HandlerFunc {
// your code...
return nil
}
func _hellomethodMw() []app.HandlerFunc {
// your code...
return nil
}
func _hellomethod2Mw() []app.HandlerFunc {
// your code...
return []app.HandlerFunc{
TestMw,
}
}
- 中间件的实现很简单,编写一个这样的函数就可以了
type HandlerFunc func(c context.Context, ctx *RequestContext)
处理器的编写
- 第一个参数context.Context(用于携带截止日期、取消信号、请求范围值以及其他请求相关的元数据)
func HelloMethod(ctx context.Context, c *app.RequestContext) {
// 示例:从上下文中获取截止日期
deadline, ok := ctx.Deadline()
if ok {
// 处理截止日期
}
// 示例:通过上下文传递值
value := ctx.Value("key")
if value != nil {
// 处理传递的值
}
// 示例:处理上下文的取消信号
select {
case <-ctx.Done():
// 上下文已取消,执行清理操作
default:
// 继续处理请求
}
// 其他可能的上下文方法和用法...
}
- 第二个参数*app.RequestContext 与请求相关的参数
// Host 返回请求的主机信息,以字节切片形式表示。
func (ctx *RequestContext) Host() []byte
// FullPath 返回完整的请求路径。
func (ctx *RequestContext) FullPath() string
// SetFullPath 设置请求的完整路径。
func (ctx *RequestContext) SetFullPath(p string)
// Path 返回请求路径的字节切片形式。
func (ctx *RequestContext) Path() []byte
// Param 根据给定的键返回请求参数的值。
func (ctx *RequestContext) Param(key string) string
// Query 根据给定的键返回请求的查询参数值。
func (ctx *RequestContext) Query(key string) string
// DefaultQuery 根据给定的键返回请求的查询参数值,如果参数不存在则返回默认值。
func (ctx *RequestContext) DefaultQuery(key, defaultValue string) string
// GetQuery 返回请求的查询参数值以及一个布尔值,指示参数是否存在。
func (ctx *RequestContext) GetQuery(key string) (string, bool)
// QueryArgs 返回请求的查询参数,以 protocol.Args 的形式。
func (ctx *RequestContext) QueryArgs() *protocol.Args
// URI 返回请求的 URI(Uniform Resource Identifier)信息。
func (ctx *RequestContext) URI() *protocol.URI
- 第二个参数*app.RequestContext 与响应相关的参数
// SetContentType 设置响应的内容类型。
func (ctx *RequestContext) SetContentType(contentType string)
// SetContentTypeBytes 以字节切片形式设置响应的内容类型。
func (ctx *RequestContext) SetContentTypeBytes(contentType []byte)
// SetConnectionClose 设置响应头,要求在完成响应后立即关闭连接。
func (ctx *RequestContext) SetConnectionClose()
// SetStatusCode 设置响应的状态码。
func (ctx *RequestContext) SetStatusCode(statusCode int)
// Status 设置响应的状态码。
func (ctx *RequestContext) Status(code int)
// NotFound 设置响应状态码为404 Not Found。
func (ctx *RequestContext) NotFound()
// NotModified 设置响应状态码为304 Not Modified。
func (ctx *RequestContext) NotModified()
// Redirect 发送重定向响应。
func (ctx *RequestContext) Redirect(statusCode int, uri []byte)
// Header 设置响应头中的键值对。
func (ctx *RequestContext) Header(key, value string)
// SetCookie 设置响应的cookie。
func (ctx *RequestContext) SetCookie(name, value string, maxAge int, path, domain string, sameSite protocol.CookieSameSite, secure, httpOnly bool)
// AbortWithStatus 中止请求并返回指定的HTTP状态码。
func (ctx *RequestContext) AbortWithStatus(code int)
// AbortWithError 中止请求并返回指定的HTTP状态码和错误信息。
func (ctx *RequestContext) AbortWithError(code int, err error) *errors.Error