add doc with blademaster

pull/13/head
felixhao 6 years ago
parent 2739a26481
commit 0c58cd9c5a
  1. BIN
      doc/img/bm-arch-2-2.png
  2. BIN
      doc/img/bm-arch-2-3.png
  3. BIN
      doc/img/bm-handlers.png
  4. 104
      doc/wiki-cn/blademaster-mid.md
  5. 79
      doc/wiki-cn/blademaster-mod.md
  6. 64
      doc/wiki-cn/blademaster-quickstart.md
  7. 33
      doc/wiki-cn/blademaster.md
  8. 6
      doc/wiki-cn/summary.md
  9. 7
      pkg/net/http/blademaster/middleware/auth/README.md
  10. 153
      pkg/net/http/blademaster/middleware/auth/auth.go
  11. 40
      pkg/net/http/blademaster/middleware/auth/example_test.go
  12. 27
      tool/kratos/template.go

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@ -0,0 +1,104 @@
# 背景
基于bm的handler机制,可以自定义很多middleware(中间件)进行通用的业务处理,比如用户登录鉴权。接下来就以鉴权为例,说明middleware的写法和用法。
# 写自己的中间件
middleware本质上就是一个handler,如下代码:
```go
// Handler responds to an HTTP request.
type Handler interface {
ServeHTTP(c *Context)
}
// HandlerFunc http request handler function.
type HandlerFunc func(*Context)
// ServeHTTP calls f(ctx).
func (f HandlerFunc) ServeHTTP(c *Context) {
f(c)
}
```
1. 实现了`Handler`接口,可以作为engine的全局中间件使用:`engine.User(YourHandler)`
2. 声明为`HandlerFunc`方法,可以作为router的局部中间件使用:`e.GET("/path", YourHandlerFunc)`
简单示例代码如下:
```go
type Demo struct {
Key string
Value string
}
// ServeHTTP implements from Handler interface
func (d *Demo) ServeHTTP(ctx *bm.Context) {
ctx.Set(d.Key, d.Value)
}
e := bm.DefaultServer(nil)
d := &Demo{}
// Handler使用如下:
e.Use(d)
// HandlerFunc使用如下:
e.GET("/path", d.ServeHTTP)
// 或者只有方法
myHandler := func(ctx *bm.Context) {
// some code
}
e.GET("/path", myHandler)
```
# 全局中间件
在blademaster的`server.go`代码中,有以下代码:
```go
func DefaultServer(conf *ServerConfig) *Engine {
engine := NewServer(conf)
engine.Use(Recovery(), Trace(), Logger())
return engine
}
```
会默认创建一个bm engine,并注册`Recovery(), Trace(), Logger()`三个middlerware,用于全局handler处理。优先级从前到后。
如果需要自定义默认全局执行的middleware,可以使用`NewServer`方法创建一个无middleware的engine对象。
如果想要将自定义的middleware注册进全局,可以继续调用Use方法如下:
```go
engine.Use(YourMiddleware())
```
此方法会将`YourMiddleware`追加到已有的全局middleware后执行。
# 局部中间件
先来看一段示例(代码再pkg/net/http/blademaster/middleware/auth模块下):
```go
func Example() {
myHandler := func(ctx *bm.Context) {
mid := metadata.Int64(ctx, metadata.Mid)
ctx.JSON(fmt.Sprintf("%d", mid), nil)
}
authn := auth.New(&auth.Config{DisableCSRF: false})
e := bm.DefaultServer(nil)
// "/user"接口必须保证登录用户才能访问,那么我们加入"auth.User"来确保用户鉴权通过,才能进入myHandler进行业务逻辑处理
e.GET("/user", authn.User, myHandler)
// "/guest"接口访客用户就可以访问,但如果登录用户我们需要知道mid,那么我们加入"auth.Guest"来尝试鉴权获取mid,但肯定会继续执行myHandler进行业务逻辑处理
e.GET("/guest", authn.Guest, myHandler)
// "/owner"开头的所有接口,都需要进行登录鉴权才可以被访问,那可以创建一个group并加入"authn.User"
o := e.Group("/owner", authn.User)
o.GET("/info", myHandler) // 该group创建的router不需要再显示的加入"authn.User"
o.POST("/modify", myHandler) // 该group创建的router不需要再显示的加入"authn.User"
e.Start()
}
```

@ -0,0 +1,79 @@
# Context
以下是 blademaster 中 Context 对象结构体声明的代码片段:
```go
// Context is the most important part. It allows us to pass variables between
// middleware, manage the flow, validate the JSON of a request and render a
// JSON response for example.
type Context struct {
context.Context
Request *http.Request
Writer http.ResponseWriter
// flow control
index int8
handlers []HandlerFunc
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}
Error error
method string
engine *Engine
}
```
* 首先可以看到 blademaster 的 Context 结构体中会 embed 一个标准库中的 Context 实例,bm 中的 Context 也是直接通过该实例来实现标准库中的 Context 接口。
* Request 和 Writer 字段用于获取当前请求的与输出响应。
* index 和 handlers 用于 handler 的流程控制;handlers 中存储了当前请求需要执行的所有 handler,index 用于标记当前正在执行的 handler 的索引位。
* Keys 用于在 handler 之间传递一些额外的信息。
* Error 用于存储整个请求处理过程中的错误。
* method 用于检查当前请求的 Method 是否与预定义的相匹配。
* engine 字段指向当前 blademaster 的 Engine 实例。
以下为 Context 中所有的公开的方法:
```go
// 用于 Handler 的流程控制
func (c *Context) Abort()
func (c *Context) AbortWithStatus(code int)
func (c *Context) Bytes(code int, contentType string, data ...[]byte)
func (c *Context) IsAborted() bool
func (c *Context) Next()
// 用户获取或者传递请求的额外信息
func (c *Context) RemoteIP() (cip string)
func (c *Context) Set(key string, value interface{})
func (c *Context) Get(key string) (value interface{}, exists bool)
// 用于校验请求的 payload
func (c *Context) Bind(obj interface{}) error
func (c *Context) BindWith(obj interface{}, b binding.Binding) error
// 用于输出响应
func (c *Context) Render(code int, r render.Render)
func (c *Context) Redirect(code int, location string)
func (c *Context) Status(code int)
func (c *Context) String(code int, format string, values ...interface{})
func (c *Context) XML(data interface{}, err error)
func (c *Context) JSON(data interface{}, err error)
func (c *Context) JSONMap(data map[string]interface{}, err error)
func (c *Context) Protobuf(data proto.Message, err error)
```
所有方法基本上可以分为三类:
* 流程控制
* 额外信息传递
* 请求处理
* 响应处理
# Handler
![handler](/doc/img/bm-handlers.png)
初次接触 blademaster 的用户可能会对其 Handler 的流程处理产生不小的疑惑,实际上 bm 对 Handler 对处理非常简单。
将 Router 模块中预先注册的中间件与其他 Handler 合并,放入 Context 的 handlers 字段,并将 index 置 0,然后通过 Next() 方法一个个执行下去。
部分中间件可能想要在过程中中断整个流程,此时可以使用 Abort() 方法提前结束处理。
有些中间件还想在所有 Handler 执行完后再执行部分逻辑,此时可以在自身 Handler 中显式调用 Next() 方法,并将这些逻辑放在调用了 Next() 方法之后。

@ -0,0 +1,64 @@
# 准备工作
推荐使用[kratos tool](kratos-tool.md)快速生成项目,如我们生成一个叫`kratos-demo`的项目。
生成目录结构如下:
```
├── CHANGELOG.md
├── CONTRIBUTORS.md
├── LICENSE
├── README.md
├── cmd
   ├── cmd
   └── main.go
├── configs
   ├── application.toml
   ├── grpc.toml
   ├── http.toml
   ├── log.toml
   ├── memcache.toml
   ├── mysql.toml
   └── redis.toml
├── go.mod
├── go.sum
└── internal
├── dao
   └── dao.go
├── model
   └── model.go
├── server
   └── http
   └── http.go
└── service
└── service.go
```
# 路由
创建项目成功后,进入`internal/server/http`目录下,打开`http.go`文件,其中有默认生成的`blademaster`模板。其中:
```go
engine = bm.DefaultServer(hc.Server)
initRouter(engine)
if err := engine.Start(); err != nil {
panic(err)
}
```
是bm默认创建的`engine`及启动代码,我们看`initRouter`初始化路由方法,默认实现了:
```go
func initRouter(e *bm.Engine) {
e.Ping(ping) // engine自带的"/ping"接口,用于负载均衡检测服务健康状态
g := e.Group("/kratos-demo") // e.Group 创建一组 "/kratos-demo" 起始的路由组
{
g.GET("/start", howToStart) // g.GET 创建一个 "kratos-demo/start" 的路由,默认处理Handler为howToStart方法
}
}
```
bm的handler方法,结构如下:
```go
func howToStart(c *bm.Context) // handler方法默认传入bm的Context对象
```
# 扩展阅读
[bm模块说明](blademaster-mod.md) [bm中间件](blademaster-mid.md) [bm基于pb生成](blademaster-pb.md)

@ -0,0 +1,33 @@
# 背景
在像微服务这样的分布式架构中,经常会有一些需求需要你调用多个服务,但是还需要确保服务的安全性、同一化每次的请求日志或者追踪用户完整的行为等等。要实现这些功能,你可能需要在所有服务中都设置一些相同的属性,虽然这个可以通过一些明确的接入文档来描述或者准入规范来界定,但是这么做的话还是有可能会有一些问题:
1. 你很难让每一个服务都实现上述功能。因为对于开发者而言,他们应当注重的是实现功能。很多项目的开发者经常在一些日常开发中遗漏了这些关键点,经常有人会忘记去打日志或者去记录调用链。但是对于一些大流量的互联网服务而言,一个线上服务一旦发生故障时,即使故障时间很小,其影响面会非常大。一旦有人在关键路径上忘记路记录日志,那么故障的排除成本会非常高,那样会导致影响面进一步扩大。
2. 事实上实现之前叙述的这些功能的成本也非常高。比如说对于鉴权(Identify)这个功能,你要是去一个服务一个服务地去实现,那样的成本也是非常高的。如果说把这个确保认证的责任分担在每个开发者身上,那样其实也会增加大家遗忘或者忽略的概率。
为了解决这样的问题,你可能需要一个框架来帮助你实现这些功能。比如说帮你在一些关键路径的请求上配置必要的鉴权或超时策略。那样服务间的调用会被多层中间件所过滤并检查,确保整体服务的稳定性。
# 设计目标
* 性能优异,不应该惨杂太多业务逻辑的成分
* 方便开发使用,开发对接的成本应该尽可能地小
* 后续鉴权、认证等业务逻辑的模块应该可以通过业务模块的开发接入该框架内
* 默认配置已经是 production ready 的配置,减少开发与线上环境的差异性
# 概览
* 参考`gin`设计整套HTTP框架,去除`gin`中不需要的部分逻辑
* 内置一些必要的中间件,便于业务方可以直接上手使用
# blademaster架构
![bm-arch](/doc/img/bm-arch-2-2.png)
blademaster 由几个非常精简的内部模块组成。其中 Router 用于根据请求的路径分发请求,Context 包含了一个完整的请求信息,Handler 则负责处理传入的 Context,Handlers 为一个列表,一个串一个地执行。
所有的中间件均以 Handler 的形式存在,这样可以保证 blademaster 自身足够精简,且扩展性足够强。
![bm-arch](/doc/img/bm-arch-2-3.png)
blademaster 处理请求的模式非常简单,大部分的逻辑都被封装在了各种 Handler 中。一般而言,业务逻辑作为最后一个 Handler。正常情况下,每个 Handler 按照顺序一个一个串形地执行下去。
但是 Handler 中可以也中断整个处理流程,直接输出 Response。这种模式常被用于校验登陆的中间件中;一旦发现请求不合法,直接响应拒绝。
请求处理的流程中也可以使用 Render 来辅助渲染 Response,比如对于不同的请求需要响应不同的数据格式(JSON、XML),此时可以使用不同的 Render 来简化逻辑。

@ -4,8 +4,10 @@
* [快速开始](quickstart.md)
* [案例](https://github.com/bilibili/kratos-demo)
* [http blademaster](blademaster.md)
* [middleware](blademaster-mid.md)
* [protobuf生成](blademaster-pb.md)
* [bm quickstart](blademaster-quickstart.md)
* [bm module](blademaster-mod.md)
* [bm middleware](blademaster-mid.md)
* [bm protobuf](blademaster-pb.md)
* [grpc warden](warden.md)
* [middleware](warden-mid.md)
* [protobuf生成](warden-pb.md)

@ -0,0 +1,7 @@
#### blademaster/middleware/auth
##### 项目简介
blademaster 的 authorization middleware,主要用于设置路由的认证策略
注:仅仅是个demo,请根据自身业务实现具体鉴权逻辑

@ -0,0 +1,153 @@
package auth
import (
"github.com/bilibili/kratos/pkg/ecode"
bm "github.com/bilibili/kratos/pkg/net/http/blademaster"
"github.com/bilibili/kratos/pkg/net/metadata"
)
// Config is the identify config model.
type Config struct {
// csrf switch.
DisableCSRF bool
}
// Auth is the authorization middleware
type Auth struct {
conf *Config
}
// authFunc will return mid and error by given context
type authFunc func(*bm.Context) (int64, error)
var _defaultConf = &Config{
DisableCSRF: false,
}
// New is used to create an authorization middleware
func New(conf *Config) *Auth {
if conf == nil {
conf = _defaultConf
}
auth := &Auth{
conf: conf,
}
return auth
}
// User is used to mark path as access required.
// If `access_token` is exist in request form, it will using mobile access policy.
// Otherwise to web access policy.
func (a *Auth) User(ctx *bm.Context) {
req := ctx.Request
if req.Form.Get("access_token") == "" {
a.UserWeb(ctx)
return
}
a.UserMobile(ctx)
}
// UserWeb is used to mark path as web access required.
func (a *Auth) UserWeb(ctx *bm.Context) {
a.midAuth(ctx, a.authCookie)
}
// UserMobile is used to mark path as mobile access required.
func (a *Auth) UserMobile(ctx *bm.Context) {
a.midAuth(ctx, a.authToken)
}
// Guest is used to mark path as guest policy.
// If `access_token` is exist in request form, it will using mobile access policy.
// Otherwise to web access policy.
func (a *Auth) Guest(ctx *bm.Context) {
req := ctx.Request
if req.Form.Get("access_token") == "" {
a.GuestWeb(ctx)
return
}
a.GuestMobile(ctx)
}
// GuestWeb is used to mark path as web guest policy.
func (a *Auth) GuestWeb(ctx *bm.Context) {
a.guestAuth(ctx, a.authCookie)
}
// GuestMobile is used to mark path as mobile guest policy.
func (a *Auth) GuestMobile(ctx *bm.Context) {
a.guestAuth(ctx, a.authToken)
}
// authToken is used to authorize request by token
func (a *Auth) authToken(ctx *bm.Context) (int64, error) {
req := ctx.Request
key := req.Form.Get("access_token")
if key == "" {
return 0, ecode.Unauthorized
}
// NOTE: 请求登录鉴权服务接口,拿到对应的用户id
var mid int64
// TODO: get mid from some code
return mid, nil
}
// authCookie is used to authorize request by cookie
func (a *Auth) authCookie(ctx *bm.Context) (int64, error) {
req := ctx.Request
session, _ := req.Cookie("SESSION")
if session == nil {
return 0, ecode.Unauthorized
}
// NOTE: 请求登录鉴权服务接口,拿到对应的用户id
var mid int64
// TODO: get mid from some code
// check csrf
clientCsrf := req.FormValue("csrf")
if a.conf != nil && !a.conf.DisableCSRF && req.Method == "POST" {
// NOTE: 如果开启了CSRF认证,请从CSRF服务获取该用户关联的csrf
var csrf string // TODO: get csrf from some code
if clientCsrf != csrf {
return 0, ecode.Unauthorized
}
}
return mid, nil
}
func (a *Auth) midAuth(ctx *bm.Context, auth authFunc) {
mid, err := auth(ctx)
if err != nil {
ctx.JSON(nil, err)
ctx.Abort()
return
}
setMid(ctx, mid)
}
func (a *Auth) guestAuth(ctx *bm.Context, auth authFunc) {
mid, err := auth(ctx)
// no error happened and mid is valid
if err == nil && mid > 0 {
setMid(ctx, mid)
return
}
ec := ecode.Cause(err)
if ecode.Equal(ec, ecode.Unauthorized) {
ctx.JSON(nil, ec)
ctx.Abort()
return
}
}
// set mid into context
// NOTE: This method is not thread safe.
func setMid(ctx *bm.Context, mid int64) {
ctx.Set(metadata.Mid, mid)
if md, ok := metadata.FromContext(ctx); ok {
md[metadata.Mid] = mid
return
}
}

@ -0,0 +1,40 @@
package auth_test
import (
"fmt"
bm "github.com/bilibili/kratos/pkg/net/http/blademaster"
"github.com/bilibili/kratos/pkg/net/http/blademaster/middleware/auth"
"github.com/bilibili/kratos/pkg/net/metadata"
)
// This example create a identify middleware instance and attach to several path,
// it will validate request by specified policy and put extra information into context. e.g., `mid`.
// It provides additional handler functions to provide the identification for your business handler.
func Example() {
myHandler := func(ctx *bm.Context) {
mid := metadata.Int64(ctx, metadata.Mid)
ctx.JSON(fmt.Sprintf("%d", mid), nil)
}
authn := auth.New(&auth.Config{
DisableCSRF: false,
})
e := bm.DefaultServer(nil)
// mark `/user` path as User policy
e.GET("/user", authn.User, myHandler)
// mark `/mobile` path as UserMobile policy
e.GET("/mobile", authn.UserMobile, myHandler)
// mark `/web` path as UserWeb policy
e.GET("/web", authn.UserWeb, myHandler)
// mark `/guest` path as Guest policy
e.GET("/guest", authn.Guest, myHandler)
o := e.Group("/owner", authn.User)
o.GET("/info", myHandler)
o.POST("/modify", myHandler)
e.Start()
}

@ -120,6 +120,7 @@ func main() {
# Reviewer
`
_tplDao = `package dao
import (
@ -312,11 +313,12 @@ func (s *Service) Close() {
import (
"net/http"
"{{.Name}}/internal/model"
"{{.Name}}/internal/service"
"github.com/bilibili/kratos/pkg/conf/paladin"
"github.com/bilibili/kratos/pkg/log"
bm "github.com/bilibili/kratos/pkg/net/http/blademaster"
"github.com/bilibili/kratos/pkg/net/http/blademaster/middleware/verify"
)
var (
@ -337,19 +339,18 @@ func New(s *service.Service) (engine *bm.Engine) {
}
svc = s
engine = bm.DefaultServer(hc.Server)
initRouter(engine, verify.New(nil))
initRouter(engine)
if err := engine.Start(); err != nil {
panic(err)
}
return
}
func initRouter(e *bm.Engine, v *verify.Verify) {
func initRouter(e *bm.Engine) {
e.Ping(ping)
e.Register(register)
g := e.Group("/{{.Name}}")
{
g.GET("/start", v.Verify, howToStart)
g.GET("/start", howToStart)
}
}
@ -360,14 +361,14 @@ func ping(ctx *bm.Context) {
}
}
func register(c *bm.Context) {
c.JSON(map[string]interface{}{}, nil)
}
// example for http request handler.
func howToStart(c *bm.Context) {
c.String(0, "Golang 大法好 !!!")
k := &model.Kratos{
Hello: "Golang 大法好 !!!",
}
c.JSON(k, nil)
}
`
_tplAPIProto = `// 定义项目 API 的 proto 文件 可以同时描述 gRPC 和 HTTP API
// protobuf 文件参考:
@ -411,7 +412,11 @@ message HelloReq {
//go:generate TODO:待完善工具protoc.sh
`
_tplModel = `package model
`
// Kratos hello kratos.
type Kratos struct {
Hello string
}`
_tplGRPCServer = `package grpc
import (

Loading…
Cancel
Save