parent
2739a26481
commit
0c58cd9c5a
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 31 KiB |
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,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 来简化逻辑。 |
@ -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() |
||||
} |
Loading…
Reference in new issue