首页
/ freeCodeCamp 社交认证实战:用 findOneAndUpdate + upsert 持久化 GitHub 用户(Advanced Node.js and Express)

freeCodeCamp 社交认证实战:用 findOneAndUpdate + upsert 持久化 GitHub 用户(Advanced Node.js and Express)

2026-09-04 11:23:18作者:舒璇辛Bertina

本文围绕 freeCodeCamp 课程 Implementation of Social Authentication III 这一挑战展开:在已配置好 Passport 的 GitHub 策略之后,如何把 GitHub 返回的用户档案(profile)落地到 MongoDB——存在则更新登录信息,不存在则创建新用户。读完你将掌握 findOneAndUpdate 配合 upsert$setOnInsert$set$inc 三组操作符的经典“首次登录建档、每次登录刷新”写法,以及如何处理第三方 OAuth 档案中缺失的隐私字段。

挑战定位:社交认证三步曲的最后一环

该挑战属于 “Advanced Node.js and Express” 项目(challengeType: 2),课程顺序由 advanced-node-and-express.json 定义,整个 GitHub 社交认证被拆成三个连续挑战:

  1. Implementation of Social Authentication:创建 /auth/github/auth/github/callback 两条 GET 路由,配置 GitHub OAuth 应用的 Client ID / Client Secret(存入 .env,以 process.env.GITHUB_CLIENT_ID 形式引用);
  2. Implementation of Social Authentication II:在 auth.jsrequire('passport-github').Strategy 并执行 passport.use(new GitHubStrategy(...)),此时 verify 回调里只有一行 console.log(profile) 占位;
  3. 本文的挑战:用数据库逻辑替换掉这行 console.log(profile),完成“查库—建档—返回用户对象”的闭环。

前两步走通的认证路径是:用户点击登录 → 路由调用 passport.authenticate('github') 跳转 GitHub → 用户授权 → 带着 profile 回到你的 callbackURL → 由策略的 verify 回调决定这个人是老用户还是新用户。本文聚焦的正是路径的最后一步。

此外,该挑战依赖更早的 Implement the Serialization of a Passport User 中建立好的 MongoDB 持久连接:服务器启动时通过 myDB(async client => {...}) 连接 MONGO_URI 指向的数据库,拿到 const myDataBase = await client.db('database').collection('users');。本文示例中的 myDataBase 即来自这一层。

核心实现:GitHub 策略的 verify 回调 + findOneAndUpdate

原文档给出的实现直接放入策略函数的第二个参数(即 verify 回调)内,替换 console.log(profile) 所在的位置:

myDataBase.findOneAndUpdate(
  { id: profile.id },
  {
    $setOnInsert: {
      id: profile.id,
      username: profile.username,
      name: profile.displayName || 'John Doe',
      photo: profile.photos[0].value || '',
      email: Array.isArray(profile.emails)
        ? profile.emails[0].value
        : 'No public email',
      created_on: new Date(),
      provider: profile.provider || ''
    },
    $set: {
      last_login: new Date()
    },
    $inc: {
      login_count: 1
    }
  },
  { upsert: true, new: true },
  (err, doc) => {
    return cb(null, doc.value);
  }
);

逐层拆解:

查询条件:用 GitHub 的全局唯一 id 作键

GitHub 在每个 OAuth 档案中提供一个唯一的 id。以 { id: profile.id } 作为查询条件,是跨应用全局稳定的身份键,比用户名可靠得多(用户名可改、可重复注册)。这也是前序序列化挑战中 serializeUser / deserializeUser 所采用的约定键。

三组操作符的分工

这是本挑战最值得内化的模式——三组 MongoDB 更新操作符各司其职:

操作符 作用时机 本例字段 设计意图
$setOnInsert 仅当文档是新插入(新用户首次登录)时执行 idusernamenamephotoemailcreated_onprovider 档案类字段只在建档时写入一次,避免重复登录时覆盖 created_on 等历史数据
$set 每次查询/更新都执行 last_login: new Date() 无论新老用户,每次登录都刷新最后登录时间
$inc 每次查询/更新都执行 login_count: 1 累计登录次数,原子自增避免并发丢失

原文档对此的概括是:findOneAndUpdate 允许你搜索并更新一个对象;如果对象不存在,它会被插入并传递给回调函数。在这个示例中,last_login 始终被设置、login_count 始终自增 1,而大部分字段只在新对象(新用户)插入时才填充。

选项对象:upsertnew

  • upsert: true——匹配不到文档时自动执行插入,这是“登录即注册”的原子保障,避免了先 findOne 再判断再 insert/update 的竞态问题;
  • new: true——让回调拿到的是更新/插入之后的文档,而不是旧值,保证 cb 返回的是带最新 last_loginlogin_count 的完整用户对象,Passport 随后用它完成序列化。

默认值防御:处理不完整的 OAuth 档案

原文档特别强调:有时返回的档案并不会填满所有信息,或者用户把相关信息设为私密,此时必须用默认值兜底以防报错。示例中的三处防御值得逐一看:

  • name: profile.displayName || 'John Doe'——GitHub 用户可能没有设置显示名;
  • photo: profile.photos[0].value || ''——没有头像时置空字符串;
  • email: Array.isArray(profile.emails) ? profile.emails[0].value : 'No public email'——用户未公开邮箱时 profile.emails 可能缺失或非预期结构,先判数组再取值。

这种“第三方数据一律不可信、字段级兜底”的写法,是集成任何 OAuth 提供商(Google、Facebook 等)时的通用实践。

回调返回值的细节:doc.value

回调中 return cb(null, doc.value) 把数据库文档交给 Passport 的 verify 回调参数 cb(对应第二步挑战中 verify 函数签名的最后一个参数)。需要指出的是:这里取的是 doc.value,这是较旧版本 MongoDB Node.js 驱动(callback 风格 API)中 findOneAndUpdate 结果对象的结构——查询结果包裹在 value 属性里。若你迁移到新版驱动(链式/命令式 API,如 findOneAndUpdate 直接返回 Mongo2BulkWriteResultfindOneAnd*result.value),字段路径会不同,迁移时需对照所用驱动版本的返回值结构做调整。此处以课程原始示例的驱动环境为准。

自动化验收:课程如何判定这段实现正确

挑战文档 # --hints-- 部分的测试断言揭示了验收标准(测试会拉取学习者沙箱中的 /_api/auth.js 源码做正则匹配):

const url = new URL("/_api/auth.js", code);
const res = await fetch(url);
const data = await res.text();
assert.match(
  data,
  /GitHubStrategy[^]*myDataBase/gi,
  'Strategy should use now use the database to search for the user'
);
assert.match(
  data,
  /GitHubStrategy[^]*cb/gi,
  'Strategy should return the callback function "cb"'
);

两条正则传达的要求很直接:

  1. /GitHubStrategy[^]*myDataBase/gi——从 GitHubStrategy 出现的位置往后([^]* 可跨行),必须出现 myDataBase,即策略实现中确实接入了数据库查询,而非仅 console.log
  2. /GitHubStrategy[^]*cb/gi——同样在策略代码范围内必须出现对 verify 回调 cb 的调用,否则 Passport 会因 verify 未调用回调而挂起认证流程。

也就是说,哪怕不连真实 GitHub 账户,从断言语义上也能确认实现是否满足“策略 → 查库 → 回调返回用户对象”这条调用链。

验证清单:如何确认登录已经打通

按原文档的收尾建议("You should be able to login to your app now. Try it!"),实操验证步骤为:

  1. 确认 .envGITHUB_CLIENT_IDGITHUB_CLIENT_SECRETMONGO_URI 均已配置,且 dotenv 已 require 并加载(第二步挑战已要求);
  2. 确认 GitHub OAuth 应用的 callback URL 与策略里的 callbackURL 完全一致(主页 URL + /auth/github/callback);
  3. 启动应用,点击首页(第一步中已加 showSocialAuth: true 的 homepage 路由)上的 GitHub 登录按钮;
  4. 授权完成后应跳转 /profile;检查 MongoDB users 集合:首次登录应有带 created_on 的新文档,再次登录时 login_count 递增、last_login 更新而 created_on 不变——这正是三组操作符分工的预期表现。

如果实现过程报错,原文档提供了 freeCodeCamp 论坛上“完成到这一步的项目”讨论帖作为排查参照(原文为外部论坛链接,此处不重复给出)。

小结

本文挑战的本质,是把一个 OAuth verify 回调写成“幂等建档 + 每次登录刷新”的原子操作:findOneAndUpdate + { upsert: true, new: true } 负责“不存在则插入、存在则更新并回传最新文档”,$setOnInsert / $set / $inc 三组操作符分别对应“只写一次”“每次覆盖”“每次累加”三类字段的更新语义,而 ||Array.isArray 默认值则兜住了第三方档案的隐私与缺失场景。这一模式不依赖 GitHub——替换 provider 字段与档案字段映射后,同样适用于其他社交登录提供商,是 Node.js + Express 应用中用户持久层的一条通用实践。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341