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

服务创建

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

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

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 := &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即可查看相关的指标信息

路由原理

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

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

当请求来的时候会调用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

如果觉得文章不错,欢迎 github 点个 star

go-zero之web框架的更多相关文章

  1. Python(九)Tornado web 框架

    一.简介 Tornado 是 FriendFeed 使用的可扩展的非阻塞式 web 服务器及其相关工具的开源版本.这个 Web 框架看起来有些像web.py 或者 Google 的 webapp,不过 ...

  2. 转-基于NodeJS的14款Web框架

    基于NodeJS的14款Web框架 2014-10-16 23:28 作者: NodeJSNet 来源: 本站 浏览: 1,399 次阅读 我要评论暂无评论 字号: 大 中 小 摘要: 在几年的时间里 ...

  3. 初步认识Node 之Web框架

    上一篇我们认识了Node是什么之后,这一篇我们主要了解的就是它的框架了.而它的框架又分为两大类,Web框架和全栈框架,接下来我们一个一个的来了解.    Web框架 Web框架可以细分为Web应用程序 ...

  4. 【原】Go语言及Web框架Beego环境无脑搭建

    本文涉及软件均以截至到2013年10月12日的最新版本为准 1. 相关软件准备: 1) go1.2rc1.windows-386.msi,对应32位windows系统安装使用 下载地址: https: ...

  5. 【译】什么是 web 框架?

    Web 应用框架,或者简单的说是“Web 框架”,其实是建立 web 应用的一种方式.从简单的博客系统到复杂的富 AJAX 应用,web 上每个页面都是通过写代码来生成的.我发现很多人都热衷于学习 w ...

  6. 冰冻三尺非一日之寒--web框架Django

    1.JS 正则    test   - 判断字符串是否符合规定的正则        rep = /\d+/;        rep.test("asdfoiklfasdf89asdfasdf ...

  7. web框架django初探

    Web框架介绍 一般会分为两部分:服务器程序和应用程序.服务器程序负责对socket服务器进行封装,并在请求到来时,对请求的各种数据进行整理.应用程序则负责具体的逻辑处理.为了方便应用程序的开发,就出 ...

  8. web前端基础知识-(六)web框架

    一.web框架本质 众所周知,对于所有的Web应用,本质上其实就是一个socket服务端,用户的浏览器其实就是一个socket客户端. #!/usr/bin/env python #coding:ut ...

  9. 第三篇 基于.net搭建热插拔式web框架(重造Controller)

    由于.net MVC 的controller 依赖于HttpContext,而我们在上一篇中的沙箱模式已经把一次http请求转换为反射调用,并且http上下文不支持跨域,所以我们要重造一个contro ...

  10. 第二篇 基于.net搭建热插拔式web框架(沙箱的构建)

    上周五写了一个实现原理篇,在评论中看到有朋友也遇到了我的问题,真的是有种他乡遇知己的感觉,整个系列我一定会坚持写完,并在最后把代码开源到git中.上一篇文章很多人看了以后,都表示不解,觉得不知道我到底 ...

随机推荐

  1. C++11——chrono库开发高精度计!我们可能学的不是同一门语言~

    一.前言 在我们写程序过程中,有时候需要测试我们的程序语句执行时间的耗时,当前也是有很多的库提供我们去使用,一直没有良好的跨平台的库可以提供出来:而且一般这种代码也是由我们程序员自己调用系统的库来进行 ...

  2. Python自动化准备工作(pycharm安装)

    一.安装Python 1.下载python-3.7.0-amd64.exe后双击 2.勾选Add Python3.7 to PATH可不用配置环境变量 3.点击下一步,可以按默认路径,也可以自己选择路 ...

  3. goland 注册

    注意:本教程已过期  请使用其他人教程激活最新版   https://www.789zhao.com/blog/JC08EIFBS9TM.html https://shimo.im/docs/dKYC ...

  4. Unity控制角色移动

    Copy代码前的准备工作 整体结构 建立个空对象命名随意,主要是为了:角色掉出地图时,可以回到空对象的位置 在建立个空对象命名为Player,放到PlayResurrectionPoints子级下,并 ...

  5. Spring Boot与多数据源那点事儿~

    持续原创输出,点击上方蓝字关注我 目录 前言 写这篇文章的目的 什么是多数据源? 何时用到多数据源? 整合单一的数据源 整合Mybatis 多数据源如何整合? 什么是动态数据源? 数据源切换如何保证线 ...

  6. poj1011 Sticks (搜索经典好题)

    poj1011 Sticks 题目连接: poj1011 Description George took sticks of the same length and cut them randomly ...

  7. Django( 学习第四部 Django的views视)

    目录 视图层 JsonResponse对象 form表单之文件上传 request方法及属性 FBV与CBV JsonResponse对象 前端序列化 JSON.stringify() json.du ...

  8. request-promise post请求微信小程序云函数调用http

    微信小程序不支持http调用,但是可以通过服务器或者云函数实现,云函数相当于云服务器中的一段代码,可以使用http协议 首先要云函数安装request-promise,右键云函数,在终端中打开,输入n ...

  9. 【转载】HPL与HPCG测试(一)

    来源:HPL与HPCG测试 (一) 一.HPL与HPCG 简介 1.HPL HPL 即 High Performance Linpack,它是针对现代并行计算集群的测试工具.用户不修改测试程序,通过调 ...

  10. LoRa技术的发展应用和LoRa应用设备

    LoRa技术的发展应用 LORA技术大约在十年前由法国和瑞士开发,到现今LORA技术已经是物联网发展应用中不可缺少的一部分,根据中国物联网研究与发展中心的数据,2025年我国物联网产业规模将达到2万亿 ...