☰
GraphQL Go 后端教程:在 Resolver 中获取登录用户并完善 CreateLink 与 Links 数据关联
2026/9/25 4:24:14 网站建设 项目流程

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载

本篇技术指南承接 Hackernews GraphQL API 克隆项目的认证体系构建,讲解如何在 gqlgen 的 Resolver 中通过context获取登录用户对象,从而补齐此前因无法鉴权而搁置的CreateLinkmutation 实现——包括写入用户外键、用 SQLINNER JOIN在查询时回填创建者信息,以及最终通过 GraphQL Playground 的 HTTP Headers 携带 JWT 完成端到端验证。读完本文,你将掌握"中间件解析用户 → context 传递 → Resolver 读取 → 数据库外键落库 → JOIN 联查回填"的完整闭环,这也是大多数真实业务 API 中"记录资源归属人"的标准实现模式。

为什么 CreateLink 之前是"未完成"状态

在教程的早期阶段(创建与检索链接章节),CreateLinkmutation 的初始实现是这样写的:

func (r *mutationResolver) CreateLink(ctx context.Context, input model.NewLink) (*model.Link, error) { var link links.Link link.Title = input.Title link.Address = input.Address linkID := link.Save() return &model.Link{ID: strconv.FormatInt(linkID, 10), Title: link.Title, Address: link.Address}, nil }

问题很明显:这条记录没有关联任何用户。虽然 Schema 中Link类型已经声明了必填的user: User!字段(见入门与 Schema 定义章节),但当时的实现既没有从请求中识别出"是谁在创建链接",也没有把用户信息写入数据库。返回的model.Link中User字段为空,本质上是在"伪造"数据。

现在认证体系已经就绪(认证实现章节 完成了 JWT 生成/解析与认证中间件,认证端点章节 完成了注册、登录与刷新 Token 三个 mutation),我们就可以回头把这块补完。

认证中间件如何把用户塞进 context

要理解 Resolver 中的取用户逻辑,先回顾认证中间件(internal/auth/middleware.go)的两个关键函数:

var userCtxKey = &contextKey{"user"} // Middleware 在每个请求进入 Resolver 前执行 func Middleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { header := r.Header.Get("Authorization") // 未携带 header 时直接放行(匿名请求) if header == "" { next.ServeHTTP(w, r) return } // 解析 JWT,得到用户名 username, err := jwt.ParseToken(header) if err != nil { http.Error(w, "Invalid token", http.StatusForbidden) return } // 根据用户名查库,得到用户 ID id, err := users.GetUserIdByUsername(username) // 把用户对象放入 context ctx := context.WithValue(r.Context(), userCtxKey, &user) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } } // ForContext 从 context 中取出用户,前提是 Middleware 已经执行过 func ForContext(ctx context.Context) *users.User { raw, _ := ctx.Value(userCtxKey).(*users.User) return raw }

整个链路是:请求带Authorization头 → 中间件解析出 JWT 中的用户名 → 查库拿到用户 ID → 构造*users.User塞进context→ Resolver 通过auth.ForContext(ctx)取回。需要特别注意的是中间件对匿名请求是放行的(header == ""时直接next.ServeHTTP),所以ForContext返回的可能是nil——这正是下文access denied分支的判定依据。

补全 CreateLink:从 context 读取登录用户

现在回到schema.resolvers.go,为CreateLink加上鉴权与归属逻辑:

func (r *mutationResolver) CreateLink(ctx context.Context, input model.NewLink) (*model.Link, error) { // 1 user := auth.ForContext(ctx) if user == nil { return &model.Link{}, fmt.Errorf("access denied") } . . . // 2 link.User = user linkID := link.Save() graphqlUser := &model.User{ ID: user.ID, Name: user.Username, } return &model.Link{ID: strconv.FormatInt(linkID, 10), Title: link.Title, Address: link.Address, User: graphqlUser}, nil }

代码只改了两处,但含义关键:

  • 1(鉴权守卫):auth.ForContext(ctx)从 context 取出用户对象;如果取到nil(说明请求没带合法 JWT、中间件放行了匿名请求),立即返回空 Link 与access denied错误,阻止未登录用户写入数据。这是所有"需要登录才能操作"的 mutation 的通用入口写法。
  • 2(写入归属):把当前登录用户赋给link.User后调用link.Save(),让数据库层能拿到user.ID写入外键;返回时构造 GraphQL 层的model.User(注意这里把ID与Username映射为 GraphQL 的id与name字段)。

这里体现了教程此前提到过的双结构体设计:links.Link(数据库层,含User *users.User)与model.Link(GraphQL 层,含User *model.User)各司其职,Resolver 负责两者之间的转换。

完善 Links 查询:返回每条链接的创建者

在补全写入侧之后,查询侧也要同步升级——Links查询此前只返回ID/Title/Address,现在要把每条链接的创建者一并返回:

func (r *queryResolver) Links(ctx context.Context) ([]*model.Link, error) { var resultLinks []*model.Link var dbLinks []links.Link dbLinks = links.GetAll() for _, link := range dbLinks { graphqlUser := &model.User{ ID: link.User.ID, Name: link.User.Username, } resultLinks = append(resultLinks, &model.Link{ID: link.ID, Title: link.Title, Address: link.Address, User: graphqlUser}) } return resultLinks, nil }

改动点集中在循环体内:直接从dbLinks里取出的每条links.Link读取其User字段,转换成model.User后装配进结果。前提是数据库层的GetAll()必须真的把User字段填充好——这正是下一节要解决的 JOIN 问题。

数据库层改动(一):Save 方法写入用户外键

internal/links/links.go的Save方法原来只插入标题和地址:

stmt, err := database.Db.Prepare("INSERT INTO Links(Title,Address) VALUES(?,?)") res, err := stmt.Exec(link.Title, link.Address)

现在要插入用户外键,改为三列:

stmt, err := database.Db.Prepare("INSERT INTO Links(Title,Address, UserID) VALUES(?,?, ?)")

执行语句时把link.User.ID作为第三个参数传入:

res, err := stmt.Exec(link.Title, link.Address, link.User.ID)

这条 SQL 之所以能成立,是因为数据库迁移阶段创建的Links表本身就定义了外键约束。回顾数据库设置章节中的迁移文件000002_create_links_table.up.sql:

CREATE TABLE IF NOT EXISTS Links( ID INT NOT NULL UNIQUE AUTO_INCREMENT, Title VARCHAR (255) , Address VARCHAR (255) , UserID INT , FOREIGN KEY (UserID) REFERENCES Users(ID) , PRIMARY KEY (ID) )

UserID列通过FOREIGN KEY (UserID) REFERENCES Users(ID)关联到Users表主键,从表结构层面保证了每条链接必然归属一个已存在的用户,外键约束在插入时就会校验引用的合法性。

数据库层改动(二):GetAll 用 INNER JOIN 回填创建者

查询侧同样需要联动修改。GetAll原来的 SQL 只查Links表自身字段,现在必须联查Users表才能拿到创建者的用户名。使用INNER JOIN让两条记录按UserID = Users.ID配对:

func GetAll() []Link { stmt, err := database.Db.Prepare("select L.id, L.title, L.address, L.UserID, U.Username from Links L inner join Users U on L.UserID = U.ID") // changed if err != nil { log.Fatal(err) } defer stmt.Close() rows, err := stmt.Query() if err != nil { log.Fatal(err) } defer rows.Close() var links []Link var username string var id string for rows.Next() { var link Link err := rows.Scan(&link.ID, &link.Title, &link.Address, &id, &username) // changed if err != nil { log.Fatal(err) } link.User = &users.User{ ID: id, Username: username, } // changed links = append(links, link) } if err = rows.Err(); err != nil { log.Fatal(err) } return links }

逐项拆解这条 JOIN 查询:

  • from Links L inner join Users U on L.UserID = U.ID:给Links起别名L、Users起别名U,以L.UserID = U.ID为连接条件。INNER JOIN只返回两边都匹配上的行——如果某条 Link 的UserID在Users表中找不到对应记录,该行会被过滤掉,这与外键约束的语义是一致的。
  • select L.id, L.title, L.address, L.UserID, U.Username:同时取链接自身字段与用户的Username,一次查询拿到组装完整Link所需的全部数据。
  • rows.Scan顺序必须与 select 列表严格一致:依次填入link.ID、link.Title、link.Address、以及临时变量id(UserID)与username。随后手动构造users.User并赋值给link.User,完成"链接带创建者"的组装。

INNER JOIN是 SQL 中最基础的连接方式之一,如果对它不熟悉,可以查阅 SQL 教程中关于sql_join_inner的内容理解其"取交集"语义。

端到端验证:从 access denied 到成功创建

所有代码改完后,整个应用就闭环了。启动服务器(默认端口 8080),打开 GraphQL Playground,先不带任何鉴权信息提交创建链接的 mutation:

mutation { createLink(input: {title: "real link!", address: "www.graphql.org"}){ user{ name } } }

此时因为请求头里没有Authorization,中间件放行匿名请求、ForContext(ctx)返回nil,于是得到:

{ "errors": [ { "message": "access denied", "path": [ "createLink" ] } ], "data": null }

这正是预期行为:未登录用户无法提交链接,access denied从源头拦截了越权写入。注意错误响应中data为null,因为 Resolver 返回了非 nil 的 error。

要成功创建,需要在 Playground 底部点击HTTP Headers按钮,填入 Authorization 头——值为登录/注册接口返回的 JWT 令牌(在认证端点章节中通过createUser或loginmutation 获取):

{ "Authorization": "" // use your own generated token }

带上合法 token 重新提交同样的 mutation,中间件会解析出用户名、查出用户 ID 放入 context,CreateLink校验通过后带着UserID落库,查询时再通过 JOIN 把创建者带回来。再次执行links查询,就能看到每条链接都带有创建者的name字段。

至此,认证与数据归属这条主线全部打通:注册/登录产出 JWT → 中间件解析并注入 context → mutation 校验登录态并写入外键 → 查询用 JOIN 回填创建者。这也是后续实现"只看某用户的链接""删除/编辑权限校验"等功能的基础设施。

关键点小结

  • 鉴权入口统一:auth.ForContext(ctx)配合if user == nil判空,是所有需要登录的 mutation 的标准守卫写法;匿名请求被中间件放行,所以判空是必须的。
  • 双结构体转换:links.Link(含*users.User)与model.Link(含*model.User)在 Resolver 中完成互转,数据库层与 GraphQL 层解耦。
  • 外键写入:Save方法在INSERT语句中带上UserID,配合迁移文件中的FOREIGN KEY约束保证引用完整性。
  • JOIN 联查回填:GetAll用INNER JOIN一次取回链接与创建者,rows.Scan的字段顺序必须与 select 列表严格对应。
  • 端到端验证:不带头部得到access denied,带上 JWT 即可创建并查询到带创建者的链接,验证了完整链路。

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载
上一篇:tt-rss-feedly-theme夜间模式一键切换:深色主题与toggle_night_mode插件详解
下一篇:终极PyTorch资源库:从入门到专家的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询