使用 PassportJS 实现动态身份验证重定向
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
如果你花了很多时间编程,那么你很可能接触过身份验证。如果你使用 Node.js,那么你很可能用过 Passport。Passport 是一款非常棒的工具,它已经为开发者节省了数百万甚至数十亿个小时,并且拥有强大的插件生态系统,几乎可以支持你能想到的任何提供商或后端。话虽如此,作为一个高度可定制的库,针对特定用例的文档和社区解答并不容易找到。当我需要将数据从身份验证循环的一端传递到另一端时,我惊讶地发现很难找到相关的文档。
过去一年里,我一直在断断续续地进行一个项目。最初,我把它交给一小群测试人员测试时,最普遍的需求——令我十分懊恼——是增加更多的身份验证选项。虽然我并不意外大家会如此频繁地提出这个需求,但我还是抱着一丝侥幸,希望我匆忙搭建的谷歌网页身份验证系统能够足够好用。因为为了避免自己处理身份验证,我特意将所有身份验证都交给了谷歌,算是走了捷径。然而,既然这是大家最希望改进的地方,我不得不开始漫长而缓慢的身份验证系统修复之旅。
最终,我遇到了需要将数据通过身份验证循环传递回服务器的情况。在我的用例中,我希望调用页面能够影响用户成功授权应用程序后被重定向到的页面。我最终的解决方案是在身份验证请求中将目标 URI 作为查询参数传递。
我花了比我愿意承认的更长的时间才弄明白如何实现它,主要是因为缺乏关于如何将数据传递回回调路由的详细文档。大多数答案都指向了某个passReqToCallback选项,但最终证明那是误导。我一直苦苦思索这个问题,直到偶然发现了GitHub 用户itajajaja的这个回答,其中详细解释了我之前使用 state 参数失败的原因。
首先,您需要像往常一样设置 Passport 配置,在本例中我们将使用passport-github。
const GitHubStrategy = require('passport-github').Strategy;
const express = require('express');
const User = require('PATH/TO/USER/MODEL');
const app = express();
passport.use(new GitHubStrategy({
clientID: GITHUB_CLIENT_ID,
clientSecret: GITHUB_CLIENT_SECRET,
callbackURL: "http://process.env.HOST:4000/auth/github/callback"
},
function(accessToken, refreshToken, profile, cb) {
User.findOrCreate({ githubId: profile.id }, function (err, user) {
return cb(err, user);
});
}
));
// Aaaand wherever you define your router instance
app.get('/auth/github',
passport.authenticate('github'));
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/login' }),
function(req, res) {
// Successful authentication, redirect home.
res.redirect('/');
});
app.listen(4000);
目前,我们有一个 Express 实例,当用户向 Github 发送 GET 请求时,它会向 Github 发送身份验证请求host:4000/auth/github,并且我们已经passport配置为在通过验证函数运行该请求后,将该请求的响应“返回”到配置的回调路由。
遗憾的是,默认设置导致重定向方案相当静态。如果我想根据用户的某些属性或请求路径重定向到特定路径,或许可以在 switch 语句中设置一些条件。然而,随着调用路由和提供程序数量的增加,这种方法变得难以维系,尤其因为它很大程度上依赖于修改请求外部的状态。
幸运的是,passport它提供了一个state参数,可以作为在认证循环中传输数据的绝佳媒介。我们可以通过更新/auth路由来使用它,如下所示:
app.get(`/auth`, (req, res, next) => {
const { returnTo } = req.query
const state = returnTo
? Buffer.from(JSON.stringify({ returnTo })).toString('base64') : undefined
const authenticator = passport.authenticate('github', { scope: [], state })
authenticator(req, res, next)
})
app.get(
`/auth/callback`,
passport.authenticate('github', { failureRedirect: '/login' }),
(req, res) => {
try {
const { state } = req.query
const { returnTo } = JSON.parse(Buffer.from(state, 'base64').toString())
if (typeof returnTo === 'string' && returnTo.startsWith('/')) {
return res.redirect(returnTo)
}
} catch {
// just redirect normally below
}
res.redirect('/')
},
)
returnTo上面,我们从请求查询中提取一个名为 `<parameter_name>` 的参数,并对其进行 Base64 编码,然后将其附加到身份验证请求的选项中。当请求返回时,我们state从返回的请求参数中提取 `<parameter_name>`,然后returnTo对其进行解码并提取其值。此时,我们验证returnTo`<parameter_name>` 的值,并将用户重定向到目标地址。
是不是很简单?现在,你还可以轻松实现更多功能。例如,在我的应用中,我还通过状态传递了额外的参数:
const authenticate = (options) => {
return (req, res, next) => {
const { redir, hash } = req.query;
const state = redir || hash
? new Buffer(JSON.stringify({ redir, hash })).toString('base64') : undefined;
const authenticator = passport.authenticate(options.provider, {
state,
// etc
});
authenticator(req, res, next);
};
};
const callback = (provider, failureRedirect) => [
passport.authenticate(provider, { failureRedirect: failureRedirect || defaultFailureRedirect }),
async (req, res) => {
if (req.isAuthenticated()) {
const { state } = req.query;
const { redir, hash } = JSON.parse(new Buffer(state, 'base64').toString());
const user = (await User.findByID(req.user.id))[0];
if (typeof returnTo === 'string' && returnTo.startsWith('/')) {
if (hash) {
User.set(hash)
}
return res.redirect(returnTo)
}
}
}
]
如上所示,如果我们hash在原始请求中传递参数,我们就可以在将用户重定向回目标页面之前,使用传递的数据更新用户。瞧!这样我们就可以轻松追踪用户上次登录的来源,统一登录和注册路由,并正确重定向等等。
你还能想到其他通过认证循环传递数据的方法吗?请在评论区告诉我!如果你喜欢这篇文章,欢迎在dev.to或Twitter上关注我。
文章来源:https://dev.to/dangolant/dynamic-auth-redirects-with-passportjs-380g