如何使用中间件以惯用方式记录身份验证信息

Posted

技术标签:

【中文标题】如何使用中间件以惯用方式记录身份验证信息【英文标题】:How to idiomatically log auth information with middleware 【发布时间】:2017-10-10 10:30:48 【问题描述】:

假设我正在构建一个 Go 网络应用,具有以下要求:

身份验证中间件可能会发出 HTTP 响应(如果出现错误) 日志中间件应记录正常的请求信息(请求 URL、响应状态、响应大小等)以及身份验证信息(即经过身份验证的用户名) context.Context 的惯用用法

乍一看,这似乎很容易:

r.Use(authMiddleware)
r.Use(loggingMiddleware)
// Other middlewares/routes

但如果 authMiddleware 发出 400、401、403 或类似错误,则此操作会失败,因为永远不会调用日志记录中间件。

所以重新排序似乎是合适的:

r.Use(loggingMiddleware)
r.Use(authMiddleware)
// Other middlewares/routes

但是现在,假设我使用context.WithValue() 设置身份验证信息,记录器不知道身份验证信息。它仍然可以记录 HTTP 响应代码、大小等,但不能记录经过身份验证的用户等。

这导致了一个非常复杂的解决方案:

r.Use(injectAuthPlaceholder)
r.Use(loggingMiddleware)
r.Use(authMiddleware)

injectAuthPlaceholder 在哪里做类似的事情:

var user *string
ctx = context.WithValue(ctx, userKey, user)

然后authMiddleware 将用户设置为:

userPtr = ctx.Value(userKey).(*string)
*userPtr = authedUsername

这具有让记录器访问经过身份验证的用户名的效果,但它需要一个两部分的身份验证机制,这似乎违反了context.Context 的惯用用法,而且感觉很恶心。

对于这个先有鸡还是先有蛋的问题,有什么更惯用的解决方案?

【问题讨论】:

为什么 r.Use(loggingMiddleware) 在错误情况下从不调用?看起来它将作为顺序编写的代码。 r.Use() 到底是什么意思,你没有解释你的中间件模型? 因为提供了响应的中间件会返回,而不是调用next.ServeHTTP(w,r)。因此,一旦中间件响应,链条就会中断。 (否则,您最终会导致后续中间件/处理程序尝试发送重复响应,这将无法正常工作,并且会警告在被劫持的连接上写入等) 我认为无论如何都应该调用链中的所有中间件/处理程序,但行为应该取决于 request.Context.Err() 或其他一些取消机制。 这是一种有趣的方法......我认为我没有能力要求所有中间件都以这种方式运行,但也许我控制的那些就足够了。 是什么让你首先想要拆分这两个中间件?在我看来,它们都与相同的逻辑(身份验证)有关。 【参考方案1】:

我正在记录请求,其中包括身份验证信息以及其他相关信息(请求的 URL、响应代码、响应大小等)

根据我对您在这两个中间件中的逻辑的理解,我觉得日志中间件混合了关注点。我认为HTTP请求信息日志应该与身份验证日志分开。

正如我所说,日志记录是关于记录在您的应用中发生的与您相关的事件。此外,日志需要上下文相关,因此需要对他们记录的内容有深入的了解。

配置身份验证涉及提供用户数据库、加密方法、机密等。配置日志记录涉及提供日志记录格式和日志记录目标(例如文件或远程服务器)。这些之间有 0 个重叠。它们的关联方式与 HTTP 请求与日志记录的关联方式相同。 如果合并日志记录和身份验证中间件是有意义的,那么合并日志记录和请求处理是有意义的。还有其他一切。

是的,中间件有点像 go 包。就像标准的 go 包一样,我不会基于它们的抽象层来拆分它们,而是基于它们解决的领域的一部分。因此,如果我有一个包auth,我会在其中添加所有身份验证逻辑,从域模型到传输层(例如 http)。我强烈推荐你这个来自Marcus Olsson 的excellent talk,关于 DDD 应用于 Go。

中间件示例

func mwLogging(next MiddlewareFunc) MiddlewareFunc 
    return func(c *Context) 
        c.Ctx.Trace("h.http.req.start", "Request start",
            log.String("method", c.Method),
            log.String("path", c.Path),
            log.String("user_agent", c.Req.Header.Get("User-Agent")),
        )

        next(c)

        c.Ctx.Trace("h.http.req.end", "Request end",
            log.Int("status", c.Res.Code()),
            log.Duration("duration", time.Since(c.StartTime)),
        )
    


func mwAuth(next MiddlewareFunc) MiddlewareFunc 
    return func(c *http.Context) 
        // Just an example
        session, err := Auth(c.Req.Header.Get("Authorization"))
        if err != nil 
            c.Ctx.Warning("http.auth.error", "Authentication failed", log.Error(err))
            c.Res.WriteHeader(http.StatusUnauthorized)
            return
        

        // You could store the session in context here                
        context.WithValue(ctx, "session", session)
        next(c)
    

无耻插件:logging middleware 取自 stairlin/lego。

【讨论】:

以上是关于如何使用中间件以惯用方式记录身份验证信息的主要内容,如果未能解决你的问题,请参考以下文章

您如何将 JMeter 与摘要式身份验证一起使用?

如何以编程方式检查某个 URL 是不是需要使用 Spring Security 进行身份验证?

如何使用 DaoAuthenticationProvider 以编程方式使用 Spring Security 对用户进行身份验证

如何使用 OWIN Jwt Bearer Authentication 记录身份验证结果

获取经过身份验证的用户 Laravel/Lumen Passport

Refresh-Token (JWT) 如何避免中间人攻击?