Skip to content

qjw/kelly

Repository files navigation

运行Sample

为了避免干扰正式(测试)环境,建议先重置GOPATH环境变量

# 重置GOPATH
king@king:~/tmp$ export GOPATH=/home/king/tmp/gopath
king@king:~/tmp$ echo $GOPATH
/home/king/tmp/gopath

# 安装
king@king:~/tmp$ go get github.com/qjw/kelly/sample

# 查看依赖
king@king:~/tmp/gopath/src$ find . -maxdepth 3 -type d | sed "/\.git/d"
.
./github.com
./github.com/dchest
./github.com/dchest/uniuri
./github.com/urfave
./github.com/urfave/negroni
./github.com/julienschmidt
./github.com/julienschmidt/httprouter
./github.com/go-playground
./github.com/go-playground/locales
./github.com/go-playground/universal-translator
./github.com/gorilla
./github.com/gorilla/securecookie
./github.com/qjw
./github.com/qjw/kelly
./gopkg.in
./gopkg.in/go-playground
./gopkg.in/go-playground/validator.v9
./gopkg.in/redis.v5
./gopkg.in/redis.v5/testdata
./gopkg.in/redis.v5/internal

king@king:~/tmp/gopath/src$ cd ../bin/

# 运行sample
king@king:~/tmp/gopath/bin$ ./sample
[negroni] listening on :9090

背景

作为web后端开发,标准的net/http非常高效灵活,足以适用非常多的场景,当然也有很多周边待补充,这就出现了各种web框架,甚至出现了替代默认的Http库的valyala/fasthttp

golang目前百花齐放,个人主要了解到的是两个项目

  1. beego: simple & powerful Go app framework
  2. gin-gonic/gin

beego没有实际用过,听说是大而全的项目,对开发者友好。不过由于了解甚少,草率评论并不合适,这里不作过多说明。

本着刨根问底的学习态度,最开始了解的是martini,后查证效率偏低(大量用到反射/reflect),所以就进一步学习了gin-gonic/gin

后者小巧灵活,学习成本低,并且提供了很多实用的补充,例如

  1. 路由和中间件核心框架,路由基于julienschmidt/httprouter
  2. gin.Context
  3. binding
  4. 校验,基于go-playground/validator.v9
  5. Http Request工具函数,获取param/path/form/header/cookie等
  6. Http Response工具函数,设置cookie,header,返回xml/json,返回template支持等
  7. 内建的几个常用中间件

martini/gin都包含非常多的中间件,两者迁移非常容易,参考

  1. https://github.com/codegangsta/martini-contrib
  2. https://github.com/gin-gonic/contrib
  3. https://github.com/gin-contrib

用久了,也发现gin也有一些问题

  1. 依赖还是偏多(虽然和很多库相比算较少的),就写个hello world都下载半天依赖
  2. 第三方middleware有的依赖gopkg.in的代码,另外一些依赖github.com的代码
  3. gin.Context对Golang标准库context不友好
  4. binding有一些问题,本人的优化版本在https://github.com/qjw/go-gin-binding
  5. 虽然middleware很多,但选择性太多,质量参差不齐,不好选择,另外太多的第三方依赖不如将大部分常用的集成到一起来的方便。

经过多方对比考察,认为urfave/negroni作为路由/中间件基础框架非常合适,(看看原型就知道他对context有多友好)所以折腾就开始了。

type Handler interface {
  ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)
}

经过综合评估,决定自己弄个类似于gin的框架

原则是尽量踏着巨人的肩膀,避免一些通用组件重复造轮子,聚焦于优秀智慧的集成

本着尊重原作者的原则和对开源协议的尊重,我会尽量备注作者和出处,若有遗漏,请知会我[email protected]

目前的主要工作包括

  1. 基于urfave/negroni的核心框架
  2. 基于julienschmidt/httprouter的路由,并作优化以支持多级路由+路由middleware
  3. binding基于https://github.com/qjw/go-gin-binding,后者代码来源于https://github.com/gin-gonic/gin/tree/master/binding
  4. kelly.Context
  5. 基于go-playground/validator.v9的校验,参考gin的代码
  6. 常用的Request/Response工具函数,参考gin和其他一些框架,特别是https://github.com/gin-gonic/gin/tree/master/render
  7. 复用urfave/negroni的recovery/log
  8. 支持http 404/405的统一全局处理
  9. 内建静态文件支持
  10. 内建常用的middleware,含认证授权

Sample

package main
import (
    "github.com/qjw/kelly"
    "net/http"
)

func main(){
    router := kelly.New()

    router.GET("/", func(c *kelly.Context) {
        c.WriteIndentedJson(http.StatusOK, kelly.H{
            "code":    "0",
        })
    })

    router.Run(":9090")
}
king@king:~/tmp/gopath/src/sample$ go run main.go
[negroni] listening on :9090

参数

PATH变量

// 根据key获取PATH变量值
GetPathVarible(string) (string, error)
// 根据key获取PATH变量值,若不存在,则panic
MustGetPathVarible(string) string

Query变量

// 根据key获取QUERY变量值,可能包含多个(http://127.0.0.1:9090/path/abc?abc=bbb&abc=aaa)
GetMultiQueryVarible(string) ([]string, error)
// 根据key获取QUERY变量值,仅返回第一个
GetQueryVarible(string) (string, error)
// 根据key获取QUERY变量值,仅返回第一个,若不存在,则返回默认值
GetDefaultQueryVarible(string, string) string
// 根据key获取QUERY变量值,仅返回第一个,若不存在,则panic
MustGetQueryVarible(string) string
r.GET("/path/:name", func(c *kelly.Context) {
    c.WriteIndentedJson(http.StatusOK, kelly.H{
        "code":  "/path",
        "path":  c.MustGetPathVarible("name"), // 获取path参数
        "query": c.GetDefaultQueryVarible("abc", "def"), // 获取query参数
    })
})

Form变量

// 根据key获取FORM变量值,可能get可能包含多个
GetMultiFormVarible(string) ([]string, error)
// 根据key获取FORM变量值,仅返回第一个
GetFormVarible(string) (string, error)
// 根据key获取FORM变量值,仅返回第一个,若不存在,则返回默认值
GetDefaultFormVarible(string, string) string
// 根据key获取FORM变量值,仅返回第一个,若不存在,则panic
MustGetFormVarible(string) string
r.GET("/form", func(c *kelly.Context) {
    data := `<form action="/form" method="post">
<p>First name: <input type="text" name="fname" /></p>
<p>Last name: <input type="text" name="lname" /></p>
<input type="submit" value="Submit" />
</form>`
    c.WriteHtml(http.StatusOK, data) // 返回html
})

r.POST("/form", func(c *kelly.Context) {
    c.WriteIndentedJson(http.StatusOK, kelly.H{ // 返回格式化的json
        "code":        "/form",
        "first name":  c.GetDefaultFormVarible("fname", "fname"), // 获取form参数
        "second name": c.GetDefaultFormVarible("lname", "lname"),
    })
})

获取Header

// 根据key获取header值
GetHeader(string) (string, error)
// 根据key获取header值,若不存在,则返回默认值
GetDefaultHeader(string, string) string
// 根据key获取header值,若不存在,则panic
MustGetHeader(string) string
// Content-Type
ContentType() string

获取Cookie

// 根据key获取cookie值
GetCookie(string) (string, error)
// 根据key获取cookie值,若不存在,则返回默认值
GetDefaultCookie(string, string) string
// 根据key获取cookie值,若不存在,则panic
MustGetCookie(string) string

文件上传

// @ref http.Request.ParseMultipartForm
ParseMultipartForm() error
// 获取(上传的)文件信息
GetFileVarible(string) (multipart.File, *multipart.FileHeader, error)
MustGetFileVarible(string) (multipart.File, *multipart.FileHeader)
r.GET("/upload", func(c *kelly.Context) {
    data := `<form enctype="multipart/form-data" action="/upload" method="post">
<input type="file" name="file1" />
<input type="file" name="file2" />
<input type="submit" value="upload" />
</form>`
    c.WriteHtml(http.StatusOK, data) // 返回html
})

r.POST("/upload", func(c *kelly.Context) {
    c.ParseMultipartForm()

    file, handler := c.MustGetFileVarible("file1")
    defer file.Close()
    f, err := os.OpenFile("./"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer f.Close()
    io.Copy(f, file)

    file2, handler2 := c.MustGetFileVarible("file2")
    defer file2.Close()
    f2, err := os.OpenFile("./"+handler2.Filename, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer f2.Close()
    io.Copy(f2, file2)

    c.WriteIndentedJson(http.StatusOK, kelly.H{ // 返回格式化的json
        "code":        "/upload",
        "first name":  handler.Filename, // 获取form参数
        "second name": handler2.Filename,
    })
})

静态文件

package main
import (
    "github.com/qjw/kelly"
    "net/http"
)

func main(){
    router := kelly.New()

    router.GET("/static/*path", kelly.Static(&kelly.StaticConfig{
        Dir:        http.Dir("/var/www/html"),
        Indexfiles: []string{"index.html"},
    }))

    router.GET("/static1/*path", kelly.Static(&kelly.StaticConfig{
        Dir:           http.Dir("/tmp"),
        EnableListDir: true,
    }))

    router.Run(":9090")
}

运行之后,可以访问http://127.0.0.1:9090/statichttp://127.0.0.1:9090/static1

type StaticConfig struct {
    Dir           http.FileSystem
    // 是否支持枚举目录
    EnableListDir bool
    // 访问目录时,是否自动查找index
    Indexfiles    []string
}

参考

  1. https://github.com/urfave/negroni/blob/master/static.go
  2. https://github.com/labstack/echo/blob/master/middleware/static.go

打包资源

打包依赖于go-bindata-assetfs

假如前端资源放在frontend目录

# target/bindata.go 表示生成的文件位置
# router指定文件的package
go-bindata-assetfs -o target/bindata.go -pkg router ./frontend/...

然后可以

r.GET("/frontend/*path", func() func(*kelly.Context) {
	fs := &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: ""}
	h := http.FileServer(fs)
	return func(c *kelly.Context) {
		h.ServeHTTP(c, c.Request())
	}
}())

由于挂载的是一个子Path,若使用下面的简洁方式,会404

http.Handle("/", http.FileServer(assetFS()))

输出

// 返回紧凑的json
WriteJson(int, interface{})
// 返回xml
WriteXml(int, interface{})
// 返回html
WriteHtml(int, string)
// 返回模板html
WriteTemplateHtml(int, *template.Template, interface{})
// 返回格式化的json
WriteIndentedJson(int, interface{})
// 返回文本
WriteString(int, string, ...interface{})
// 返回二进制数据
WriteData(int, string, []byte)
render.GET("/t", func() kelly.HandlerFunc {
    data := `<form action="#" method="get">
<p>First {{ .First }}: <input type="text" name="fname" /></p>
<p>Last {{ .Last }}: <input type="text" name="lname" /></p>
<input type="submit" value="Submit" />
</form>`

    // 通过闭包预先编译好
    t := template.Must(template.New("t1").Parse(data))
    return func(c *kelly.Context) {
        c.WriteTemplateHtml(http.StatusOK, t, map[string]string{
            "First": "Qiu",
            "Last": "King",
        })
    }
}())

render.GET("/a", func(c *kelly.Context) {
    c.WriteString(http.StatusOK, "test %d %d", 123, 456) // 返回普通文本
})

重定向

// 返回重定向
Redirect(int, string)
c.Redirect(http.StatusFound, "/api/v1/flask_res")

设置Header

// 设置header
SetHeader(string, string)
func Version(ver string) kelly.HandlerFunc {
    return func(c *kelly.Context) {
        c.SetHeader("X-ACCOUNT-VERSION", ver)
        c.InvokeNext()
    }
}

设置Cookie

// 设置cookie
SetCookie(string, string, int, string, string, bool, bool)

Context数据

由于Context在一个中间件链执行,为了方便传递数据,支持Context读写数据。比如auth中间件就会保存current_user变量

func Middleware(ver string) kelly.HandlerFunc {
    return func(c *kelly.Context) {
        // 设置context参数
        c.Set("v1", ver)

        // 调用下一个handle
        c.InvokeNext()
    }
}

router.GET("/", func(c *kelly.Context) {
    c.WriteIndentedJson(http.StatusOK, kelly.H{
        "code":    "/",
        "value":   c.MustGet("v1"),  // 获取context数据
    })
})
Set(interface{}, interface{}) dataContext
Get(interface{}) interface{}
MustGet(interface{}) interface{}

重建Body

某些情况下,Middleware的一些操作依赖body,而body一旦读完就不会再有,这时可以通过Context将Body内容传递给后续handle。

或者纯http库/c.SetBody来重建body,这种方案的好处是,后续handle对此透明,无需特殊处理

bodyBytes, _ := ioutil.ReadAll(req.Body)
c.SetBody(bodyBytes)
bodyBytes, _ := ioutil.ReadAll(req.Body)
req.Body.Close()
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

中间件 middleware

一个最简单的中间件实现

type HandlerFunc func(c *Context)
func Version(ver string) kelly.HandlerFunc {
    return func(c *kelly.Context) {
        c.SetHeader("X-ACCOUNT-VERSION", ver)
        c.InvokeNext()
    }
}

默认会中断中间件链的执行,若希望继续执行,需要手动调用【c.InvokeNext()

func (c *Context) InvokeNext() {
    if c.next != nil {
        c.next.ServeHTTP(c, c.Request())
    }
}

全局中间件

全局中间件会被 所有 的请求执行

router := kelly.New(
    middleware.Version("v1"),
)

动态添加中间件

router.Use(
    middleware.Version("v1"),
    Middleware("v1", "v1", true),
)

多级路由

子路由也支持注入中间件,只影响它自己的请求,以及他的子路由

router := kelly.New(
    middleware.Version("v1"),
)

// 新建一个子router,并注入一个middleware
ar := r.Group(
    "/aaa",
    Middleware("v2", "v2", true),
)
ar.GET("/", func(c *kelly.Context) {
    c.WriteJson(http.StatusOK, kelly.H{ // 返回json(紧凑格式)
        "code": "/aaa",
    })
})

// 新建一个子router,并注入一个middleware
sar := ar.Group(
    "/bbb",
    Middleware("v3", "v3", true),
)
sar.GET("/", func(c *kelly.Context) {
    c.WriteXml(http.StatusOK, kelly.H{  // 返回XML
        "code": "/aaa/bbb",
    })
})

空路由

空路由指在创建子路由(Group)时,使用路径"/"的路由,返回的新路由和父路由使用相同的路径

空路由的好处是可以针对同一个url的不同请求,使用不同的中间件

假如/api/v1一部分不需要登录,剩下的则需要。正常情况下,需要对需要登录的请求每个都加入中间件进行认证,而空路由则可以只注册一次中间件,需要登录的请求都基于这个空路由来注册。

api.GET("/login",
    func(c *kelly.Context) {
        // 登录授权
        sessions.Login(c, &User{
            Id:   1,
            Name: c.GetDefaultQueryVarible("name", "p1"),
        })
        c.Redirect(http.StatusFound, "/api/v1/")
    })

api2 := api.Group("/",sessions.LoginRequired())
api2.GET("/logout",
    func(c *kelly.Context) {
        // 注销登录
        sessions.Logout(c)
        c.WriteJson(http.StatusFound, "/logout")
    })

单个API中间件注入

下面的代码,通过一个中间件作登录认证

api.GET("/",
    sessions.LoginRequired(),
    func(c *kelly.Context) {
        // 获取登录用户
        user := sessions.LoggedUser(c).(*User)
        c.WriteJson(http.StatusOK, kelly.H{
            "message": user.Name,
        })
    })

其他Http方法

GET(string, ...HandlerFunc)
HEAD(string, ...HandlerFunc)
OPTIONS(string, ...HandlerFunc)
POST(string, ...HandlerFunc)
PUT(string, ...HandlerFunc)
PATCH(string, ...HandlerFunc)
DELETE(string, ...HandlerFunc)
r.POST("/ok", func(c *kelly.Context) {
    c.WriteIndentedJson(http.StatusOK, kelly.H{ // 返回格式化的json
        "code": "/csrf ok",
    })
})

处理404/405

router.SetNotFoundHandle(func(c *kelly.Context) {
    c.WriteString(http.StatusNotFound, http.StatusText(http.StatusNotFound))
})
router.SetMethodNotAllowedHandle(func(c *kelly.Context) {
    c.WriteString(http.StatusMethodNotAllowed, http.StatusText(http.StatusMethodNotAllowed))
})
// 设置404处理句柄
SetNotFoundHandle(HandlerFunc)
// 设置405处理句柄
SetMethodNotAllowedHandle(HandlerFunc)

重置Request

例如使用context就需要替换默认的Request对象

func (c *Context) SetRequest(r *http.Request){

重建Body

bodyBytes, _ := ioutil.ReadAll(req.Body)
c.SetBody(bodyBytes)

数据绑定和校验

手动绑定

会同时绑定和校验

type BindPathObj struct {
    A string `json:"aaa" binding:"required,max=32,min=6" error:"aerror"`
    B string `json:"bbb" binding:"required,max=32,min=6" error:"berror"`
    C string `json:"ccc" binding:"required,max=32,min=6" error:"cerror"`
}

api.GET("/path/:aaa/:bbb/:ccc", func(c *kelly.Context) {
    var obj BindPathObj
    if err, _ := c.BindPath(&obj); err == nil {
        c.WriteJson(http.StatusOK, obj)
    } else {
        c.WriteString(http.StatusOK, "param err")
    }
})

type BindJsonObj struct {
    Obj1 BindPathObj `json:"obj"`
    A    string      `json:"aaa" binding:"required,max=32,min=6" error:"aerror"`
    B    string      `json:"bbb" binding:"required,max=32,min=6" error:"berror"`
    C    string      `json:"ccc" binding:"required,max=32,min=6" error:"cerror"`
}

api.POST("/json", func(c *kelly.Context) {
    var obj BindJsonObj
    if err, _ := c.Bind(&obj); err == nil {
        c.WriteJson(http.StatusOK, obj)
    } else {
        c.WriteString(http.StatusOK, "param err")
    }
})

所有的绑定接口

// 绑定一个对象,根据Content-type自动判断类型
Bind(interface{}) (error, []string)
// 绑定json,从body取数据
BindJson(interface{}) (error, []string)
// 绑定xml,从body取数据
BindXml(interface{}) (error, []string)
// 绑定form,从body/query取数据
BindForm(interface{}) (error, []string)
// 绑定path变量
BindPath(interface{}) (error, []string)

校验规则

参考https://godoc.org/gopkg.in/go-playground/validator.v9

自定义错误输出

参考struct tag中的error

type BindJsonObj struct {
    Obj1 BindPathObj `json:"obj"`
    A    string      `json:"aaa" binding:"required" error:"aerror"`
    B    string      `json:"bbb" binding:"required" error:"berror"`
    C    string      `json:"ccc" binding:"required" error:"cerror"`
}

自动绑定

自动绑定可以减少非常多拖沓的重复代码

注意,可以在bind时设置缺省参数

api.GET("/path2/:aaa/:bbb/:ccc",
    kelly.BindPathMiddleware(func() interface{} { return &BindPathObj{
        AAA: "testa",
        BBB: "testb",
    }}),
    func(c *kelly.Context) {
        c.WriteJson(http.StatusOK, c.GetBindPathParameter())
    })
api.POST("/form2",
    kelly.BindMiddleware(func() interface{} { return &BindPathObj{}}),
    func(c *kelly.Context) {
        c.WriteJson(http.StatusOK, c.GetBindParameter())
    })
api.POST("/json2",
    kelly.BindMiddleware(func() interface{} { return &BindJsonObj{}}),
    func(c *kelly.Context) {
        c.WriteJson(http.StatusOK, c.GetBindParameter())
    })
GetBindParameter() interface{}
GetBindJsonParameter() interface{}
GetBindXmlParameter() interface{}
GetBindFormParameter() interface{}
GetBindPathParameter() interface{}

自动绑定实现

本质上就是将原来每个接口都需要写的重复逻辑抽象到中间件,并且通过Context传递

func BindMiddleware(objG func()interface{}) HandlerFunc {
	return func(c *Context) {
		obj := objG()
		err, msgs := c.Bind(obj)
		if err == nil {
			c.Set(contextBindKey, obj)
			c.InvokeNext()
		} else {
			handleValidateErr(c, err, msgs, obj)
		}
	}
}

单独校验

校验框架可以自动从http请求获取参数并校验,当然也可以单独对已经存在的struct进行校验

type Configuration struct {
    Port int    `json:"port" binding:"max=65536"`
    Host string `json:"host" binding:"ip4_addr"`
}

func F(){
    if err := kelly.Validate(conf); err != nil {
        panic(err)
    }
}

Session

Flash

flash用于在多个后端接口传递数据

flask基于cookie的session,不依赖于redis等文件系统/数据库

sessions.InitFlash([]byte("abcdefghijklmn"))

api.GET("/flash", func(c *kelly.Context) {
    sessions.AddFlash(c, "hello world")
    c.Redirect(http.StatusFound, "/api/v1/flash_res")
})

api.GET("/flash_res", func(c *kelly.Context) {
    msgs := sessions.Flashes(c)
    if len(msgs) > 0 {
        c.WriteJson(http.StatusOK, kelly.H{
            "message": msgs[0].(string),
        })
    } else {
        c.WriteJson(http.StatusOK, kelly.H{
            "message": "",
        })
    }
})

Session

对于简单的应用,可以使用基于cookie的session,即全部(加密)内容都通过cookie传输,其他的建议使用基于服务器的session,即正文存储在后端的存储/文件系统中,只将key通过cookie传输。

一些其他的方案,参考json web token

// gopkg.in/redis.v5
// 初始化redis,返回一个store对象
func initStore() sessions.Store {
    redisClient := redis.NewClient(&redis.Options{
        Addr:     "127.0.0.1:6379",
        Password: "",
        DB:       3,
    })
    if err := redisClient.Ping().Err(); err != nil {
        log.Fatal("failed to connect redis")
    }

    store, err := sessions.NewRediStore(redisClient, []byte("abcdefg"))
    if err != nil {
        log.Print(err)
    }
    return store
}

store := initStore()

// 注入session的中间件,用于将session实例存入Context
api := r.Group("/api/v1",
    sessions.SessionMiddleware(store, sessions.AUTH_SESSION_NAME),
)

func(c *kelly.Context) {
    // 从Context获得session的实例
    session := c.MustGet(AUTH_SESSION_NAME).(Session)
    // 从session读取内容
    value := session.Get(AUTH_SESSION_KEY)
}
type Session interface {
    // Get returns the session value associated to the given key.
    Get(key interface{}) interface{}
    // Set sets the session value associated to the given key.
    Set(key interface{}, val interface{})
    // Delete removes the session value associated to the given key.
    Delete(key interface{})
    // Clear deletes all values in the session.
    Clear()
    // Options sets confuguration for a session.
    // Options(Options)
    // Save saves all sessions used during the current request.
    Save() error
}

依赖

  1. https://github.com/gorilla/sessions
  2. https://github.com/martini-contrib/sessions

最终修改的项目见https://github.com/qjw/sessions

认证授权

授权依赖sessions

// 在注入session的中间件之后,注入
store := initStore()
api := r.Group("/api/v1",
    sessions.SessionMiddleware(store, sessions.AUTH_SESSION_NAME),
    sessions.AuthMiddleware(&sessions.AuthOptions{
        User: &User{},
    }),
)

// 通过一个中间件作登录权限认证
api.GET("/",
    sessions.LoginRequired(),
    func(c *kelly.Context) {
        // 获取登录用户
        user := sessions.LoggedUser(c).(*User)
        c.WriteJson(http.StatusOK, kelly.H{
            "message": user.Name,
        })
    })

// 登录
api.GET("/login",
    func(c *kelly.Context) {
        // 是否已经登录
        if sessions.IsAuthenticated(c) {
            c.Redirect(http.StatusFound, "/api/v1/")
            return
        }

        // 登录授权
        sessions.Login(c, &User{
            Id:   1,
            Name: c.GetDefaultQueryVarible("name", "p1"),
        })

        // 重定向到首页
        c.Redirect(http.StatusFound, "/api/v1/")
    })

// 登出
api.GET("/logout",
    sessions.LoginRequired(),
    func(c *kelly.Context) {
        // 注销登录
        sessions.Logout(c)
        c.WriteJson(http.StatusFound, "/logout")
    })

权限

初始化,需要提供几个数据

  1. 所有的权限id/名称
  2. 一个通过user查询所有权限的函数

接下来使用中间件sessions.PermissionRequired实现自动权限判断

权限判断会自动检查,是否已登录

sessions.InitPermission(&sessions.PermissionOptions{
    UserPermissionGetter: func(user interface{}) (map[int]bool, error) {
        ruser := user.(*User)
        if ruser.Name == "p1" {
            return map[int]bool{
                1: true,
            }, nil
        } else if ruser.Name == "p2" {
            return map[int]bool{
                1: true,
                2: true,
            }, nil
        } else {
            return map[int]bool{}, nil
        }
    },
    AllPermisionsGetter: func() (map[string]int, error) {
        return map[string]int{
            "perm1": 1,
            "perm2": 2,
            "perm3": 3,
        }, nil
    },
})

api.GET("/p1",
    sessions.PermissionRequired("perm1"),
    func(c *kelly.Context) {
        // 获取登录用户
        user := sessions.LoggedUser(c).(*User)
        c.WriteJson(http.StatusOK, kelly.H{
            "perm": "p1",
            "user": user.Name,
        })
    })
api.GET("/p2",
    sessions.PermissionRequired("perm2"),
    func(c *kelly.Context) {
        // 获取登录用户
        user := sessions.LoggedUser(c).(*User)
        c.WriteJson(http.StatusOK, kelly.H{
            "perm": "p2",
            "user": user.Name,
        })
    })
api.GET("/p3",
    sessions.PermissionRequired("perm3"),
    func(c *kelly.Context) {
        // 获取登录用户
        user := sessions.LoggedUser(c).(*User)
        c.WriteJson(http.StatusOK, kelly.H{
            "perm": "p3",
            "user": user.Name,
        })
    })

内建中间件

  1. Version
  2. NoCache
  3. BasicAuth,来自https://github.com/martini-contrib/auth
  4. Throttle:请求频率限制,来自https://github.com/martini-contrib/throttle
  5. Cors,来自https://github.com/gin-contrib/cors
  6. Secure,来自https://github.com/gin-contrib/secure
  7. Csrf,来自https://github.com/tommy351/gin-csrf
  8. Gzip,来自https://github.com/gin-contrib/gzip,支持gzip/deflate

Csrf

// 初始化
middleware.InitCsrf(middleware.CsrfConfig{
    Secret: []byte("fasdffasdfas"),
})

// 注入Middleware
api := r.Group("/csrf",
    middleware.Csrf(),
)

api.GET("/ok", func() kelly.HandlerFunc {
    // token放在一个hidden表单中自动带入
    data := `<form action="/csrf//ok" method="post">
<p>First {{ .First }}: <input type="text" name="fname" /></p>
<p><input type="hidden" name="_csrf" value="{{ .Token }}"> </p>
<input type="submit" value="Submit" />
</form>`

    // 通过闭包预先编译好
    t := template.Must(template.New("ok").Parse(data))
    return func(c *kelly.Context) {
        // 在前一个请求中,返回token给前端
        c.WriteTemplateHtml(http.StatusOK, t, map[string]string{
            "First": "Qiu",
            "Token": middleware.GetCsrfToken(c),
        })
    }
}())

api.POST("/ok", func(c *kelly.Context) {
    c.WriteIndentedJson(http.StatusOK, kelly.H{ // 返回格式化的json
        "code": "/csrf ok",
    })
})

Cors

由于实现的原因,未绑定的Path,中间件无法监听,而cors依赖于options方法,所以为了支持,需要手动添加options绑定

router := r.Group("/swagger", middleware.Cors(&middleware.CorsConfig{
    AllowAllOrigins: true,
    AllowMethods:    []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"},
    AllowHeaders:    []string{"Origin", "Content-Length", "Content-Type"},
}))

// 绑定所有的options请求来支持中间件作跨域处理
router.OPTIONS("/*path", func(c *kelly.Context) {
    c.WriteString(http.StatusOK, "ok")
})

Annotation注解和Swagger

很多语言都支持注解,例如Java和Python

  1. https://docs.oracle.com/javase/tutorial/java/annotations/index.html
  2. http://www.infoq.com/cn/articles/cf-java-annotation
  3. http://www.cnblogs.com/Jerry-Chou/archive/2012/05/23/python-decorator-explain.html

Golang并不支持注解之类的语法,比较类似的是Struct Tag

kelly实现了一个【类似】于注解的方法,在每次注册请求时被触发,回调的原型如下

type AnnotationHandlerFunc func(c *AnnotationContext)

AnnotationContext如下

// endpint Context,用于记录每个请求的信息
type AnnotationContext struct {
    // endpoint所属的路由对象,从这里可以获取他的Path
    r           Router
    // endpoint的Http方法,例如PUT
    method      string
    // endpoint的路径,例如 /aaa
    path        string
    // router的中间件链条
    middlewares []HandlerFunc
    // endpoint 自己的中间件链条,最后一个就是最终的Http请求处理函数
    handles     []HandlerFunc
}

在程序真正监听端口提供服务之前,这条链条已经执行完毕,所以并不影响运行性能

有两种方法添加注解

// 添加全局的 注解 函数。该router下面和子(孙)router下面的endpoint注册都会被触发
GlobalAnnotation(handles ...AnnotationHandlerFunc) Router

// 添加临时 注解 函数,只对使用返回的AnnotationRouter对象进行注册的endpoint有效
Annotation(handles ...AnnotationHandlerFunc) AnnotationRouter

GlobalAnnotation注入的函数会一直存在于router对象,以及它的子router。(务必在注册请求或者添加子router之前)

Annotation只对返回的新router对象有效(需要链式调用),相当于对单个router临时有效

router.Annotation(func(c *kelly.AnnotationContext) {
    log.Printf("have register %s%s %s", c.R().Path(), c.Path(), c.Method())
}).GET("/", func(c *kelly.Context) {
    log.Print(c.GetDefaultCookie("session", "ss"))
    log.Print(c.MustGet("v1"))
    c.Redirect(http.StatusFound, "/doc")
})

打印所有的请求注册操作

根Router注入GlobalAnnotation即可

router := kelly.New()

// 增加全局的endpoint钩子
router.GlobalAnnotation(func(c *kelly.AnnotationContext) {
    handle := c.HandlerFunc()
    name := runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name()
    log.Printf("register [%7s|%2d|%2d]%s%s ---- %s",
        c.Method(), c.MiddlewareCnt(), c.HandleCnt(), c.R().Path(), c.Path(), name)
})

启动后的输出如下

2017/09/19 20:44:30 register [    GET| 4| 1]/path/:name ---- main.InitParam.func1
2017/09/19 20:44:30 register [    GET| 4| 1]/form ---- main.InitParam.func2
2017/09/19 20:44:30 register [   POST| 4| 1]/form ---- main.InitParam.func3
2017/09/19 20:44:30 register [    GET| 5| 1]/aaa/ ---- main.InitGroupMiddleware.func1

也可以在子router,或者终端注册请求时(例如router.GET)添加

router.Annotation(func(c *kelly.AnnotationContext) {
    log.Printf("have register %s%s %s", c.R().Path(), c.Path(), c.Method())
}).GET("/", func(c *kelly.Context) {
    log.Print(c.GetDefaultCookie("session", "ss"))
    log.Print(c.MustGet("v1"))
    c.Redirect(http.StatusFound, "/doc")
})

router.Annotation(func(c *kelly.AnnotationContext) {
    log.Printf("have register %s%s %s", c.R().Path(), c.Path(), c.Method())
}).GET("/health", func(c *kelly.Context) {
    c.WriteString(http.StatusOK, "ok")
})
2017/09/19 20:44:30 register [    GET| 4| 1]/ ---- main.main.func5
2017/09/19 20:44:30 have register / GET
2017/09/19 20:44:30 register [    GET| 4| 1]/health ---- main.main.func7
2017/09/19 20:44:30 have register /health GET

Swagger

由于每次请求注册都会执行注入的回调AnnotationHandlerFunc,所以对于一些针对请求的业务相当有用,比如生成doc文档的swagger

初始化

// swagger
swagger.InitializeApiRoutes(router,
    &swagger.Config{
        BasePath:         "/api/v1",
        Title:            "Swagger测试工具",
        Description:      "Swagger测试工具",
        DocVersion:       "0.1",
        // swagger ui 用于显示文档,为了支持其他域名,需要后端开启cors
        SwaggerUiUrl:     "http://swagger.qiujinwu.com",
        // 文档访问的path,例如127.0.0.1:9090/doc
        SwaggerUrlPrefix: "doc",
        Debug:            true,
    }, // 默认可以直接通过struct生成文档,若依赖yaml文件,需要这个接口来loadyaml文件的内容
    func(key string) ([]byte, error) {
        // 自行修改路径,key是文件名
        return ioutil.ReadFile("/home/king/code/go/src/github.com/qjw/kelly/sample/swagger.yaml")
    },
)

Annotation 中间件

基于yaml文件,留意swagger.SwaggerFile

router.Annotation(
    swagger.SwaggerFile("swagger.yaml:upload_material"),
).POST("/upload_material", func(c *kelly.Context) {
    c.WriteIndentedJson(http.StatusOK, kelly.H{
        "code": "0",
    })
})

基于struct对象,留意swagger.Swagger

router.Annotation(swagger.Swagger(&swagger.StructParam{
    ResponseData: &swagger.SuccessResp{},
    FormData:     &swaggerParam{},
    Summary:      "api1",
    Tags:         []string{"API接口"},
})).PATCH("/api1", func(c *kelly.Context) {
    c.WriteIndentedJson(http.StatusOK, kelly.H{
        "code": "0",
    })
})

router context

在中间件链中,可以通过kelly.Context来读写数据实现中间件之间的数据传输。

在Annotation链中,也有类似的接口,具体保存在kelly.Router中

在swagger中,常见的场景是同一个router下面的api具有相同的Tag,所以我们可以在router层面写入一个全局的tag,然后每个API读取即可

留意 swagger.SetGlobalParam

// 增加中间件处理跨域问题
router := r.Group("/swagger", middleware.Cors(&middleware.CorsConfig{
    AllowAllOrigins: true,
    AllowMethods:    []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"},
    AllowHeaders:    []string{"Origin", "Content-Length", "Content-Type"},
})).GlobalAnnotation(swagger.SetGlobalParam(&swagger.StructParam{
    Tags:         []string{"API接口"},
})).OPTIONS("/*path", func(c *kelly.Context) {
    c.WriteString(http.StatusOK, "ok")
})

router.Annotation(swagger.Swagger(&swagger.StructParam{
    ResponseData: &swagger.SuccessResp{},
    FormData:     &swaggerParam{},
    Summary:      "api1",
})).PATCH("/api1", func(c *kelly.Context) {
    c.WriteIndentedJson(http.StatusOK, kelly.H{
        "code": "0",
    })
})

验证码

// 生成验证码的ID,不是实际的验证码数字,参数是验证码的长度
func GenerateCaptchaID(len int) string
// request的url必须是 http(s)://host/path/{CaptchaID}.png
func ServerCaptcha(c *kelly.Context,width,height int)
// 验证验证码
func VerifyCaptcha(captchaID,captchaCode string) bool

新增依赖https://github.com/dchest/captcha

github.com/dchest/captcha内部使用了一个简化版的内建redis作为(类似于服务器session)验证码的容器,对外暴露一个id,这个ID用于

  1. 请求验证码图片
  2. 回传用于验证码校验

由于验证码并不是频繁调用,所以这种办法挺靠谱,分离id和实际的code最大的好处是,重新获取验证码,前端影响较小,因为通过内部容器做了一层映射,所以可以确保在验证码变化的情况下,验证码id保持一致。

一种简化的做法是去掉内建的映射容器,直接将加密过的验证码使用cookie或者其他方式传递和回传,保持无状态。

验证码使用

留意代码中的js,通过获取验证码图片时,补上【reload=1】参数即可实现更新,而无须修改html的所有验证码id

func InitCaptcha(r kelly.Router) {

    r.GET("/captcha", func() kelly.HandlerFunc {
        data := `<form action="/captcha" method="post">
<p>验证码: <input type="text" name="captchaStr" /></p>
<input type="hidden" name="captchaID" value="{{ .CaptchaID }}" /></p>
<img id="captcha-img" width="104" height="36" src="/captcha/image/{{ .CaptchaID }}.png" />
<input type="submit" value="提交" />
</form>
<script type="text/javascript" src="//cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script type="text/javascript">
$(function() {
    $("#captcha-img").click(function() {
      var captcha_url = $(this).attr("src").split("?")[0];
      captcha_url += "?reload=1&timestamp=" + new Date().getTime()
      $(this).attr("src",  captcha_url);
    });
})
</script>`

        // 通过闭包预先编译好
        t := template.Must(template.New("t1").Parse(data))
        return func(c *kelly.Context) {
            captchaID := toolkits.GenerateCaptchaID(4)
            c.WriteTemplateHtml(http.StatusOK, t, map[string]string{
                "CaptchaID": captchaID,
            })
        }
    }())

    r.GET("/captcha/image/:id", func(c *kelly.Context) {
        toolkits.ServerCaptcha(c,104,36)
    })

    r.POST("/captcha", func(c *kelly.Context) {
        if toolkits.VerifyCaptcha(
            c.MustGetFormVarible("captchaID"),
            c.MustGetFormVarible("captchaStr"),
        ){
            c.ResponseStatusOK()
        }else{
            c.ResponseStatusForbidden(nil)
        }
    })
}

模板

// 创建新的模板管理器
func NewTemplateManage(path string) TemplateManage

type TemplateManage interface {
    // 加载模板
    GetTemplate(string) (Template, error)
    MustGetTemplate(string) Template
}

type Template interface {
    // 渲染模板到kelly.Context
    Render(c *kelly.Context, context kelly.H) error
    MustRender(c *kelly.Context, context kelly.H)
}

新增依赖https://github.com/flosch/pongo2。语法参考后者官网

func InitTemplate(r kelly.Router) {
    mng := toolkits.NewTemplateManage(ProjectRoot)
    r.GET("/template", func() kelly.HandlerFunc {
        temp := mng.MustGetTemplate("template/index.html")
        return func(c *kelly.Context) {
            temp.Render(c, kelly.H{
                "Body": "Kelly",
            })
        }
    }())
}

中间件

手动获取模板灵活性很高,不过可以用中间件简化逻辑

步骤

  1. (全局)用TemplateManage接口初始化
  2. 增加TemplateMiddleware中间件
  3. 使用CurrentTemplate获取当前的template
toolkits.InitTemplateMiddleware(mng)
r.GET("/template2",
    toolkits.TemplateMiddleware("template/index.html"),
    func(c *kelly.Context) {
        toolkits.CurrentTemplate(c).Render(c, kelly.H{
            "Body": "Kelly",
        })
    })

Go内建模板

只需要将初始化函数从NewTemplateManage 替换成NewGoTemplateManage即可

二维码

新增依赖https://github.com/skip2/go-qrcode

func NewQRCode(content string, level int) (*Qrcode, error)
func (q *Qrcode) Image(size int) image.Image
func (q *Qrcode) Write(size int, out io.Writer) error
func (q *Qrcode) WriteFile(size int, filename string) error
func (q *Qrcode) WriteKelly(size int, c *kelly.Context) error
router.GET("/qrcode", func(c *kelly.Context) {
    qrcode,_ := toolkits.NewQRCode(c.MustGetQueryVarible("content"),toolkits.QrcodeMedium)
    qrcode.WriteKelly(400,c)
})

About

一个简单的golang后端框架

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published