项目地址:github
什么是middleware
在web服务中,通常我们都有实现拦截器的需求,或用于日志的记录,或用于事务的开启,或是权限的鉴定等。
在Go Web中,这种拦截器常常被称为middleware——中间件。
在原生的Go http服务中,middleware通常使用装饰模式实现,定义常常如下。
type WrapperHander func(ResponseWriter, *Request) func(ResponseWriter, *Request)
我们通过这样一层一层的装饰,来实现顺序的调用。
在Go中,常用的middleware多用于记录请求日志,recovery等。同样的,在graphql中,也提供了类似的实现。
全局middleware
顾名思义,全局的middleware,将会在每一次请求中都被调用。参考gin框架对此的实现,graphql也以类似的方式实现了middleware。
其实早在上一节main方法中,我们就已经使用了graphql提供的两个middleware。
graphql.Use(middleware.Logger(), middleware.Recovery())
使用graphql.Use函数进行注册。这里我们将自己实现这些middleware。
Logger
新建middleware目录,在目录下新建middleware.go文件。
package middleware
import (
"github.com/shyptr/graphql"
"github.com/shyptr/jianshu/util"
"time"
)
func Logger() graphql.HandlerFunc {
return func(c *graphql.Context) {
logger := util.GetLogger()
c.Set("logger", logger)
start := time.Now()
defer func() {
reqMethod := c.Request.Method
statusCode := c.Writer.Status()
clientIP := c.ClientIP()
operationName := c.Value("operationName")
if operationName == "" {
operationName = "query"
}
logger.Info().Int("status", statusCode).Str("method", reqMethod).TimeDiff("latencyTime", start, time.Now()).
Str("ip", clientIP).Interface("operationName", operationName).Send()
util.PutLogger(logger)
}()
c.Next()
}
}
graphql的Context提供了设置key-value的方法。这样做有一个好处,就是下游函数设置的key-value,在上游函数中也可以读到。
在这里我们将初始化的logger放入上下文中,以供后续函数的调用。同时我们在defer中,最后将使用完毕的logger,放回pool池内,实现复用。
Recovery
recovery是对全局panic的捕捉,一切经由它执行的后续方法,一旦发生panic都会被它捕捉。虽然graphql中,对于每个字段的解析函数,都进行了recover,但是为了以防万一在其他地方出现panic,我们仍然需要在入口处进行recover。
向middleware中添加内容。
func Recovery() graphql.HandlerFunc {
return func(c *graphql.Context) {
logger := c.Value("logger").(zerolog.Logger)
defer func() {
if r := recover(); r != nil {
logger.Error().Caller(4).Interface("[Recovery] panic received", r).Send()
c.ServerError("服务错误", http.StatusInternalServerError)
}
}()
c.Next()
}
}
这里只做了一个简单的实现,将panic recover起来,打印panic信息,并返回客户端内部错误的状态码。
Tx
在入口处开启一个贯穿整个调用的事务,并对事务的提交和回滚进行自动处理,听起来似乎并不是一个好的选择。因为有些时候,我们的解析函数并不需要事务。
但是鉴于graphql中解析函数的分散,我们可能在一次调用中,对多个字段进行了请求,而这些字段又都对数据库进行了修改,为了保证事务的一致,在简书项目中,还是决定对事务进行全局的管理。
向middleware添加内容。
func Tx(ld *sqlog.DB) graphql.HandlerFunc {
db := ld.Runner.(*sqlx.DB)
return func(c *graphql.Context) {
logger := c.Value("logger").(zerolog.Logger)
tx, err := db.Beginx()
if err != nil {
logger.Error().AnErr("transition begin failed", err).Send()
c.ServerError("服务错误", http.StatusInternalServerError)
return
}
c.Set("tx", &sqlog.DB{Runner: tx, Logger: ld.Logger})
defer func() {
if c.Err() != nil {
tx.Rollback()
return
}
if err := tx.Commit(); err != nil {
logger.Error().AnErr("transition commit failed", err).Send()
tx.Rollback()
}
}()
c.Next()
}
}
<br/>
最后,我们修改main函数,将三个middleware注册到graphql中。
graphql.Use(middleware.Logger(), middleware.Tx(model.DB), middleware.Recovery())
字段上的middleware
我们知道,在流行的Go Web框架,如Gin,Echo中,都是基于restful标准的。路由即资源。这些框架都提供了,对单个路由进行处理的handler。
在Gin中,一个路由接收多个handler,并以顺序执行,以此实现对单个路由的middleware。
其中比较常用的是BasicAuth这样用于权限校验的handler。
但是GraphQL本身只是一套api标准,并不具备这样的能力。GraphQL官方提供的示例中,我们需要在解析函数内部,从参数或者上下文中,获取验证所需的条件,并进行判断。
这样无疑会产生很多重复的代码逻辑。
var postType = new GraphQLObjectType({
name: ‘Post’,
fields: {
body: {
type: GraphQLString,
resolve: (post, args, context, { rootValue }) => {
// return the post body only if the user is the post's author
if (context.user && (context.user.id === post.authorId)) {
return post.body;
}
return null;
}
}
}
});
当然,万能的使用者总会有办法的。在GraphQL标准中,实现者应该提供用户自定义Directive的能力。这些自定义的Directive可以被用于类型的定义,或者具体的请求上。
我们通过使用自定义的Directive,也能达到handler的效果。
query($id:String!) {
User @auth(id:$id) {
name
}
}
譬如上述例子,我们对User字段使用了自定义的Directive auth,在auth中进行权限的校验。
但遗憾的是,graphql对自定义directive的支持并不好,目前仅限于在Field类型上使用自定义的Directive,而且存在诸多限制。
所以参考于各web框架对路由的定义,graphql假定每个被定义的Object的字段,都同效于路由,那么自然也可以使用类似的机制。
于是graphql提供了另类的只能用在FieldFunc中的HandlerFunc。
type ExecuteFunc func(ctx context.Context, args, source interface{}) error
ExecuteFunc支持在解析函数执行之前,对原始的map类型的args和source进行处理,并最终返回一个错误,用于终止后续解析函数的执行。
BasicAuth
现在,我们来编写基础的权限校验。
首先我们要明确,我们对于用户的session要如何处理,这关系到我们怎么进行权限的验证。
简书项目中,我们将会把session信息存储在客户端的cookie中。为了简单实现,session信息只包括用户的id。但是我们并不是要把id直接存储到cookie中去。
而是要经过加密。这里我们使用jwt进行加密认证。
在util包下,新建jwt.go文件。
package util
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/shyptr/jianshu/setting"
"strconv"
"time"
)
func GeneraToken(id uint64, age int) (string, error) {
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
ExpiresAt: int64(time.Hour) * int64(age),
Id: fmt.Sprintf("%d", id),
})
return claims.SignedString(setting.GetJwtSecret())
}
func ParseToken(token string) (uint64, error) {
claims, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(*jwt.Token) (interface{}, error) {
return []byte(setting.GetJwtSecret()), nil
})
if err != nil {
return 0, err
}
standardClaims := claims.Claims.(*jwt.StandardClaims)
expiresAt := standardClaims.VerifyExpiresAt(time.Now().Unix(), true)
if !expiresAt {
return 0, errors.New("token失效")
}
return strconv.ParseUint(standardClaims.Id, 10, 0)
}
向middleware中添加内容。
func BasicAuth() schemabuilder.ExecuteFunc {
return func(ctx context.Context, args, source interface{}) error {
c := ctx.(*graphql.Context)
logger := c.Value("logger").(zerolog.Logger)
cookie, _ := c.Request.Cookie("me")
if cookie == nil {
return nil
}
token := cookie.Value
if token != "" {
id, err := util.ParseToken(token)
if err != nil {
logger.Error().AnErr("basicAuth parse token", err).Send()
return errors.New("解析token失败")
}
c.Set("userId", id)
}
return nil
}
}
func LoginNeed() schemabuilder.ExecuteFunc {
return func(ctx context.Context, args, source interface{}) error {
id := ctx.Value("userId")
if id == nil {
c := ctx.(*graphql.Context)
c.ServerError("必须先登录", http.StatusUnauthorized)
return errors.New("你必须先登录")
}
return nil
}
}
func NotLogin() schemabuilder.ExecuteFunc {
return func(ctx context.Context, args, source interface{}) error {
id := ctx.Value("userId")
if id != nil {
c := ctx.(*graphql.Context)
c.ServerError("必须先退出登录", http.StatusMethodNotAllowed)
return errors.New("你必须先退出登录")
}
return nil
}
}
作者个人博客地址:https://unrotten.org
作者微信公众号:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。