go-zero之web框架

go-zero 是一個集成了各種工程實踐的 web 和 rpc 框架,其中rest是web框架模塊,基于Go語言原生的http包進行構(gòu)建,是一個輕量的,高性能的,功能完整的,簡單易用的web框架

服務(wù)創(chuàng)建

go-zero中創(chuàng)建http服務(wù)非常簡單,官方推薦使用goctl工具來生成。為了方便演示,這里通過手動創(chuàng)建服務(wù),代碼如下

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() // 啟動服務(wù)
}

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"` // 定義參數(shù)
    }
    if err := httpx.Parse(r, &req); err != nil { // 解析參數(shù)
        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]) // 返回結(jié)果
}

通過rest.NewServer創(chuàng)建服務(wù),示例配置了端口號和日志路徑,服務(wù)啟動后偵聽在9090端口,并在當前目錄下創(chuàng)建logs目錄同時創(chuàng)建各等級日志文件

然后通過srv.AddRoutes注冊路由,每個路由需要定義該路由的方法、Path和Handler,其中Handler類型為http.HandlerFunc

最后通過srv.Start啟動服務(wù),啟動服務(wù)后通過訪問http://localhost:9090/user/info?user_id=1可以看到返回結(jié)果

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

到此一個簡單的http服務(wù)就創(chuàng)建完成了,可見使用rest創(chuàng)建http服務(wù)非常簡單,主要分為三個步驟:創(chuàng)建Server、注冊路由、啟動服務(wù)

JWT鑒權(quán)

鑒權(quán)幾乎是每個應(yīng)用必備的能力,鑒權(quán)的方式很多,而jwt是其中比較簡單和可靠的一種方式,在rest框架中內(nèi)置了jwt鑒權(quán)功能,jwt的原理流程如下圖

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

rest框架中通過rest.WithJwt(secret)啟用jwt鑒權(quán),其中secret為服務(wù)器秘鑰是不能泄露的,因為需要使用secret來算簽名驗證payload是否被篡改,如果secret泄露客戶端就可以自行簽發(fā)token,黑客就能肆意篡改token了。我們基于上面的例子進行改造來驗證在rest中如何使用jwt鑒權(quán)

獲取jwt

第一步客戶端需要先獲取jwt,在登錄接口中實現(xiàn)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))
}

啟動服務(wù)后通過cURL訪問

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

會得到如下返回結(jié)果

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

添加Header

通過rest.WithJwt(accessSecret)啟用jwt鑒權(quán)

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

訪問/user/data接口返回 401 Unauthorized 鑒權(quán)不通過,添加Authorization Header,即能正常訪問

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

獲取信息

一般會將用戶的信息比如用戶id或者用戶名存入jwt的payload中,然后從jwt的payload中解析出我們預存的信息,即可知道本次請求時哪個用戶發(fā)起的

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

實現(xiàn)原理

jwt鑒權(quán)的實現(xiàn)在authhandler.go中,實現(xiàn)原理也比較簡單,先根據(jù)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框架中的中間件是實現(xiàn)業(yè)務(wù)和非業(yè)務(wù)功能解耦的一種方式,在web框架中我們可以通過中間件來實現(xiàn)諸如鑒權(quán)、限流、熔斷等等功能,中間件的原理流程如下圖

rest框架中內(nèi)置了非常豐富的中間件,在rest/handler路徑下,通過alice工具把所有中間件鏈接起來,當發(fā)起請求時會依次通過每一個中間件,當滿足所有條件后最終請求才會到達真正的業(yè)務(wù)Handler執(zhí)行業(yè)務(wù)邏輯,上面介紹的jwt鑒權(quán)就是通過authHandler來實現(xiàn)的。由于內(nèi)置中間件比較多篇幅有限不能一一介紹,感興趣的伙伴可以自行學習,這里我們介紹一下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)
        })
    }
}

在該中間件中,在請求開始時記錄了起始時間,在請求結(jié)束后在defer中通過prometheus的Histogram和Counter數(shù)據(jù)類型分別記錄了當前請求path的耗時和返回的code碼,此時我們通過訪問http://127.0.0.1:9101/metrics即可查看相關(guān)的指標信息

路由原理

rest框架中通過AddRoutes方法來注冊路由,每一個Route有Method、Path和Handler三個屬性,Handler類型為http.HandlerFunc,添加的路由會被換成featuredRoutes定義如下

featuredRoutes struct {
        priority  bool // 是否優(yōu)先級
        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)
}

調(diào)用Start方法啟動服務(wù)后會調(diào)用engine的Start方法,然后會調(diào)用StartWithRouter方法,該方法內(nèi)通過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
}

最終會調(diào)用patRouter的Handle方法進行綁定,patRouter實現(xiàn)了Router接口

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

patRouter中每一種請求方法都對應(yīng)一個樹形結(jié)構(gòu),每個樹節(jié)點有兩個屬性item為path對應(yīng)的handler,而children為帶路徑參數(shù)和不帶路徑參數(shù)對應(yīng)的樹節(jié)點, 定義如下:

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

Tree struct {
  root *node
}

通過Tree的Add方法把不同path與對應(yīng)的handler注冊到該樹上我們通過一個圖來展示下該樹的存儲結(jié)構(gòu),比如我們定義路由如下

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

路由存儲的樹形結(jié)構(gòu)如下圖

<img src="https://oscimg.oschina.net/oscnet/up-fc2d96766688c6ce4f652588786cd46021d.png" style="zoom:40%;">

當請求來的時候會調(diào)用patRouter的ServeHTTP方法,在該方法中通過tree.Search方法找到對應(yīng)的handler進行執(zhí)行,否則會執(zhí)行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 { // 在樹中搜索對應(yīng)的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)
    }
}

總結(jié)

本文從整體上介紹了rest,通過該篇文章能夠基本了解rest的設(shè)計和主要功能,其中中間件部分是重點,里面集成了各種服務(wù)治理相關(guān)的功能,并且是自動集成的不需要我們做任何配置,其他功能比如參數(shù)自動效驗等功能由于篇幅有限在這里就不做介紹了,感興趣的朋友可以自行查看官方文檔進行學習。go-zero中不光有http協(xié)議還提供了rpc協(xié)議和各種提高性能和開發(fā)效率的工具,是一款值得我們深入學習和研究的框架。

項目地址

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

如果覺得文章不錯,歡迎 github 點個 star ??

項目地址:
https://github.com/tal-tech/go-zero

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容