Writing Per-Handler Middleware

2019-05-06 21:23发布

问题:

I'm looking to pull some repetitive logic out of my handlers and put it into some per-handler middleware: specifically things like CSRF checks, checking for an existing session value (i.e. for auth, or for preview pages), etc.

I've read a few articles on this, but many examples focus on a per-server middleware (wrapping http.Handler): I have a smaller set of handlers that need the middleware. Most of my other pages do not, and therefore if I can avoid checking sessions/etc. for those requests the better.

My middleware, so far, typically looks something like this:

func checkCSRF(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // get the session, check/validate/create the token based on HTTP method, etc.
        // return HTTP 403 on a failed check
        // else invoke the wrapped handler h(w, r)
    }
}

However, in many cases I want to pass a variable to the wrapped handler: a generated CSRF token to pass to the template, or a struct that contains form data—one piece of middleware checks the session for the presence of some saved form data before the user hits a /preview/ URL, else it redirects them away (since they have nothing to preview!).

I'd like to pass that struct along to the wrapped handler to save having to duplicate the session.Get/type assertion/error checking logic I just wrote in the middleware.

I could write something like:

type CSRFHandlerFunc func(w http.ResponseWriter, r *http.Request, t string)

... and then write the middleware like so:

func csrfCheck(h CSRFHandlerFunc) http.HandlerFunc {
     return func(w http.ResponseWriter, r *http.Request) {
        // get the session, check/validate/create the/a token based on HTTP method, etc.
        // return HTTP 403 on a failed check
        // else invoke the wrapped handler and pass the token h(w, r, token)
    }

... but that raises a few questions:

  • Is this a sensible way to implement per-handler middleware and pass per-request variables?
  • Prior to testing this (don't have access to my dev machine!), if I need to wrap a handler with multiple pieces of middleware, I assume I can just r.HandleFunc("/path/preview/", checkCSRF(checkExisting(previewHandler)))? The issue I'm seeing here is that the middleware is now tightly coupled: the wrapped middleware now needs to receive and then pass on the variable from the outer middleware. This makes extending http.HandlerFunc trickier/more convoluted.
  • Would gorilla/context fit better here and allow me to avoid writing 2-3 custom handler types (or a generic handler type) — and if so, how would I make use of it? Or could I implement my own "context" map (and run into issues with concurrent access?).

Where possible I'm trying to avoid falling for the "don't get caught writing a library" trap, but middleware is something that I'm likely to add/build on later in the project's life, and I'd like to "get it right" the first time around.

Some guidance on this would be much appreciated. Go's been great so far for writing a web application, but there's not a ton of examples around at this stage in its life and I'm therefore leaning on SO a little.

回答1:

If I understood your question correctly, you're looking for a convenient way to pass additional parameters to your middleware, right?

Now, it's important to define what those parameters are. They could be some configuration values for your middleware – those can be set when the Handler type is being constructed). Instead of NewMyMiddleware(MyHandler), you do NewMyMiddleware(MyHandler, "parameter"), no problem here.

But in your case it seems like you want to pass per-request parameters, like a CSRF token. Passing those into the handler function would modify its signature and it would deviate from the standard Handler[Func] interface. You're right about middleware being more tightly coupled in this case.

You kind of mentioned the solution yourself – a context map is, in my opinion, a viable tool for this. It's not that hard to write one yourself – you basically need a map[*http.Request]interface{} and an RWMutex for safe concurrent access. Still, simply using gorilla/context should suffice – it seems like a (relatively) mature, well-written package with a nice API.

Shameless plug: if you're dealing with CSRF checks, why not try out my nosurf package?