1. はじめに
ウェブアプリケーションの開発において、ユーザー体験を向上させるためには、認証の管理が重要です。今回は、ログインしていないユーザーでも投稿詳細を確認できるようにし、ログインしていないユーザー保存や応募ボタンを押すとログインを促す実装を行いました。この記事では、その際起きたエラーとそれに対するアプローチについて解説します。
2. 何をしたかったか
-
ログインしていないユーザーでも投稿詳細を確認できるようにする
- ユーザーがアプリケーションにログインしていなくても、投稿の詳細情報を閲覧できるようにしたい。
-
保存や応募ボタンを押すとログインを促す
- ユーザーが投稿を保存したり、応募したりするボタンを押した際に、ログインしていない場合はログインページにリダイレクトして、ログインを促すようにしたい。
これにより、ユーザーはログイン前に投稿の内容を確認でき、必要な操作を行う際に適切にログインを促されることで、よりスムーズで安全なユーザー体験を提供することができます。
3. セキュリティの考慮
認証に関する処理を適切に行うことは、アプリケーションのセキュリティを確保するために非常に重要です。
1. 認証のスキップと対象の限定
- 認証をスキップする場合、対象を限定することが重要です。すべてのアクションに対して認証をスキップすると、セキュリティリスクが高まります。必要なアクションにのみ認証をスキップし、他のアクションには適切な認証を行うようにしましょう。
2. トークンの有効期限と管理
- トークンの有効期限を適切に設定し、トークンが期限切れになった場合には再認証を要求するようにします。また、トークンの管理方法にも注意が必要で、クライアント側での保管にはセキュアな方法を採用しましょう。
3. エラーメッセージ・ログの重要性
- 認証エラーが発生した場合には、適切にエラーハンドリングを行い、ユーザーに対して適切なフィードバックを提供します。特に、セキュリティに関するエラーは詳細な情報をユーザーに提供せず、一般的なメッセージで対処することが推奨されます。
4. HTTPSの利用
- 認証情報のやり取りを行う際には、通信を暗号化するためにHTTPSを使用することが必須です。これにより、第三者による盗聴や改ざんを防ぐことができます。
4. 発生したエラーと原因
まず何をしたか
- Railsコントローラーで、投稿詳細の取得に関するアクションに対して認証をスキップする設定を行いました。
skip_before_action :authenticate_user, only: [:show]
しかし、、、
backend-1 | Started GET "/api/applications/check?recruitment_id=5" for 192.168.65.1 at 2024-07-09 03:04:27 +0000
backend-1 | Processing by Api::ApplicationsController#check as HTML
backend-1 | Parameters: {"recruitment_id"=>"5"}
backend-1 | JWT::ExpiredSignature: Signature has expired
backend-1 | [active_model_serializers] Rendered ActiveModel::Serializer::Null with Hash (0.12ms)
backend-1 | Filter chain halted as :authenticate_user rendered or redirected
backend-1 | Completed 401 Unauthorized in 12ms (Views: 2.2ms | ActiveRecord: 0.0ms | Allocations: 227)
投稿詳細画面は表示されませんでした。
原因:
投稿詳細の取得時に認証をスキップする設定を行ったものの、関連する他の処理(例:応募の確認)が認証を必要とし、認証エラーが発生していました。このため、投稿詳細画面を閲覧できない状況が続いていました。
次に何をしたか
- サーバー側では認証をスキップして、クライアント側で必要なアクション(応募や保存など)を実行する際に認証を行う、ログインしていない場合はログインページにリダイレクトする処理を追加しました。
サーバー側の変更:
before_action :authenticate_user, except: [:check]
# 省略
# GET /applications/check?recruitment_id=:recruitment_id
def check
if @current_user
recruitment = Recruitment.find(params[:recruitment_id])
is_applied = recruitment.applications.exists?(user: @current_user)
render json: { is_applied: is_applied }
else
render json: { is_applied: false }
end
end
クライアント側の変更:
const handleApply = () => {
const token = localStorage.getItem('token');
if (!token) {
toast({
title: 'ログインが必要です',
description: 'この操作を行うにはログインしてください。',
status: 'warning',
duration: 3000,
isClosable: true,
});
router.push('/login'); // ログインページへのリダイレクト
return;
}
axios.post(`${apiUrl}/applications`, { recruitment_id: id }, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
})
.then(() => {
setIsApplied(true);
toast({
title: '応募が完了しました!',
status:'success',
duration: 3000,
isClosable: true,
});
})
.catch(error => {
console.error("There was an error applying for the recruitment!", error);
toast({
title: '応募中にエラーが発生しました。',
status: 'error',
duration: 3000,
isClosable: true,
});
});
};
公開情報(投稿詳細など)と認証が必要なアクションを適切に分離 することで、ユーザーがログインしていなくても投稿詳細を確認でき、保存や応募の際にはログインを促すことができるようになりました。
これで完成! と思ったが、、、
新たに発生したエラー
ログインしていない状態で応募または保存ボタンを押すと '応募中にエラーが発生しました' と表示されてしまいました。
backend-1 | Started POST "/api/applications" for 192.168.65.1 at 2024-07-09 03:31:22 +0000
backend-1 | Processing by Api::ApplicationsController#create as HTML
backend-1 | Parameters: {"recruitment_id"=>"5", "application"=>{"recruitment_id"=>"5"}}
backend-1 | JWT::ExpiredSignature: Signature has expired
backend-1 | [active_model_serializers] Rendered ActiveModel::Serializer::Null with Hash (0.24ms)
backend-1 | Filter chain halted as :authenticate_user rendered or redirected
backend-1 | Completed 401 Unauthorized in 10ms (Views: 4.7ms | ActiveRecord: 0.0ms | Allocations: 227)
ログを確認したところ、401エラーが発生していることが分かりました。このエラーは、認証情報が提供されていないか、提供されたトークンが無効である場合に発生するものです。
(これに気づくまで時間かかりました😅)
最終的な解決策
条件分岐を使用してトーストメッセージを表示し、ログインが必要な操作であれば、401エラーをキャッチして適切に対応するようにしました。
const handleApply = () => {
const token = localStorage.getItem('token');
if (!token) {
toast({
title: 'ログインが必要です',
description: 'この操作を行うにはログインしてください。',
status: 'warning',
duration: 3000,
isClosable: true,
});
router.push('/login'); // ログインページへのリダイレクト
return;
}
axios.post(`${apiUrl}/applications`, { recruitment_id: id }, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
})
.then(() => {
setIsApplied(true);
toast({
title: '応募が完了しました!',
status:'success',
duration: 3000,
isClosable: true,
});
})
.catch(error => {
if (error.response && error.response.status === 401) {
toast({
title: 'ログインが必要です',
description: 'この操作を行うにはログインしてください。',
status: 'warning',
duration: 3000,
isClosable: true,
});
router.push('/login'); // ログインページへのリダイレクト
} else {
toast({
title: '応募中にエラーが発生しました。',
status: 'error',
duration: 3000,
isClosable: true,
});
}
console.error("There was an error applying for the recruitment!", error);
});
これでログインしていないユーザーも投稿詳細画面を閲覧でき、認証が必要なアクションではログイン画面に遷移できるようになりました!
4.学んだこと
今回の実装を通じて、いくつか重要なポイントを学びました。
1. 認証の管理
- 投稿詳細の取得に関して認証をスキップすることで、ログインしていないユーザーでも投稿詳細を閲覧できるようにしました。一方で、認証が必要なアクションに対しては、しっかりと認証を行う必要があることを理解しました。どのアクションに対して認証をスキップし、どのアクションに対して認証を必須にするかを適切に判断することが重要です。
2. エラーメッセージ・ログの重要性
- 今回の "401 Unauthorized" のようにエラーが発生した際に、ログに出力されるエラーメッセージが非常に役立ちました。ログにより、どの部分でエラーが発生しているのかを迅速に特定し、適切な対応ができるようになりました。
3. エラーハンドリング
- ユーザーが操作を行う際に、スムーズに対応するための方法を学びました。具体的には、トーストメッセージを使用してユーザーにフィードバックを提供し、ログインが必要な操作に対して適切にログインを促すことで、ユーザーエクスペリエンスを向上させることができました。
.catch(error => {
if (error.response && error.response.status === 401) {
toast({
title: 'ログインが必要です',
description: 'この操作を行うにはログインしてください。',
status: 'warning',
duration: 3000,
isClosable: true,
});
router.push('/login'); // ログインページへのリダイレクト
} else {
toast({
title: '応募中にエラーが発生しました。',
status: 'error',
duration: 3000,
isClosable: true,
});
}
console.error("There was an error applying for the recruitment!", error);
});
4. セキュリティの考慮
- 認証に関する処理を適切に行うことで、アプリケーションのセキュリティを維持することができました。特に、ユーザーの認証情報が有効であるかどうかを確認し、無効なトークンに対して適切にエラーハンドリングを行うことが重要です。
このように、今回の実装を通じて、認証の管理、エラーメッセージ・ログの重要性、ユーザーエクスペリエンスの向上について学ぶことができました。これらの知見を、今後の開発に役立てていきたいです。