温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

go-zero框架之如何理解rest

发布时间:2021-10-23 17:33:44 来源:亿速云 阅读:211 作者:iii 栏目:编程语言

这篇文章主要介绍“go-zero框架之如何理解rest”,在日常操作中,相信很多人在go-zero框架之如何理解rest问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”go-zero框架之如何理解rest”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

服务创建

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

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的原理流程如下图

<img src="https://oscimg.oschina.net/oscnet/up-cd9dc0dbd93e7be4b46a3e8cba1f3438ccd.png" alt="jwt" >

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, &amp;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(), &amp;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框架中我们可以通过中间件来实现诸如鉴权、限流、熔断等等功能,中间件的原理流程如下图

go-zero框架之如何理解rest

rest框架中内置了非常丰富的中间件,在rest/handler路径下,通过alice工具把所有中间件链接起来,当发起请求时会依次通过每一个中间件,当满足所有条件后最终请求才会到达真正的业务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 := &amp;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即可查看相关的指标信息

go-zero框架之如何理解rest

路由原理

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

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

<img src="https://oscimg.oschina.net/oscnet/up-fc2d96766688c6ce4f652588786cd46021d.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) &gt; 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)
	}
}

到此,关于“go-zero框架之如何理解rest”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI