Node.jsバックエンドログインロジック
そこで、Node.jsで、ユーザーを認証してJWTを処理するかなり原始的なログインロジックを作成しました。セキュリティ、効率、構築、非同期/同期、ロギングの点で何か良いですか?セキュリティは私の主な関心事です。 よりきれいな形式では、質問は次のようになります。
- セキュリティ:私のウェブサイトは、何らかの形、形、形式で安全ですか?によって提供されるメソッドに組み込まれているもの以外のセキュリティ対策を実装できるかどうか疑問に思ってい
Node.jsます。また、パスワードを推測するのは明らかですが、さまざまなユーザーが機能するときに確実にログインできるようにするためのパスワードです。 - 効率:ユーザー名とパスワードのチェック方法は効率的ですか?これを行うためのより良い方法はありますか?
- 建物:ウェブサイトをどのように読み込んだかは問題ありませんか?ファイルから読み取り、応答を終了しますか?
- ASYNC / SYNC:プリフォーム
asyncとsync呼び出しを同時に行うことはわかっています。これに問題はありますか? - LOGGING:私は、サーバーへの接続をすべてログに記録し、すべてのログイン試行。これは良い習慣ですか、それともロギングが達成するはずのことをやり過ぎていますか?(出典:Node.jsを使用したログインサーバー)
私のコードは:
// main login logic
app.post('/api/login', apiLimiter, async function (req, res) {
// TODO: implement cookie check whether a still valid token is set and if so respond with cookie already set
// TODO: add roles into jwt and add roles checking into other code
// TODO: if wrong password send a response telling there's a wrong password/username
try {
const pw = req.body.password;
const submittedUser = req.body.email;
User.findOne({eMail: req.body.email}, async function (err, user) {
if (err) throw err;
console.log(user);
const match = await bcrypt.compare(pw, user.password);
console.log(match);
if (match === true && user.eMail == submittedUser) {
jwt2.sign({user}, 'secrettokenhere', { expiresIn: '15min'}, (err, token) =>{
res.cookie(
'access_token', 'Bearer '+ token, {
//domain: 'localhost',
path: '/',
expires: new Date(Date.now() + 900000), // cookie will be removed after 15 mins
httpOnly: true // in production also add secure: true
})
.json(
user
);
});
}
else {
res.status(200).send("Bad login");
}
});
} catch (err) {
res.status(500).send();
console.log(err);
}
});
PSフロントエンドロジックに関するフォローアップの個別の質問があります。
回答
非同期ログインバグザ・はtry/catch周りにUser.findOneいるので、何も達成していないfindOne非同期です。ときにfindOne失敗し、それがコールバックにエラーを渡しますが、コールバックがないときにthrow err応答がユーザーに送信されませんので、何も、非同期エラーをキャッチすることはありません。もう1つの問題は、user存在するかどうかを確認していないことです。存在しない場合は、そのpasswordプロパティにアクセスしようとするとエラーがスローされます。(.signエラーが発生するかどうかもチェックしていません)
コールバックの代わりにPromisesを使用することを検討することもできます-findOneすでにPromiseが返されます。Promisesはチェーン化が容易で、エラー処理がよりクリーンになるため、多くの場合、Promisesが推奨されます。
ただし、セキュリティは妥当に見えます。
- 接続がHTTPS経由でのみ許可されていることを確認してください。HTTPリクエストは傍受可能です。
- 上記のログインバグが修正された場合でも、現在の実装では、特定の電子メールが登録されているかどうかを誰にでも通知できます。たとえば、あなたのメールアドレスがわかっている場合は、あなたとしてログインし、応答を調べて、パスワードが間違っているかどうか、またはそのようなメールが存在しないかどうかを確認できます。これは望ましくない場合があります。修正したい場合は、パスワードが一致しない場合は、メールが見つからない場合と同じブロックを入力して、ユーザーが見分けがつかないようにします
Bad login。どちらの場合も、の返信を送信します。 - 同様に、認証が失敗した場合、送信する適切なステータスコードは200ではなく401(未承認)です。
ユーザー名とパスワードのチェック方法は効率的ですか?
私には完全に正常に見えます。
ロギング:サーバーへのすべての接続とすべてのログイン試行をログに記録します。これは良い習慣ですか
ログを記録している場合、ログインの試行はログに記録する最も重要なことの1つです。しかしconsole.log、それを行う正しい方法ではありません。少なくとも単独ではありません。一部のユーザーが自分のログインについて心配していると言って、最近のログイン試行をどのように調べますか?Control-アプリケーションstdoutを介したF-ingは、それを行うための非常に管理しやすい方法ではありません。これの業界標準が何であるかはわかりませんが、ログデータベースに保存することを検討できます。
電子メールログインあなたはします:
const pw = req.body.password;
const submittedUser = req.body.email;
User.findOne({eMail: req.body.email}, async function (err, user) {
if (err) throw err;
console.log(user);
const match = await bcrypt.compare(pw, user.password);
console.log(match);
if (match === true && user.eMail == submittedUser) {
リクエストメールをという名前の変数に入れますがsubmittedUser、これは私にはそれほど直感的に聞こえません-ユーザーではなくメール文字列が含まれていることを示す変数名を使用する方が良いemailです-次のように両方を同時に分解できます:
const { password, email } = req.body;
その後、req.body再度実行するのではなく、これらの変数を使用します。
の後には.findOne、何もする必要はありません。呼び出しの制約をuser.eMail == submittedUser考えると、そのチェックは不要なはずfindOneです。
本文にemailはが含まれていますが、データベースにはが含まれていますeMail。大文字の使用は異なります。これは、タイプミスやバグを引き起こす可能性のある奇妙な矛盾です。単一のプロパティ名を使用することをお勧めします-おそらくemail、その大文字化がはるかに一般的であるためです。
一致をと比較するのではなく=== true、一致が真であるかどうかを確認できます。または、別のオプションとして、不必要なインデントを減らすには、一致が真でない場合にスローし、のエラーを処理し.catchます。
以来.sign、コールバックベースのですが、あなたが約束して仕事をしたい、それとの約束をベースとしますutil.promisify。
全体として、コードを次のようにしたいと思います。
const jwtSignPromisified = util.promisify(jwt2.sign).bind(jwt2);
app.post('/api/login', apiLimiter, async function (req, res) {
const failLogin = () => {
logLoginAttempt(email, false); // or something - 2nd param indicates success
// could also pass IP address
res.status(401).send('Bad login');
};
const { password, email } = req.body;
try {
const user = await User.findOne({ email });
if (!user) return failLogin();
const match = await bcrypt.compare(password, user.password);
if (!match) return failLogin();
logLoginAttempt(email, true);
const token = await jwtSignPromisified({ user }, 'secrettokenhere', { expiresIn: '15min' });
res.cookie(
'access_token', 'Bearer ' + token, {
//domain: 'localhost',
path: '/',
expires: new Date(Date.now() + 900000), // cookie will be removed after 15 mins
httpOnly: true // in production also add secure: true
})
.json(user);
} catch (error) {
// This should not be entered under normal circumstances:
logServerError(error);
res.status(500).send('Unexpected server error');
}
});
どこでlogLoginAttempt、logServerError検査できるロギングデータベースに保存します。