diff --git a/doc/img/bm-arch-2-2.png b/doc/img/bm-arch-2-2.png new file mode 100644 index 000000000..ba2163f85 Binary files /dev/null and b/doc/img/bm-arch-2-2.png differ diff --git a/doc/img/bm-arch-2-3.png b/doc/img/bm-arch-2-3.png new file mode 100644 index 000000000..4cf0dfde3 Binary files /dev/null and b/doc/img/bm-arch-2-3.png differ diff --git a/doc/img/bm-handlers.png b/doc/img/bm-handlers.png new file mode 100644 index 000000000..5d9b5e8c8 Binary files /dev/null and b/doc/img/bm-handlers.png differ diff --git a/doc/wiki-cn/blademaster-mid.md b/doc/wiki-cn/blademaster-mid.md index e69de29bb..5356ffff0 100644 --- a/doc/wiki-cn/blademaster-mid.md +++ b/doc/wiki-cn/blademaster-mid.md @@ -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() +} +``` diff --git a/doc/wiki-cn/blademaster-mod.md b/doc/wiki-cn/blademaster-mod.md new file mode 100644 index 000000000..1bb68d68d --- /dev/null +++ b/doc/wiki-cn/blademaster-mod.md @@ -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() 方法之后。 diff --git a/doc/wiki-cn/blademaster-quickstart.md b/doc/wiki-cn/blademaster-quickstart.md new file mode 100644 index 000000000..633c236ce --- /dev/null +++ b/doc/wiki-cn/blademaster-quickstart.md @@ -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) diff --git a/doc/wiki-cn/blademaster.md b/doc/wiki-cn/blademaster.md index e69de29bb..1d2a54d57 100644 --- a/doc/wiki-cn/blademaster.md +++ b/doc/wiki-cn/blademaster.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 来简化逻辑。 diff --git a/doc/wiki-cn/summary.md b/doc/wiki-cn/summary.md index 88a935a65..d0f604d34 100644 --- a/doc/wiki-cn/summary.md +++ b/doc/wiki-cn/summary.md @@ -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) diff --git a/pkg/net/http/blademaster/middleware/auth/README.md b/pkg/net/http/blademaster/middleware/auth/README.md new file mode 100644 index 000000000..22f7554eb --- /dev/null +++ b/pkg/net/http/blademaster/middleware/auth/README.md @@ -0,0 +1,7 @@ +#### blademaster/middleware/auth + +##### 项目简介 + +blademaster 的 authorization middleware,主要用于设置路由的认证策略 + +注:仅仅是个demo,请根据自身业务实现具体鉴权逻辑 diff --git a/pkg/net/http/blademaster/middleware/auth/auth.go b/pkg/net/http/blademaster/middleware/auth/auth.go new file mode 100644 index 000000000..7c77c280e --- /dev/null +++ b/pkg/net/http/blademaster/middleware/auth/auth.go @@ -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 + } +} diff --git a/pkg/net/http/blademaster/middleware/auth/example_test.go b/pkg/net/http/blademaster/middleware/auth/example_test.go new file mode 100644 index 000000000..f1e20ea21 --- /dev/null +++ b/pkg/net/http/blademaster/middleware/auth/example_test.go @@ -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() +} diff --git a/tool/kratos/template.go b/tool/kratos/template.go index 2a250e303..95ef99c14 100644 --- a/tool/kratos/template.go +++ b/tool/kratos/template.go @@ -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 (