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

Suppose I'm building a Go web app, with the following requirements:

  • Auth middleware that may issue an HTTP response (in case of error)
  • Logging middleware should log normal request information (request URL, response status, response size, etc), along with the authentication information (i.e. the authenticated user name)
  • Idiomatic uses of context.Context

At first blush, this seems easy:

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

But this fails if the authMiddleware issues a 400, 401, 403, or similar error, because then the logging middleware is never called.

So a re-order seems appropriate:

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

But now, assuming I set the auth information using context.WithValue(), the logger has no knowledge of the authentication information. It could still log the HTTP response code, size, etc, but not the authenticated user, etc.

This leads to what feels like a very convoluted solution:

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

Where injectAuthPlaceholder does something like:

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

Then authMiddleware sets the user with:

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

This has the effect of giving the logger access to the authenticated username, but it requires a two-part auth mechanism, it seems to violate idiomatic use of context.Context, and it just feels gross.

What is a more idiomatic solution to this chicken-and-egg problem?

I'm logging requests, which include authentication information, as well as other relevant information (requested URL, response code, response size, etc)

From what I understand in the logic you have put in these two middlewares, I have the feeling that the logging middleware mixes concerns. I think the HTTP request information logs should be separated from the authentication logs.

As I said, logging is about recording events that occur in your app that are relevant to you. Also, logs need context to be relevant and thus need to have an intimate knowledge about what they log.

Configuring auth involves providing a user database, crypt methods, secrets, etc. Configuring loging involves providing a logging format and logging destination (such as a file, or remote server). There is 0 overlap between these. They are related in the same way that the HTTP request is related to logging. If it makes sense to merge logging and auth middlewares, then it makes sense to merge logging and request handling. And everything else.

Yes, middlewares are a bit like go packages. Just like the standard go packages, I would not split them based on their abstraction layer, but rather based on the part of the domain they solve. So if I have a package auth, I would add the all the authentication logic in it, from the domain models, down to the transport layer (e.g. http). I highly recommend you this excellent talk from Marcus Olsson about DDD applied to Go.

Middleware examples

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)
    }
}

Shameless plug: the logging middleware is taken from stairlin/lego.