如何开发rest接口服务_好未来开源框架 gozero:如何用它进行 rest 开发?

点击上方蓝色“polarisxu”关注我,设个星标,不会让你失望

go-zero 是一个集成了各种工程实践的 web 和 rpc 框架,其中 rest 是 web 框架模块,基于 Go 语言原生的 http 包进行构建,是一个轻量的,高性能的,功能完整的,简单易用的 web 框架。

服务创建

go-zero 中创建 http 服务非常简单,官方推荐使用goctl[1]工具来生成。为了方便演示,这里通过手动创建服务,代码如下

package main

import (
 "log"
 "net/http"

 "github.com/tal-tech/go-zero/core/logx"
 "github.com/tal-tech/go-zero/core/service"
 "github.com/tal-tech/go-zero/rest"
 "github.com/tal-tech/go-zero/rest/httpx"
)

func main() {
 srv, err := rest.NewServer(rest.RestConf{
  Port: 9090, // 侦听端口
  ServiceConf: service.ServiceConf{
   Log: logx.LogConf{Path: "./logs"}, // 日志路径
  },
 })
 if err != nil {
  log.Fatal(err)
 }
 defer srv.Stop()
 // 注册路由
 srv.AddRoutes([]rest.Route{
  {
   Method:  http.MethodGet,
   Path:    "/user/info",
   Handler: userInfo,
  },
 })

 srv.Start() // 启动服务
}

type User struct {
 Name  string `json:"name"`
 Addr  string `json:"addr"`
 Level int    `json:"level"`
}

func userInfo(w http.ResponseWriter, r *http.Request) {
 var req struct {
  UserId int64 `form:"user_id"` // 定义参数
 }
 if err := httpx.Parse(r, &req); err != nil { // 解析参数
  httpx.Error(w, err)
  return
 }
 users := map[int64]*User{
  1: &User{"go-zero", "shanghai", 1},
  2: &User{"go-queue", "beijing", 2},
 }
 httpx.WriteJson(w, http.StatusOK, users[req.UserId]) // 返回结果
}

通过 rest.NewServer 创建服务,示例配置了端口号和日志路径,服务启动后侦听在 9090 端口,并在当前目录下创建 logs 目录同时创建各等级日志文件

然后通过 srv.AddRoutes 注册路由,每个路由需要定义该路由的方法、Path 和 Handler,其中 Handler 类型为 http.HandlerFunc

最后通过 srv.Start 启动服务,启动服务后通过访问http://localhost:9090/user/info?user_id=1可以看到返回结果

{
 name: "go-zero",
 addr: "shanghai",
 level: 1
}

到此一个简单的 http 服务就创建完成了,可见使用 rest 创建 http 服务非常简单,主要分为三个步骤:创建 Server、注册路由、启动服务

JWT 鉴权

鉴权几乎是每个应用必备的能力,鉴权的方式很多,而 jwt 是其中比较简单和可靠的一种方式,在 rest 框架中内置了 jwt 鉴权功能,jwt 的原理流程如下图

dee6b9e4a5493dd76ff5d9282e456220.png

rest 框架中通过 rest.WithJwt(secret)启用 jwt 鉴权,其中 secret 为服务器秘钥是不能泄露的,因为需要使用 secret 来算签名验证 payload 是否被篡改,如果 secret 泄露客户端就可以自行签发 token,黑客就能肆意篡改 token 了。我们基于上面的例子进行改造来验证在 rest 中如何使用 jwt 鉴权

获取 jwt

第一步客户端需要先获取 jwt,在登录接口中实现 jwt 生成逻辑

srv.AddRoute(rest.Route{
  Method:  http.MethodPost,
  Path:    "/user/login",
  Handler: userLogin,
})

为了演示方便,userLogin 的逻辑非常简单,主要是获取信息然后生成 jwt,获取到的信息存入 jwt payload 中,然后返回 jwt

func userLogin(w http.ResponseWriter, r *http.Request) {
 var req struct {
  UserName string `json:"user_name"`
  UserId   int    `json:"user_id"`
 }
 if err := httpx.Parse(r, &req); err != nil {
  httpx.Error(w, err)
  return
 }
 token, _ := genToken(accessSecret, map[string]interface{}{
  "user_id":   req.UserId,
  "user_name": req.UserName,
 }, accessExpire)

 httpx.WriteJson(w, http.StatusOK, struct {
  UserId   int    `json:"user_id"`
  UserName string `json:"user_name"`
  Token    string `json:"token"`
 }{
  UserId:   req.UserId,
  UserName: req.UserName,
  Token:    token,
 })
}

生成 jwt 的方法如下

func genToken(secret string, payload map[string]interface{}, expire int64) (string, error) {
 now := time.Now().Unix()
 claims := make(jwt.MapClaims)
 claims["exp"] = now + expire
 claims["iat"] = now
 for k, v := range payload {
  claims[k] = v
 }
 token := jwt.New(jwt.SigningMethodHS256)
 token.Claims = claims
 return token.SignedString([]byte(secret))
}

启动服务后通过 cURL 访问

curl -X "POST" "http://localhost:9090/user/login" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "user_name": "gozero",
  "user_id": 666
}'

会得到如下返回结果

{
  "user_id": 666,
  "user_name": "gozero",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM"
}

添加 Header

通过 rest.WithJwt(accessSecret)启用 jwt 鉴权

srv.AddRoute(rest.Route{
  Method:  http.MethodGet,
  Path:    "/user/data",
  Handler: userData,
}, rest.WithJwt(accessSecret))

访问/user/data 接口返回 401 Unauthorized 鉴权不通过,添加 Authorization Header,即能正常访问

curl "http://localhost:9090/user/data?user_id=1" \
      -H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM'

获取信息

一般会将用户的信息比如用户 id 或者用户名存入 jwt 的 payload 中,然后从 jwt 的 payload 中解析出我们预存的信息,即可知道本次请求时哪个用户发起的

func userData(w http.ResponseWriter, r *http.Request) {
 var jwt struct {
  UserId   int    `ctx:"user_id"`
  UserName string `ctx:"user_name"`
 }
 err := contextx.For(r.Context(), &jwt)
 if err != nil {
  httpx.Error(w, err)
 }
 httpx.WriteJson(w, http.StatusOK, struct {
  UserId   int    `json:"user_id"`
  UserName string `json:"user_name"`
 }{
  UserId:   jwt.UserId,
  UserName: jwt.UserName,
 })
}

实现原理

jwt 鉴权的实现在 authhandler.go 中,实现原理也比较简单,先根据 secret 解析 jwt token,验证 token 是否有效,无效或者验证出错则返回 401 Unauthorized

func unauthorized(w http.ResponseWriter, r *http.Request, err error, callback UnauthorizedCallback) {
 writer := newGuardedResponseWriter(w)

 if err != nil {
  detailAuthLog(r, err.Error())
 } else {
  detailAuthLog(r, noDetailReason)
 }
 if callback != nil {
  callback(writer, r, err)
 }

 writer.WriteHeader(http.StatusUnauthorized)
}

验证通过后把 payload 中的信息存入 http request 的 context 中

ctx := r.Context()
for k, v := range claims {
  switch k {
    case jwtAudience, jwtExpire, jwtId, jwtIssueAt, jwtIssuer, jwtNotBefore, jwtSubject:
    // ignore the standard claims
    default:
    ctx = context.WithValue(ctx, k, v)
  }
}

next.ServeHTTP(w, r.WithContext(ctx))

中间件

web 框架中的中间件是实现业务和非业务功能解耦的一种方式,在 web 框架中我们可以通过中间件来实现诸如鉴权、限流、熔断等等功能,中间件的原理流程如下图

6fcc2521e0091f017bbf9ff4709ec8cf.png

handler

rest 框架中内置了非常丰富的中间件,在 rest/handler 路径下,通过alice[2]工具把所有中间件链接起来,当发起请求时会依次通过每一个中间件,当满足所有条件后最终请求才会到达真正的业务 Handler 执行业务逻辑,上面介绍的 jwt 鉴权就是通过 authHandler 来实现的。由于内置中间件比较多篇幅有限不能一一介绍,感兴趣的伙伴可以自行学习,这里我们介绍一下 prometheus 指标收集的中间件 PromethousHandler,代码如下

func PromethousHandler(path string) func(http.Handler) http.Handler {
 return func(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   startTime := timex.Now() // 起始时间
   cw := &security.WithCodeResponseWriter{Writer: w}
   defer func() {
        // 耗时
    metricServerReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), path)
        // code码
    metricServerReqCodeTotal.Inc(path, strconv.Itoa(cw.Code))
   }()

   next.ServeHTTP(cw, r)
  })
 }
}

在该中间件中,在请求开始时记录了起始时间,在请求结束后在 defer 中通过 prometheus 的 Histogram 和 Counter 数据类型分别记录了当前请求 path 的耗时和返回的 code 码,此时我们通过访问http://127.0.0.1:9101/metrics即可查看相关的指标信息

2f38e3654514dfb92a7567be4fddcd3e.png

rest_metric

路由原理

rest 框架中通过 AddRoutes 方法来注册路由,每一个 Route 有 Method、Path 和 Handler 三个属性,Handler 类型为 http.HandlerFunc,添加的路由会被换成 featuredRoutes 定义如下

featuredRoutes struct {
  priority  bool // 是否优先级
  jwt       jwtSetting  // jwt配置
  signature signatureSetting // 验签配置
  routes    []Route  // 通过AddRoutes添加的路由
 }

featuredRoutes 通过 engine 的 AddRoutes 添加到 engine 的 routes 属性中

func (s *engine) AddRoutes(r featuredRoutes) {
 s.routes = append(s.routes, r)
}

调用 Start 方法启动服务后会调用 engine 的 Start 方法,然后会调用 StartWithRouter 方法,该方法内通过 bindRoutes 绑定路由

func (s *engine) bindRoutes(router httpx.Router) error {
 metrics := s.createMetrics()

 for _, fr := range s.routes {
  if err := s.bindFeaturedRoutes(router, fr, metrics); err != nil { // 绑定路由
   return err
  }
 }

 return nil
}

最终会调用 patRouter 的 Handle 方法进行绑定,patRouter 实现了 Router 接口

type Router interface {
 http.Handler
 Handle(method string, path string, handler http.Handler) error
 SetNotFoundHandler(handler http.Handler)
 SetNotAllowedHandler(handler http.Handler)
}

patRouter 中每一种请求方法都对应一个树形结构,每个树节点有两个属性 item 为 path 对应的 handler,而 children 为带路径参数和不带路径参数对应的树节点, 定义如下:

node struct {
  item     interface{}
  children [2]map[string]*node
}

Tree struct {
  root *node
}

通过 Tree 的 Add 方法把不同 path 与对应的 handler 注册到该树上我们通过一个图来展示下该树的存储结构,比如我们定义路由如下

{
  Method:  http.MethodGet,
  Path:    "/user",
  Handler: userHander,
},
{
  Method:  http.MethodGet,
  Path:    "/user/infos",
  Handler: infosHandler,
},
{
  Method:  http.MethodGet,
  Path:    "/user/info/:id",
  Handler: infoHandler,
},

路由存储的树形结构如下图

52fb1fdba45a54f25acc54ee1c3d0672.png

当请求来的时候会调用 patRouter 的 ServeHTTP 方法,在该方法中通过 tree.Search 方法找到对应的 handler 进行执行,否则会执行 notFound 或者 notAllow 的逻辑

func (pr *patRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 reqPath := path.Clean(r.URL.Path)
 if tree, ok := pr.trees[r.Method]; ok {
  if result, ok := tree.Search(reqPath); ok { // 在树中搜索对应的handler
   if len(result.Params) > 0 {
    r = context.WithPathVars(r, result.Params)
   }
   result.Item.(http.Handler).ServeHTTP(w, r)
   return
  }
 }

 allow, ok := pr.methodNotAllowed(r.Method, reqPath)
 if !ok {
  pr.handleNotFound(w, r)
  return
 }

 if pr.notAllowed != nil {
  pr.notAllowed.ServeHTTP(w, r)
 } else {
  w.Header().Set(allowHeader, allow)
  w.WriteHeader(http.StatusMethodNotAllowed)
 }
}

总结

本文从整体上介绍了 rest,通过该篇文章能够基本了解 rest 的设计和主要功能,其中中间件部分是重点,里面集成了各种服务治理相关的功能,并且是自动集成的不需要我们做任何配置,其他功能比如参数自动效验等功能由于篇幅有限在这里就不做介绍了,感兴趣的朋友可以自行查看官方文档进行学习。go-zero 中不光有 http 协议还提供了 rpc 协议和各种提高性能和开发效率的工具,是一款值得我们深入学习和研究的框架。

项目地址

https://github.com/tal-tech/go-zero[3]

如果觉得文章不错,欢迎 github[4] 点个 star ?

参考资料

[1]

goctl: https://github.com/tal-tech/go-zero/tree/master/tools/goctl

[2]

alice: https://github.com/justinas/alice

[3]

https://github.com/tal-tech/go-zero: https://github.com/tal-tech/go-zero

[4]

github: https://github.com/tal-tech/go-zero


往期推荐

  • Go 语言名人:除了 Rob Pike,很多人可能不知道他

觉得不错,欢迎关注:

1180694c9bd80c96c0b09f7c6fc98bda.png

点个赞、在看和转发是最大的支持

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值