您好,登錄后才能下訂單哦!
本篇內(nèi)容主要講解“如何使用GVP web框架”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“如何使用GVP web框架”吧!
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端口,并在當(dāng)前目錄下創(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ù)
鑒權(quán)幾乎是每個應(yīng)用必備的能力,鑒權(quán)的方式很多,而jwt是其中比較簡單和可靠的一種方式,在rest框架中內(nèi)置了jwt鑒權(quán)功能,jwt的原理流程如下圖
<img src="https://oscimg.oschina.net/oscnet/6d98ffb00454488690e7fb5419167e1c~tplv-k3u1fbpfcp-zoom-1.jpg" alt="jwt" >
rest框架中通過rest.WithJwt(secret)啟用jwt鑒權(quán),其中secret為服務(wù)器秘鑰是不能泄露的,因為需要使用secret來算簽名驗證payload是否被篡改,如果secret泄露客戶端就可以自行簽發(fā)token,黑客就能肆意篡改token了。我們基于上面的例子進(jìn)行改造來驗證在rest中如何使用jwt鑒權(quán)
第一步客戶端需要先獲取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, &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" }
通過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中解析出我們預(yù)存的信息,即可知道本次請求時哪個用戶發(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(), &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鑒權(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工具把所有中間件鏈接起來,當(dāng)發(fā)起請求時會依次通過每一個中間件,當(dāng)滿足所有條件后最終請求才會到達(dá)真正的業(yè)務(wù)Handler執(zhí)行業(yè)務(wù)邏輯,上面介紹的jwt鑒權(quán)就是通過authHandler來實現(xiàn)的。由于內(nèi)置中間件比較多篇幅有限不能一一介紹,感興趣的伙伴可以自行學(xué)習(xí),這里我們介紹一下prometheus指標(biāo)收集的中間件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) }) } }
在該中間件中,在請求開始時記錄了起始時間,在請求結(jié)束后在defer中通過prometheus的Histogram和Counter數(shù)據(jù)類型分別記錄了當(dāng)前請求path的耗時和返回的code碼,此時我們通過訪問http://127.0.0.1:9101/metrics即可查看相關(guān)的指標(biāo)信息
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方法進(jìn)行綁定,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é)點(diǎn)有兩個屬性item為path對應(yīng)的handler,而children為帶路徑參數(shù)和不帶路徑參數(shù)對應(yīng)的樹節(jié)點(diǎn), 定義如下:
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/32b3488353174baa8769414b817d8e7e~tplv-k3u1fbpfcp-zoom-1.jpg" >
當(dāng)請求來的時候會調(diào)用patRouter的ServeHTTP方法,在該方法中通過tree.Search方法找到對應(yīng)的handler進(jìn)行執(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) > 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) } }
到此,相信大家對“如何使用GVP web框架”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。