OpenAI ChatGPT API 与 React JS 的完美结合:全面指南
Node.js 破译身份验证指南:示例与预防
开发人员在服务器上的应用程序中实现的最重要的功能之一是身份验证。身份验证对应用程序的未来功能有很大影响。例如,没有它,您就无法实现基于角色的访问控制。没有它,您也无法在应用中添加付款。事实上,在大多数情况下,它实际上是应用程序旅程的第一步!
身份验证对您的业务决策、产品和用户有着重大影响。例如,要评估每日或每周的活跃用户,您需要从经过身份验证的用户会话中提取数据。总而言之,这是一项您绝不能妥协的安全功能。
但是,在实施身份验证时,您可能会无意中忽略某些用例。或者您的身份验证工作流程可能会引入漏洞。在这些情况下,您的应用程序会遭受身份验证中断的困扰。
因此,在这篇文章中,我将讨论什么是身份验证失败。我还将向您展示如何在 NodeJS 应用程序中防止这种情况发生。
服务器端身份验证
在传统的 Web 应用程序中,身份验证有两个组成部分。首先,它使用一组后端 Web 服务进行登录、注册、密码重置和其他安全功能。其次,有一个在前端集成这些服务的 UI。由于 Node.js 主要用于构建后端 API,因此我们来谈谈服务器端身份验证的通用实现。
客户端或前端将用户的凭据作为输入。然后,它将这些凭据发送到后端。为此,它会向身份验证端点发出 HTTP 请求。这可能是/login或/signup,分别表示登录和注册 API 调用。
服务器从请求中接收凭据并对其进行验证。如果凭据正确,它将为该会话创建一个身份验证令牌。它将此身份验证或 auth 令牌发送回客户端。客户端将此令牌存储在前端,并在每次后续请求中发送它。auth 令牌指定两件事:它保存经过身份验证的用户的签名,并代表处于经过身份验证状态的用户。
身份验证失败的情况
此工作流程中可能存在多种漏洞。让我们简单讨论一下。
会话管理不佳
从您创建身份验证令牌的那一刻起,它就成为验证用户是否登录的唯一实体。这意味着您需要考虑以下问题:
- 您为新的身份验证令牌设置了什么TTL ?
- 如何通过合法令牌处理虚假的身份验证请求?
这些问题的答案共同决定了应用程序会话管理的强度。如果用户的身份验证令牌泄露给攻击者,该用户的帐户和私人资源可能会受到损害。
攻击者可以以用户的名义发出虚假请求并访问他们的私人资源。
还有其他场景,比如当用户在公共网络或设备上时。如果用户忘记退出账户怎么办?或者如果恶意的客户端脚本从前端访问身份验证令牌怎么办?
如果您的工作流程没有解决上述三个问题,您的用户帐户可能会受到损害。最终,由于应用程序中的会话管理不善,您将面临身份验证漏洞。
凭证薄弱
如果攻击者设置的密码很容易被猜到,那么他们如何才能阻止他们访问用户帐户?弱凭证是身份验证失败的主要原因。但您可能会想,用户设置的密码实际上不受您的控制,对吗?
你说得对,他们不是。但这并不意味着你对此无能为力!弱凭证可以在很多方面被利用。当用户生成全新密码时,它可能会在创建帐户时发生。或者,如果你让用户重置旧密码,它可能会在密码重置期间发生。
例如,假设用户需要重置密码,他们可以重新使用旧密码之一。如果攻击者过去曾截获过该旧密码,并且您允许用户选择旧密码,那么同一个攻击者就更容易猜出密码,因为他们知道用户经常重复使用密码。这使得攻击者更容易侵入用户的帐户。
因此,评估密码存储方式、验证密码强度以及在身份验证工作流中重置密码非常重要。现在让我们继续学习如何防止 Node.js 应用程序中的身份验证失败。
使用哈希存储密码
散列法可能看起来很明显,但在身份验证系统中仍然经常被忽略。您永远不应该将密码存储为纯文本。相反,您应该将它们散列为安全的加密值。另一个重要的做法是,您不应该着手创建自己的散列算法。
加密是一个复杂的主题。最好留给专家去做,所以最安全的做法是使用一个值得信赖且流行的哈希库。例如,您可以使用类似bcrypt 的东西。
const bcrypt = require('bcrypt');
//Generate Hashed Passwords when saving them to db
userSchema.pre('save', async function(next) {
const salt = await bcrypt.genSalt();
this.password = await bcrypt.hash(this.password, salt);
next();
});
//Validate passwords using bcrypt on login
userSchema.statics.login = async function(email, password) {
const user = await this.findOne({ email });
if (user) {
const auth = await bcrypt.compare(password, user.password);
if (auth) {
return user;
}
throw Error('incorrect password');
}
throw Error('incorrect email');
};
在上面的代码中,我们使用 Mongoose 模型,使用 bcrypt 对密码进行哈希处理。然后我们将这个哈希密码保存在我们的数据库中。对于登录,我们让 bcrypt 解密哈希密码并将其与存储的凭据进行比较,以验证用户的登录详细信息。
在整个过程中,服务器、数据库开发人员或 bcrypt 都不知道用户的密码。这个标准流程是正确实施身份验证的第一步,以防止身份验证被破坏!
NodeJS 中的会话管理
会话管理在塑造身份验证工作流程中起着至关重要的作用。现在我们将看看如何通过对身份验证 API 进行一些小改动来加强会话管理。
以下是 Node.js 中登录 REST API 的控制器:
module.exports.LOGIN = async (req, res) => {
const { email, password } = req.body;
try {
const user = await User.login(email, password);
const token = uuidv4();
res.cookie('auth_token', token);
res.status(200).json({ user: user._id });
}
catch (err) {
const errors = handleErrors(err);
res.status(400).json({ errors });
}
}
在上面的 API 控制器中,我们使用 Mongoose 和 MongoDB 验证凭据。如果凭据有效,我们将从 MongoDB 获取用户详细信息。然后,在响应中,我们发送 UUID作为身份验证令牌。
使用安全可靠的 JWT 和 TTL
JWT代表 JSON Web Token。它是一个 JSON 对象,其中包含一些编码信息,例如签名。这是创建安全可靠的身份验证令牌的标准方法。因此,在上面的控制器中,让我们创建一个生成强大且安全的 JWT 的函数。
您可以使用Node.js 中的jsonwebtoken库来生成 JWT。我们将使用登录用户的 ID 作为签名,并为我们的 JWT 设置 TTL。
const jwt = require('jsonwebtoken');
...
const maxAge = 3 * 24 * 60 * 60;
const createToken = (id) => {
return jwt.sign({ id }, 'secret salt', {
expiresIn: maxAge
});
};
在jwt.sign方法的第二个参数中,我们添加了一个秘密盐关键字。为了演示,我采用了一些简单的东西。但是,您必须并且应该使用更复杂的东西。
我们的身份验证令牌的 TTL 为三天,但您可以根据应用程序的需求进行调整。例如,您可以使用更少的天数。您还可以通过将其设置为取决于用户登录的时间、用户从哪里登录等来使其动态化。
所以现在我们的登录控制器变成:
module.exports.LOGIN = async (req, res) => {
const { email, password } = req.body;
try {
const user = await User.login(email, password);
const token = createToken(user._id);
res.cookie('jwt', token);
res.status(200).json({ user: user._id });
}
catch (err) {
const errors = handleErrors(err);
res.status(400).json({ errors });
}
}
太棒了!您刚刚使身份验证工作流程更加安全,并降低了遇到身份验证漏洞的可能性。让我们继续前进。
使用带 TTL 的 HttpOnly Cookie
我们将 JWT 或身份验证令牌作为 cookie 发送。此 cookie 会自行设置到客户端的浏览器。但是,此 cookie 并没有什么特别之处。任何客户端 JavaScript 实际上都可以访问、修改或删除该 cookie。此外,通过授予客户端访问 cookie 的权限,客户端将承担处理和管理用户会话的部分责任。
我们知道,客户端代码很容易通过浏览器访问。这意味着客户端验证可以在前端轻松绕过。最佳实践是让服务器端到端处理会话管理。我们可以将此身份验证令牌设为特殊的服务器端 cookie。这称为HttpOnly cookie。只有设置此 cookie 的服务器才能访问它。
此外,HttpOnly cookie 会随浏览器的每个请求自动发送。因此,服务器仍然完全负责整个身份验证工作流程。
即使有人攻击客户端代码,客户端 JavaScript 也无法通过 JavaScript 访问或修改 cookie。
res.cookie('jwt', token, { httpOnly: true, maxAge: maxAge * 1000 });
我们还在 cookie 上设置了 TTL。请记住,身份验证令牌的 TTL 表示会话持续时间,而 cookie 的 TTL 则表示该身份验证令牌的生命周期。您需要确保同时实现这两项功能,以实现万无一失的身份验证工作流程。
验证服务器上的凭证强度
我们可以使用名为owasp-password-strength-tester 的库在服务器端验证用户密码的强度,该库根据一些常见的准则来验证密码的强度。
您可以通过运行以下命令来安装它:
npm install owasp-password-strength-test
然后,如下所示导入它:
const owasp = require('owasp-password-strength-test');
接下来,我们可以相应地修改我们的注册控制器:
module.exports.SIGNUP = async (req, res) => {
const { email, password } = req.body;
try {
const result = owasp.test(password);
if(!result.strong){
res.status(401).json({error:result.errors})
}
...
}
catch(err) {
...
}
}
现在,如果用户的密码较弱,您的 SignUp API 会发送错误。此错误中会说明密码缺少哪些内容以确保其安全。客户端可以使用此信息让用户选择更强的密码。
结论
总而言之,以下是您的服务器为防止身份验证中断需要做的:
- 使用哈希算法将密码存储在数据库中
- 使用有效 TTL 将 JWT 保护为身份验证令牌
- 如果可能的话,将 JWT 作为 HttpOnly cookie
- 密码强度验证
文章来源:NodeJS Broken Authentication Guide: Examples and Prevention