A peep beneath the hood of PassportJS' OAuth flow Index

2025-06-08

深入了解 PassportJS 的 OAuth 流程

指数

库很棒。它们提供了标准化、广泛兼容且简洁的方法来执行常见任务,抽象出了我们通常不关心的细节。它们帮助我们不必担心身份验证、数据库处理或表单验证的具体细节,只需编写我们想到的代码即可。

但是,如果事情没有按预期进行,会发生什么呢?如果你只是按下黑盒子上的按钮,你怎么知道出了什么问题?

我们迟早需要了解我们借来的图书馆是如何做这些小事的🎶,以找出我们(或他们)在哪里走错了方向并能够纠正它。

当我决定为个人项目学习PassportJS时,我遇到了这种情况。在本文中,我打算深入探讨使用 PassportJS 处理 OAuth 流程时最难解决的问题。

如果你需要关于如何实现 PassportJS 实现 OAuth 的完整教程,一如既往,我推荐YouTube 上精彩的 Net Ninja 教程。或者,如果你只是想复习一下如何操作 OAuth,可以看看我之前关于这个主题的文章。

指数

  • 基本 PassportJS 设置
  • 调用身份验证
  • verify回调之路
  • 序列化和反序列化到底是什么鬼?
  • 完整的登录流程
  • 经过身份验证的请求流程

基本 PassportJS 设置

这些是我们开始所需要的唯一东西:正确配置的护照策略和2 个端点(一个用于授权,另一个用于重定向)。

const passport = require('passport')
const GitHubStrategy = require('passport-github')
passport.use(
new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: process.env.GITHUB_REDIRECT_URL || '/auth/github/redirect',
scope: ['user:email']
},
(accessToken, refreshToken, profile, done) => {
// This function is the "verify callback".
done(null, { accessToken, profile })
}
)
)
app.get('/auth/github', passport.authenticate('github'))
app.get('/auth/github/redirect',
passport.authenticate('github')
},
(req, res) => {
res.send(req.user)
}
)
view raw server.js hosted with ❤ by GitHub
const passport = require('passport')
const GitHubStrategy = require('passport-github')
passport.use(
new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: process.env.GITHUB_REDIRECT_URL || '/auth/github/redirect',
scope: ['user:email']
},
(accessToken, refreshToken, profile, done) => {
// This function is the "verify callback".
done(null, { accessToken, profile })
}
)
)
app.get('/auth/github', passport.authenticate('github'))
app.get('/auth/github/redirect',
passport.authenticate('github')
},
(req, res) => {
res.send(req.user)
}
)
view raw server.js hosted with ❤ by GitHub

调用身份验证

Passport 的优点在于,您可以使用它注册任意数量的策略,然后使用身份验证方法根据所调用的路线告诉它使用哪一个策略,如下所示:

passport.authenticate('github');
Enter fullscreen mode Exit fullscreen mode

当您配置其中一种策略时,您必须定义一些参数以及一个验证回调函数,该函数将处理从提供商处获取的用户数据。

passport.authenticate()至少对我来说,奇怪的是必须呼叫两条不同路线的原因。

但这里有个诀窍:

一次调用 Passport时authenticate(),它会尝试根据你传递给先前注册的函数的字符串名称来查找它是否具有相应的策略。如果存在,它会通过访问提供商的授权端点来启动 OAuth 流程。如果找不到,它会抛出一个错误,提示策略未知。

现在,第二次调用是在提供商 OAuth 服务器的回调中,在重定向路由中。这一次,虽然看起来完全一样,但 Passport 会检测到它处于 OAuth 流程的第二阶段,并告诉策略使用刚刚获得的临时代码来请求 OAuth 令牌。策略确切地知道如何以及在何处请求令牌。 

这之后会发生什么?

验证回调之路

看看我最新的手绘作品,这是一张关于 PassportJS 中 OAuth 流程的图表。现在我们到达了那个红色的气泡,上面写着getProfile()

手绘图

如果这让您比以前更加困惑,请继续阅读;我保证它会变得更好!

获取 OAuth 令牌后,策略首先会获取该用户的个人资料。这是策略的内部机制,它知道在特定提供商上从哪里获取该个人资料。
 
紧接着,策略会尝试将个人资料解析为它为该提供商内部定义的模型,然后将其与它拥有的所有其他数据(accessToken、refreshToken 和 profile)一起传递给我们的验证回调

还记得我们在配置策略时定义的验证回调吗?现在是我们自定义的代码第一次被策略执行。在这个例子中,我们可以检查该用户的数据库,在必要时为其创建一条记录,并验证其他任何需要的内容。

一旦我们检查完了所有需要的内容,我们就会调用 done (或者verify 回调函数的回调函数),这是它的第四个也是最后一个函数参数。我们会将它(如果没有错误)和所有我们认为相关的信息传递null给用户。

(accessToken, refreshToken, profile, done) => {
  // verify things here and then...
  done(null, {accessToken, profile})
}
Enter fullscreen mode Exit fullscreen mode

最后,Passport 将执行其自身的操作req.login(),将用户保存到文件中req.user以供将来使用。
 
再看一下上面的图表,你现在应该理解得更清楚了。

接下来是serializeUser👇

序列化和反序列化到底是什么鬼?

序列化是将数据结构或对象状态转换为可存储、传输和后续重建的格式的过程。——
维基百科的序列化文章

在我们的例子中,“数据”就是我们一直在处理的用户信息。我们serializeUser应该在 Passport 的方法中自定义代码,定义哪些信息需要持久化到会话中,以便稍后能够通过将其传递给 serializeUser 的回调函数来获取完整的用户信息done。 

这是 Passport 的序列化用户方法,形式非常简单:

 

passport.serializeUser((user, done) => done(null, {
  id: user.profile.id,
  accessToken: user.access_token
}))
Enter fullscreen mode Exit fullscreen mode

☝️此对象最终将在后续请求req.userreq.session.passport.user使用。

现在deserializeUser,此函数将接收会话中存在的用户数据,并使用这些数据从我们的数据库中获取所有用户数据。例如:

passport.deserialize((user, done) => {
  dbHelper.getUser(user.id)
    .then(profile => done(profile))
})
Enter fullscreen mode Exit fullscreen mode

传递到done这里的任何内容都将在 中可用req.user
 

完整的登录流程

让我们放大一下上图,特别是 OAuth 流程结束后的样子。我想深入研究一下这个流程,因为我记得刚开始使用 PassportJS 进行 OAuth 时,它显得特别神秘。

先前绘制的图表的特写

因此,在用户说“是,允许”并且我们的应用程序获取他们的访问令牌后,会发生以下情况:

  • Passport 从提供商处接收 OAuth 令牌
  • 它使用它来获取用户的个人资料信息
  • 运行verifyCallback,完成后它将用户对象传递给它自己的done回调
  • Passport 调用其自己的方法req.login(),然后调用serializeUser()。serializeUser 提取一些用户信息以保存在会话中,然后继续执行重定向路由的以下处理程序。

经过身份验证的请求流程

现在,这一切都非常好,但是我们的应用程序如何知道用户在进一步的请求中仍然经过身份验证,并且可以安全地提供私人信息? 

这不是一个完整的教程,但如果您一直在关注它,那么您的服务器代码中可能有类似这样的内容:

server.use(passport.initialize())
server.use(passport.session())
Enter fullscreen mode Exit fullscreen mode

这些行配置了两个中间件,它们将在我们的服务器收到的每个请求时运行。

当发出经过身份验证的请求时,Express 会将会话加载到 req 中,从而使我们的序列化用户数据在 处可用req.session.passport.user

然后,第一个中间件initialize()将尝试在请求中找到该用户,如果不存在则将其创建为空对象(这意味着用户未经过身份验证)。 

然后,session()Which 会启动,通过尝试在请求中查找序列化对象来判断该请求是否已通过身份验证。
 
找到后,它会将其传递给deserializeUserWhich,Which 会使用它来获取完整的用户数据(可能来自数据库),并将其添加到req.user我们可以使用它来创建其他请求的地方。

因此,即使serializeUser 仅在登录时调用,它deserializeUser也是一个全局中间件,它将在每次请求时执行,以使完整的用户对象可用于经过身份验证的请求。


我对 OAuth 流程的深入探讨到此结束,希望它能帮助您至少更好地理解 PassportJS 的幕后运作。它确实帮助我理清了一些疑惑,让我得以撰写本文。感谢您的阅读!

当我为本文进行研究时,我偶然发现了J. Walton 编写的PassportJS 非官方文档,它肯定能帮助您解决您可能遇到的任何其他疑问。

鏂囩珷鏉ユ簮锛�https://dev.to/anabella/a-peep-beneath-the-hood-of-passportjs-oauth-flow-eb5
PREV
了解 CSS Position 的不同搭配
NEXT
为什么 {} > [] ?