GraphQL Go 后端教程:在 Resolver 中获取登录用户并完善 CreateLink 与 Links 数据关联
【免费下载链接】howtographqlThe 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.LinkGraphQL 层含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.sqlCREATE 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、以及临时变量idUserID与username。随后手动构造users.User并赋值给link.User完成链接带创建者的组装。INNER JOIN是 SQL 中最基础的连接方式之一如果对它不熟悉可以查阅 SQL 教程中关于sql_join_inner的内容理解其取交集语义。端到端验证从 access denied 到成功创建所有代码改完后整个应用就闭环了。启动服务器默认端口 8080打开 GraphQL Playground先不带任何鉴权信息提交创建链接的 mutationmutation { 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 放入 contextCreateLink校验通过后带着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 即可创建并查询到带创建者的链接验证了完整链路。赞分享【免费下载链接】howtographqlThe Fullstack Tutorial for GraphQL项目地址https://gitcode.com/gh_mirrors/ho/howtographql点击查看免费下载相关推荐How to GraphQLGo 后端用 gqlgen 实现 CreateLink 变更与 links 查询的数据库读写实战How to GraphQLGo 后端用 gqlgen 实现 CreateLink 变更与 links 查询的数据库读写实战 本篇技术指南以 How toMultiJSON与Rails集成打造高效JSON API的最佳实践MultiJSON与Rails集成打造高效JSON API的最佳实践 MultiJSON是一个为JSON处理提供通用可交换后端的Ruby库它允许开发者在不同终极VSCode Fortran插件指南5分钟快速上手教程终极VSCode Fortran插件指南5分钟快速上手教程 想要在Visual Studio Code中高效编写Fortran代码吗fortran lang网页爬虫上一篇tt-rss-feedly-theme夜间模式一键切换深色主题与toggle_night_mode插件详解下一篇终极PyTorch资源库从入门到专家的完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考