如何将Gin中间件的上下文值传递给gqlgen的GraphQL解析器?
问题根因定位
- 全局变量不具备请求隔离能力,在多并发请求场景下会出现值互相覆盖的问题,完全无法适配HTTP服务的并发处理逻辑
- 之前编写的
var ConstID = middleware.GetUID()属于包初始化阶段执行的代码,此时服务还未启动任何HTTP请求,拿到的永远是全局UID的初始值"dummy",哪怕后续中间件更新了UID,这个变量也不会重新赋值 - 即使全程用全局UID传递值,高并发时请求A把UID改成用户1的ID,还没等解析器取走,请求B就把UID改成了用户2的ID,最终会出现用户的创建/更新人归属错误,存在严重的安全隐患
正确实现方案(基于Gin+gqlgen官方链路)
1. 改造Token认证中间件,去掉全局变量
完全移除代码里的全局UID变量、GetUID方法,提取到user_id后直接存在Gin的请求上下文里:
func TokenAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { err := auth.TokenValid(c.Request) if err != nil { c.JSON(http.StatusUnauthorized, "You need to be authorized to access this route") c.Abort() return } // 提取token中的user_id userID, err := auth.ExtractTokenID(c.Request) if err != nil { c.JSON(http.StatusUnauthorized, "Invalid token identity") c.Abort() return } // 把user_id存入Gin上下文,和当前请求绑定 c.Set("user_id", userID) c.Next() } }
2. 编写上下文透传中间件,把Gin上下文值传递给GraphQL解析器
gqlgen的解析器只会拿到标准库context.Context,所以需要把存在Gin上下文里的user_id,转到标准请求上下文里,让解析器可以获取:
// 自定义上下文键类型,避免和其他内置/第三方包的键冲突 type contextKey string const CtxKeyUserID = contextKey("graphql_user_id") // 透传Gin存储的user_id到标准请求上下文 func TransferUserIDToGraphQL() gin.HandlerFunc { return func(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.Next() return } // 基于当前请求生成带user_id的新上下文 ctx := context.WithValue(c.Request.Context(), CtxKeyUserID, userID) // 替换原请求的上下文,后续转给gqlgen的就是带值的上下文 c.Request = c.Request.WithContext(ctx) c.Next() } }
3. 路由注册时挂载对应中间件
把认证中间件、上下文透传中间件按顺序挂载到GraphQL的查询路由上:
// 初始化gqlgen服务 graphqlHandler := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &resolver.Resolver{}})) // 挂载GraphQL查询接口 r.POST( "/query", TokenAuthMiddleware(), // 第一步:校验token,提取user_id TransferUserIDToGraphQL(), // 第二步:把user_id透传到GraphQL上下文 gin.WrapH(graphqlHandler), ) // GraphiQL Playground如果不需要认证,就单独挂路由即可 r.GET("/playground", gin.WrapH(playground.Handler("GraphQL Playground", "/query")))
4. 在解析器中从上下文取user_id
完全删除包级的ConstID全局变量,在每个需要取用户身份的解析器函数里,直接从传入的ctx参数中读取user_id:
func (r *mutationResolver) CreateFarmer(ctx context.Context, input model.NewFarmer) (*model.Farmer, error) { // 从上下文读取当前请求的user_id userID, ok := ctx.Value(CtxKeyUserID).(string) if !ok { return nil, fmt.Errorf("unauthorized: missing valid user identity") } db := model.FetchConnection() defer db.Close() now := time.Now() farmer := model.Farmer{ Name: input.Name, Surname: input.Surname, Dob: input.Dob, Fin: input.Fin, PlotLocLat: input.PlotLocLat, PlotLocLong: input.PlotLocLong, CreatedAt: now, UpdatedAt: now, CreatedBy: userID, UpdatedBy: userID, } db.Create(&farmer) return &farmer, nil }
该方案完全基于请求链路传递身份值,没有任何全局共享变量,每个请求的user_id都是独立隔离的,既不会出现值不更新的问题,也能避免高并发下的身份串用风险。
内容的提问来源于stack exchange,提问作者Helen Grey
相关产品推荐
相关产品推荐

