[gogf/gf]中间件的执行逻辑问题

2024-06-25 152 views
8

使用的v1.9.10的GF,现有如下代码:

package main

import (
    "fmt"
    "net/http"

    "github.com/gogf/gf/frame/g"
    "github.com/gogf/gf/net/ghttp"
)

func MiddlewareAuth1(r *ghttp.Request) {
    fmt.Println("middleware 1")
    token := r.Get("token")
    if token == "123456" {
        r.Middleware.Next()
    } else {
        r.Response.WriteStatus(http.StatusForbidden)
    }
}
func MiddlewareAuth2(r *ghttp.Request) {
    fmt.Println("middleware 2")
    token := r.Get("token")
    if token == "123456" {
        r.Middleware.Next()
    } else {
        r.Response.WriteStatus(http.StatusForbidden)
    }
}

func MiddlewareFree(r *ghttp.Request) {
    fmt.Println("middleware free")
    r.Response.CORSDefault()
    r.Middleware.Next()
}
func MiddlewareCORS(r *ghttp.Request) {
    fmt.Println("middleware CORS")
    r.Response.CORSDefault()
    r.Middleware.Next()
}

func main() {
    s := g.Server()
    s.Group("/", func(g *ghttp.RouterGroup) {
        g.MiddlewarePattern("/*", func(r *ghttp.Request) {
            if r.URL.Path == "/login" {
                r.Middleware.Next()
                return
            }
            MiddlewareAuth1(r)
            MiddlewareAuth2(r)
        })
    })
    s.Group("/", func(g *ghttp.RouterGroup) {
        g.ALL("/login", func(r *ghttp.Request) {
            r.Response.Write("login")
        })
        g.ALL("/dashboard", func(r *ghttp.Request) {
            r.Response.Write("dashboard")
        })
    })
    s.Group("/api.v2", func(g *ghttp.RouterGroup) {
        g.Middleware(MiddlewareFree, MiddlewareCORS)
        g.ALL("/user/list", func(r *ghttp.Request) {
            r.Response.Write("list")
        })
    })
    s.SetPort(8199)
    s.Run()
}

当我请求localhost:8199/api.v2/user/list的时候,的确应该返回403,但是问题是我收到了两个403,响应是这样的:ForbiddenForbidden。我的理解应该是:前一个中间件已经返回403了,不是200了,后面的中间件是不是不要再执行了?或者要其他什么手段控制它是否执行? 如果我把代码改成这样:

package main

import (
    "fmt"
    "net/http"

    "github.com/gogf/gf/frame/g"
    "github.com/gogf/gf/net/ghttp"
)

func MiddlewareAuth1(r *ghttp.Request) {
    fmt.Println("middleware 1")
    token := r.Get("token")
    if token == "123456" {
        r.Middleware.Next()
    } else {
        r.Response.WriteStatus(http.StatusForbidden)
    }
}
func MiddlewareAuth2(r *ghttp.Request) {
    fmt.Println("middleware 2")
    token := r.Get("token")
    if token == "123456" {
        r.Middleware.Next()
    } else {
        r.Response.WriteStatus(http.StatusForbidden)
    }
}

func MiddlewareFree(r *ghttp.Request) {
    fmt.Println("middleware free")
    r.Response.CORSDefault()
    r.Middleware.Next()
}
func MiddlewareCORS(r *ghttp.Request) {
    fmt.Println("middleware CORS")
    r.Response.CORSDefault()
    r.Middleware.Next()
}

func main() {
    s := g.Server()
    s.Group("/", func(g *ghttp.RouterGroup) {
        g.Middleware(MiddlewareAuth1, MiddlewareAuth2)
        g.MiddlewarePattern("/*", func(r *ghttp.Request) {
            if r.URL.Path == "/login" {
                r.Middleware.Next()
                return
            }
        })
    })
    s.Group("/", func(g *ghttp.RouterGroup) {
        g.ALL("/login", func(r *ghttp.Request) {
            r.Response.Write("login")
        })
        g.ALL("/dashboard", func(r *ghttp.Request) {
            r.Response.Write("dashboard")
        })
    })
    s.Group("/api.v2", func(g *ghttp.RouterGroup) {
        g.Middleware(MiddlewareFree, MiddlewareCORS)
        g.ALL("/user/list", func(r *ghttp.Request) {
            r.Response.Write("list")
        })
    })
    s.SetPort(8199)
    s.Run()
}

注意第44行,是我的改动,此时再请求localhost:8199/api.v2/user/list,只会返回1个403,一个Forbidden,然而,新的问题来了,不能正常访问login接口了。

总结一下:
  1. 如果使用MiddlewarePattern绑定中间件,前一个403了之后,后一个还会执行,是否bug?
  2. 如果使用RouterGroup绑定中间件,官方文档中的例外控制action就失效了,是否bug?

回答

4

再把MiddlewarePattern改一下,完全和官方文档中的一样,如下所示:

package main

import (
    "fmt"
    "net/http"

    "github.com/gogf/gf/frame/g"
    "github.com/gogf/gf/net/ghttp"
)

func MiddlewareAuth1(r *ghttp.Request) {
    fmt.Println("middleware 1")
    token := r.Get("token")
    if token == "123456" {
        r.Middleware.Next()
    } else {
        r.Response.WriteStatus(http.StatusForbidden)
    }
}
func MiddlewareAuth2(r *ghttp.Request) {
    fmt.Println("middleware 2")
    token := r.Get("token")
    if token == "123456" {
        r.Middleware.Next()
    } else {
        r.Response.WriteStatus(http.StatusForbidden)
    }
}

func MiddlewareFree(r *ghttp.Request) {
    fmt.Println("middleware free")
    r.Response.CORSDefault()
    r.Middleware.Next()
}
func MiddlewareCORS(r *ghttp.Request) {
    fmt.Println("middleware CORS")
    r.Response.CORSDefault()
    r.Middleware.Next()
}

func main() {
    s := g.Server()
    s.Group("/", func(g *ghttp.RouterGroup) {
        g.MiddlewarePattern("/*action", func(r *ghttp.Request) {
            if action := r.GetRouterString("action"); action != "" {
                switch action {
                case "login":
                    r.Middleware.Next()
                    return
                }
            }
            MiddlewareAuth1(r)
            MiddlewareAuth2(r)
        })
    })
    s.Group("/", func(g *ghttp.RouterGroup) {
        g.ALL("/login", func(r *ghttp.Request) {
            r.Response.Write("login")
        })
        g.ALL("/dashboard", func(r *ghttp.Request) {
            r.Response.Write("dashboard")
        })
    })
    s.Group("/api.v2", func(g *ghttp.RouterGroup) {
        g.Middleware(MiddlewareFree, MiddlewareCORS)
        g.ALL("/user/list", func(r *ghttp.Request) {
            r.Response.Write("list")
        })
    })
    s.SetPort(8199)
    s.Run()
}

login能访问,但是当访问localhost:8199/api.v2/user/list的时候,仍然会收到两个403,middleware1和middleware2都被执行了,而且都返回了403

9

我觉得还是应该把需求描述的更清楚一些:

在/路径上绑定中间件,进行权限验证(也就是全局所有请求都要走中间件),但是下列请求URL例外:

  • /adm/login
  • /api/login_captcha
  • /api/login_handler

同时,我有多个中间件,比如鉴权一个,CSRF一个,请求监控一个。这些中间件都全局生效,除了上述例外。

9

@ccpwcn 你的示例不错,好久没看到这么方便的issue提问了,使得我能快速帮助你处理问题。你这里遇到的问题并不是中间件的问题,中间件已经按照你设定的逻辑在运行。你的第一个示例中将两个中间件放到了一个闭包中执行,并且没有任何的退出机制,所以当然两个中间件的方法都会执行。我将你的示例做了一下小修改,加入了流程退出机制,你看看有什么差别:

小调整方案1:使用流程退出Exit方法
package main

import (
    "fmt"
    "net/http"

    "github.com/gogf/gf/frame/g"
    "github.com/gogf/gf/net/ghttp"
)

func MiddlewareAuth1(r *ghttp.Request) {
    fmt.Println("middleware 1")
    token := r.Get("token")
    if token == "123456" {
        r.Middleware.Next()
    } else {
        r.Response.WriteStatus(http.StatusForbidden)
        r.Exit()
    }
}
func MiddlewareAuth2(r *ghttp.Request) {
    fmt.Println("middleware 2")
    token := r.Get("token")
    if token == "123456" {
        r.Middleware.Next()
    } else {
        r.Response.WriteStatus(http.StatusForbidden)
        r.Exit()
    }
}

func MiddlewareFree(r *ghttp.Request) {
    fmt.Println("middleware free")
    r.Response.CORSDefault()
    r.Middleware.Next()
}
func MiddlewareCORS(r *ghttp.Request) {
    fmt.Println("middleware CORS")
    r.Response.CORSDefault()
    r.Middleware.Next()
}

func main() {
    s := g.Server()
    s.Group("/", func(group *ghttp.RouterGroup) {
        group.Middleware(func(r *ghttp.Request) {
            if r.URL.Path == "/login" {
                r.Middleware.Next()
                return
            }
            MiddlewareAuth1(r)
            MiddlewareAuth2(r)
        })
        group.ALL("/login", func(r *ghttp.Request) {
            r.Response.Write("login")
        })
        group.ALL("/dashboard", func(r *ghttp.Request) {
            r.Response.Write("dashboard")
        })
    })
    s.Group("/api.v2", func(group *ghttp.RouterGroup) {
        group.Middleware(MiddlewareFree, MiddlewareCORS)
        group.ALL("/user/list", func(r *ghttp.Request) {
            r.Response.Write("list")
        })
    })
    s.SetPort(8199)
    s.Run()
}

另外,在这两天会发布v1.10.0版本,可以将WriteStatus修改为WriteStatusExit也可以达到效果。

小调整方案2:调整权限校验方法流程逻辑
package main

import (
    "fmt"
    "net/http"

    "github.com/gogf/gf/frame/g"
    "github.com/gogf/gf/net/ghttp"
)

func MiddlewareAuthStep1(r *ghttp.Request) {
    if token := r.Get("token"); token == "123456" {
        r.Middleware.Next()
    } else {
        MiddlewareStep2(r)
    }
}
func MiddlewareStep2(r *ghttp.Request) {
    if token := r.Get("token"); token == "123456" {
        r.Middleware.Next()
    } else {
        r.Response.WriteStatus(http.StatusForbidden)
    }
}

func MiddlewareFree(r *ghttp.Request) {
    fmt.Println("middleware free")
    r.Response.CORSDefault()
    r.Middleware.Next()
}
func MiddlewareCORS(r *ghttp.Request) {
    fmt.Println("middleware CORS")
    r.Response.CORSDefault()
    r.Middleware.Next()
}

func main() {
    s := g.Server()
    s.Group("/", func(group *ghttp.RouterGroup) {
        group.Middleware(func(r *ghttp.Request) {
            if r.URL.Path == "/login" {
                r.Middleware.Next()
                return
            }
            MiddlewareAuthStep1(r)
        })
        group.ALL("/login", func(r *ghttp.Request) {
            r.Response.Write("login")
        })
        group.ALL("/dashboard", func(r *ghttp.Request) {
            r.Response.Write("dashboard")
        })
    })
    s.Group("/api.v2", func(group *ghttp.RouterGroup) {
        group.Middleware(MiddlewareFree, MiddlewareCORS)
        group.ALL("/user/list", func(r *ghttp.Request) {
            r.Response.Write("list")
        })
    })
    s.SetPort(8199)
    s.Run()
}
3

关于这个中间件逻辑,今天遇到一个新的问题:

我将中间件这么写:

// 中间件
s.Group("/", func(g *ghttp.RouterGroup) {
    g.MiddlewarePattern("/*action", func(r *ghttp.Request) {
        action := r.GetRouterString("action")
        glog.Line().Debugf("MiddlewarePattern administrator action: %s", action)
        switch action {
        case "adm/login.html", "api/adm/login_handler", "api/adm/login_captcha":
            r.Middleware.Next()
            return
        }
        api.MiddlewareAuth(r)
        api.MiddleWareCSRF(r)
        api.MiddlewareAntiHook(r)
    })
})

能满足需求以下需求:

  • 全站请求都要走中间件
  • 以下URL例外
    • /adm/login.html
    • /api/adm/login_handler
    • /api/adm/login_captcha

上述规则很好,工作的不错,既不会出现例外不生效的问题,也没有拦截错误,除了两个403之外。

然而,现在遇到了一个新的问题: 我在api.MiddleWareCSRF这个中间件中,调用了r.Request.Header.Set("csrf_token", serverCsrfToken),本意是向请求头里面加了一个参数,然后我会在Controller中取出来用,问题出来了,当我在Controller中使用r.Request.Header.Get("csrf_token")取的时候,我并没有取到值。为什么呢?原因是:进入Controller的,是由api.MiddlewareAuth进来的而不是api.MiddleWareCSRF。简而言之:我设置了两个中间件M1和M2,正常请求进入Controller.Action1的时候,这两个中间件都会生效,此时,我在M2里设置了HTTP Header,但是执行Controller.Action1的是M1并不是M2,于是,我放在HTTP Header中的数据,取不到。

所以,我想要探讨的是:

  1. 是我的中间件的设计机制有问题?
  2. 还是中间件本身的工作机制我理解的有问题?

附加说明一下我写的这三个中间件的作用:

  • api.MiddlewareAuth 身份验证,说白了就是是否登录的状态验证
  • api.MiddleWareCSRF CSRF安全验证
  • api.MiddlewareAntiHook 服务安全中间件,反劫持用的
2

面对我楼上写的3个中间件,系统会按我注册的顺序执行,但是现在有一个问题无解:

这个顺序:

api.MiddlewareAuth(r)
api.MiddleWareCSRF(r)
api.MiddlewareAntiHook(r)

身份验证通过了,数据已经入库了,却发现这个请求被劫持了,数据可能被污染了,所以没有起到应有的效果。于是,我改成这样:

api.MiddlewareAntiHook(r)
api.MiddleWareCSRF(r)
api.MiddlewareAuth(r)

预期结果是:先检查安全与否,请求安全有效,数据才入库,然而,事于愿违,在api.MiddleWareCSRF(r)中,发现不安全的时候,我执行了这样的代码:

if userCsrfToken == "" {
    glog.Line().Stack(false).Errorf("用户:%d,请求地址:%s,csrf token无效", uid, r.RequestURI)
    r.Response.WriteStatus(http.StatusForbidden, "您的请求不能受理")
    return
}

本意是本次请求作废,不再往下走了,但是悲剧发生了:数据竟然被入库了,也就是说MiddleWareCSRF向客户端发出403的状态之后之后,后续的中间件MiddlewareAuth竟然被执行了。

6

这么绑定中间件:

g.Middleware(api.MiddlewareAntiHook, api.MiddleWareCSRF, api.MiddlewareAuth)

可以做到前一个中止之后,后一个不会再执行,但是问题是:使用g.MiddlewarePattern写的例外规则就不能生效了。

唉。。。

5

终于理解了

  1. MiddlewarePattern上绑定的多个中间件,正常情况下前一个执行之后,无论状态如何,后一个都会继续执行,除非在中间件显式地调用了r.Exit()
  2. RouterGroup上绑定的多个中间件,前一个失败之后,后一个不会继续执行
  3. 如果一个中间件已经在RouterGroup上绑定了,它将不能与MiddlewarePattern中定义的规则进行匹配,我看了框架路由的源码,虽然底层都是把这些Handler绑定到了一个map上,但是它们的处理优先级是不同的。

总结一下: 同时在RouterGroupMiddlewarePattern上绑定多个中间件在有些时候不可避免,而框架会按你注册的顺序逐个执行RouterGroup上的中间件,然后再执行MiddlewarePattern上的。所以,你期望的中间件执行顺序是什么?里面的逻辑是什么样的?这些中间件之间是不是还有互动有关联,要不要把这些互动和关联拆出来,还是它们就应该保持联动,这些都是一具体的业务需求息息相关的。总之,在router中怎么注册,谁先谁后,出现错误之后往哪条路上去,要想清楚,否则可能出现意料之外的结果。

9

@ccpwcn 可能文档有些细节没有阐述的特别明确,因此我稍微更新了一下官方文档中关于中间件的说明:https://goframe.org/net/ghttp/router/middleware

GF的中间件功能非常灵活,参考了流行的PHP Laravel中间件设计方式,并且许多Go的其他Server库也提供了类似功能。MiddlewarePattern方法相比较于Middleware方法多了一个可以自定义路由规则的参数pattern,如果对路由规则掌握得好用起来就会更加灵活。我在生产环境中也只使用了Middleware方法注册多个中间件即满足需求。较复杂的项目也是根据业务模块分别注册中间件来单独控制,根据业务需求在分组路由中灵活组织多个中间件。当然会有在不同业务模块反复使用同一种或者几种中间件的情况,但极少有类似于/*的全局中间件控制,那样很不灵活也不太好管理。

你的问题集中在流程控制这块,流程控制的方式有很多,前提是需要对中间件的机制要掌握。中间件的要点主要是:洋葱式模型、优先级控制。流程控制的要点主要是:代码逻辑的组织、流程控制方法的辅助使用。

1

想来想去,觉得还是需要探讨一下有没有什么好的路由设计和中间件控制机制。现在我们假定系统的路由是这样的: / 也就是直接域名访问,自动重定向到/index/index.html /index/index.html 首页 /user/index.html 用户管理首页 /user/edit.html 用户编辑页面 /usr/del 用户删除AJAX接口 /order/index.html 订单首页 /order/edit.html 订单编辑页面 /adm/index.html 后台账号列表页面 /adm/edit.html 后台账号编辑页面 /adm/login.html 后台账号登录页面 /adm/login_handler 后台账号登录AJAX接口 /adm/login_captcha 后台账号登录图形验证码获取AJAX接口 /adm/del 后台账号删除AJAX接口 /upload/txt 文本文件上传AJAX接口 /upload/image 图片文件上传AJAX接口 /static/... 静态资源路径 ...... 系统中有类似于这样的请求和页面有很多,那么,我们常规情况下要做两件事情: 1、身份验证,我的理解是:所有请求都要验证,除了登录页面、登录接口、登录验证码接口 2、安全验证,部分接口进行,比如所有的edit页面上回填csrf_token,然后在表单提交的AJAX接口中验证csrf_token 像这种需求,这个中间件到底要怎么设计?路由到底要怎么设计?才算理解了并且用好了这个中间件洋葱模型呢?

3

@johngcn 不提新issue了,我觉得@ccpwcn把问题说的很清晰了; 我觉得问题所在就是gf把中间件绑定到所在路由组下的/*下了,导致路由规则和中间件绑定有些混乱。

1:这样做引起的第一个问题就是没有限制路由,我客户端可以构造很多无效的404路由进来,加上洋葱圈模型问题,这些404也都经过了前置中间件,没有意义。如果单纯要记录404日志的话,是否可以在框架统一错误处理进行,laravel是有可以定义一个全局的execption,go这块不太熟悉。

2:第二个问题就是路由鉴权排除,目前来说gf文档上的例子实现感觉繁琐。还是参考laravel的实现来看,如果gf要实现以下例子的话比较麻烦。

QQ图片20191203145952 我认证和不认证都是要在backapi下的,其次我认证和不认证url的第二个参数可能都是auth。

我觉得最好是把gf的group参数第一个string去掉,只保留一个回调函数。另外加个prefix方法来设定前缀。路由注册的时候结合prefix和group里面的路由规则对其进行合并,只保留详细的路由规则,并把中间件绑定到这些详细的路由规则上。 这样一方面去掉/*无效的路由,一方面可以很好的实现排除路由鉴权。

0

@mushu1990 感谢反馈,针对于你提出的问题我觉得确实可以改进一下。

0

@ccpwcn @mushu1990 请更新到最新的v1.10.0试试。