在处理程序之后访问HTTP请求上下文

huangapple go评论79阅读模式
英文:

Accessing HTTP Request context after handler

问题

在我的日志中间件(链中的第一个)中,我需要访问一些上下文信息,这些信息是在链中的某个身份验证中间件中编写的,并且仅在处理程序本身执行后才能访问。

附注:日志中间件需要首先调用,因为我需要记录请求的持续时间,包括在中间件中花费的时间。此外,身份验证中间件能够在权限不足时中止请求。在这种情况下,我还需要记录失败的请求。

我的问题是,从http.Request指针中读取上下文不会返回我期望的身份验证数据。请参考下面的示例:

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

const (
	contextKeyUsername = "username"
)

func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		ctx = context.WithValue(ctx, contextKeyUsername, "user123")
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func logMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func(start time.Time) {
			ctx := r.Context()
			username := ctx.Value(contextKeyUsername)
			if username != nil {
				fmt.Printf("user %s has accessed %s, took %d\n", username,
					r.URL.Path, time.Since(start).Milliseconds())
			} else {
				fmt.Printf("annonyous has accessed %s, took %d\n",
					r.URL.Path, time.Since(start).Milliseconds())
			}
		}(time.Now())
		next.ServeHTTP(w, r)
	})
}

func welcome(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	username := ctx.Value(contextKeyUsername)
	if username != nil {
		fmt.Fprintf(w, fmt.Sprintf("hello %s", username.(string)))
	} else {
		fmt.Fprintf(w, "hello")
	}
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/welcome", welcome)
	chain := logMiddleware(authMiddleware(mux))
	http.ListenAndServe(":5050", chain)
}

虽然对127.0.0.1:5050/welcome的GET请求确实返回了预期的字符串hello user123,但日志的输出却是:

annonyous has accessed /welcome, took 0

由于请求被作为指针传递,我本来期望在执行延迟操作时,上下文中将包含预期的username值。

我在这里漏掉了什么?

英文:

In my logging middleware (first in chain) I need to access some context that is written in some auth middleware futher down the chain and only after the handler itself is executed.

Side note: The logging middleware needs to be called first since I need to log the duration of the request including the time spend in middleware. Also the auth middleware is able to abort a request when permissions are not sufficient. in that case I need to log the failed request as well.

My problem with that is that reading the context from the http.Request pointer does not return the auth data I would expect it to have. See the example bellow:

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

const (
	contextKeyUsername = "username"
)

func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		ctx = context.WithValue(ctx, contextKeyUsername, "user123")
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func logMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func(start time.Time) {
			ctx := r.Context()
			username := ctx.Value(contextKeyUsername)
			if username != nil {
				fmt.Printf("user %s has accessed %s, took %d\n", username,
					r.URL.Path, time.Since(start).Milliseconds())
			} else {
				fmt.Printf("annonyous has accessed %s, took %d\n",
					r.URL.Path, time.Since(start).Milliseconds())
			}
		}(time.Now())
		next.ServeHTTP(w, r)
	})
}

func welcome(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	username := ctx.Value(contextKeyUsername)
	if username != nil {
		fmt.Fprintf(w, fmt.Sprintf("hello %s", username.(string)))
	} else {
		fmt.Fprintf(w, "hello")
	}
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/welcome", welcome)
	chain := logMiddleware(authMiddleware(mux))
	http.ListenAndServe(":5050", chain)
}

While a get request to 127.0.0.1:5050/welcome does return the expected string hello user123, the output of the log is:

annonyous has accessed /welcome, took 0

Since the request is passed along as pointer I would have expected that at the time the defer is executed, the context would contain the expected username value.

What am I missing here?

答案1

得分: 2

WithContext 返回请求的浅拷贝,即由 authMiddleware 创建的请求与 logMiddleware 从中读取上下文的请求不是同一个请求。

你可以让根中间件(在这种情况下是 logMiddleware)创建带有值的上下文和浅拷贝的请求,但是不要存储一个普通字符串,而是在上下文中存储一个非空指针,然后让 authMiddleware 使用指针间接赋值给指针指向的值,然后在 next 退出后,logMiddleware 可以解引用该指针来访问该值。

为了避免不愉快的解引用,你可以使用指向具有字符串字段的结构体的指针。

type ctxKey uint8

const userKey ctxKey = 0

type user struct{ name string }

func logMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		u := new(user)
		r = r.WithContext(context.WithValue(r.Context(), userKey, u))

		defer func(start time.Time) {
			if u.name != "" {
				fmt.Printf("用户 %s 已访问 %s,耗时 %s\n", u.name, r.URL.Path, time.Since(start))
			} else {
				fmt.Printf("匿名用户已访问 %s,耗时 %s\n", r.URL.Path, time.Since(start))
			}
		}(time.Now())

		next.ServeHTTP(w, r)
	})
}
func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if u, ok := r.Context().Value(userKey).(*user); ok {
			u.name = "user123"
		}
		next.ServeHTTP(w, r)
	})
}
func welcome(w http.ResponseWriter, r *http.Request) {
	if u, ok := r.Context().Value(userKey).(*user); ok && u.name != "" {
		fmt.Fprintf(w, "你好 %s", u.name)
	} else {
		fmt.Fprintf(w, "你好")
	}
}

https://go.dev/play/p/N7vmjQ7iLM1

英文:

WithContext returns a shallow copy of the request, i.e. the request created by the authMiddleware is not the same request as the one from which logMiddleware is reading the context.

You could have the root middleware (in this case that would be the logMiddleware) create the context-with-value and the shallow request copy, but instead of a plain string store an non-nil pointer in the context, then have the authMiddleware use pointer indirection to assign the value to which the pointer points, then the logMiddleware, after next exits, can dereference that pointer to access that value.

And to avoid the unpleasant dereferencing, instead of a pointer to a string, you can use a pointer to a struct with a string field.

type ctxKey uint8

const userKey ctxKey = 0

type user struct{ name string }

func logMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		u := new(user)
		r = r.WithContext(context.WithValue(r.Context(), userKey, u))

		defer func(start time.Time) {
			if u.name != "" {
				fmt.Printf("user %s has accessed %s, took %s\n", u.name, r.URL.Path, time.Since(start))
			} else {
				fmt.Printf("annonyous has accessed %s, took %s\n", r.URL.Path, time.Since(start))
			}
		}(time.Now())

		next.ServeHTTP(w, r)
	})
}
func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if u, ok := r.Context().Value(userKey).(*user); ok {
			u.name = "user123"
		}
		next.ServeHTTP(w, r)
	})
}
func welcome(w http.ResponseWriter, r *http.Request) {
	if u, ok := r.Context().Value(userKey).(*user); ok && u.name != "" {
		fmt.Fprintf(w, "hello %s", u.name)
	} else {
		fmt.Fprintf(w, "hello")
	}
}

https://go.dev/play/p/N7vmjQ7iLM1

huangapple
  • 本文由 发表于 2022年10月4日 20:10:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/73947665.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定