WebSocket レッスン4
認証とエラー処理
接続時の認証、再接続戦略、エラーハンドリングを学ぼう
JWT認証で接続を保護する
WebSocket接続は長時間維持されるため、接続時に認証を 行うことが重要です。JWTトークンを使って認証する一般的なパターンを見ていきましょう。
// ===== クライアント側 =====
import { io } from "socket.io-client";
// JWTトークンを接続時に送信
const socket = io("http://localhost:3001", {
auth: {
token: localStorage.getItem("jwt_token"),
},
});
// 認証エラーの処理
socket.on("connect_error", (error) => {
if (error.message === "authentication_error") {
console.error("認証に失敗しました。再ログインしてください");
// ログインページにリダイレクト
window.location.href = "/login";
}
});Socket.IOミドルウェア
Socket.IOのミドルウェアを使うと、 接続の前にJWTの検証やログ記録などの処理を差し込めます。 Expressのミドルウェアと同じ考え方です。
import jwt from "jsonwebtoken";
// 認証ミドルウェア
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error("authentication_error"));
}
try {
// JWTを検証
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// ソケットにユーザー情報を付与
socket.data.user = decoded;
next();
} catch (err) {
next(new Error("authentication_error"));
}
});
// ログ記録ミドルウェア
io.use((socket, next) => {
console.log(`接続試行: ${socket.handshake.address}`);
console.log(`ユーザー: ${socket.data.user?.name}`);
next();
});
// 認証済みユーザーの接続処理
io.on("connection", (socket) => {
const user = socket.data.user;
console.log(`認証済み接続: ${user.name} (${user.id})`);
// ユーザー固有のルームに自動参加
socket.join(`user:${user.id}`);
socket.on("chat:message", (data) => {
// socket.data.user から認証済みユーザー情報を使える
io.emit("chat:message", {
...data,
user: user.name,
userId: user.id,
});
});
});再接続戦略
ネットワークの不安定さに対応するため、再接続戦略を適切に設定することが重要です。
const socket = io("http://localhost:3001", {
// 再接続の設定
reconnection: true, // 自動再接続を有効化
reconnectionAttempts: 10, // 最大試行回数
reconnectionDelay: 1000, // 初回の待機時間(ミリ秒)
reconnectionDelayMax: 30000, // 最大待機時間(ミリ秒)
randomizationFactor: 0.5, // ランダム化係数
// タイムアウト設定
timeout: 10000, // 接続タイムアウト
});
// 再接続イベント
socket.io.on("reconnect_attempt", (attempt) => {
console.log(`再接続試行 ${attempt} 回目...`);
// トークンの更新が必要な場合
socket.auth.token = getLatestToken();
});
socket.io.on("reconnect", (attempt) => {
console.log(`再接続成功!(${attempt} 回目で成功)`);
// 再接続後にデータを再取得
socket.emit("sync:request", {
lastEventId: getLastEventId(),
});
});
socket.io.on("reconnect_failed", () => {
console.error("再接続に失敗しました");
showReconnectButton(); // 手動再接続ボタンを表示
});
// 手動で再接続
function manualReconnect() {
socket.connect();
}エラーハンドリング
WebSocket通信では様々なエラーが発生し得ます。適切なエラーハンドリングで ユーザー体験を損なわないようにしましょう。
// ===== クライアント側のエラーハンドリング =====
// 接続エラー
socket.on("connect_error", (error) => {
switch (error.message) {
case "authentication_error":
handleAuthError();
break;
case "rate_limit":
handleRateLimit();
break;
default:
console.error("接続エラー:", error.message);
}
});
// サーバーからのエラーイベント
socket.on("error", (data) => {
console.error("サーバーエラー:", data.message);
showErrorNotification(data.message);
});
// ===== サーバー側のエラーハンドリング =====
io.on("connection", (socket) => {
socket.on("chat:message", (data, callback) => {
try {
// バリデーション
if (!data.text || data.text.length > 1000) {
throw new Error("メッセージが不正です");
}
// 処理実行
const result = processMessage(data);
callback({ status: "ok", result });
} catch (error) {
// エラーを送信者に通知
callback({ status: "error", message: error.message });
// ログに記録
console.error(`エラー [${socket.id}]: ${error.message}`);
}
});
// 予期しないエラーのキャッチ
socket.on("error", (error) => {
console.error(`ソケットエラー [${socket.id}]:`, error);
});
});ハートビート(Ping/Pong)
ハートビートは、 接続が生きているかどうかを定期的に確認する仕組みです。 Socket.IOには自動ハートビートが組み込まれていますが、カスタマイズも可能です。
// ===== サーバー側の設定 =====
const io = new Server(httpServer, {
// ハートビート設定
pingInterval: 25000, // Pingを送信する間隔(ミリ秒)
pingTimeout: 20000, // Pongの応答を待つタイムアウト
});
// ===== カスタムハートビート =====
io.on("connection", (socket) => {
let isAlive = true;
// クライアントからのPongを受信
socket.on("pong:custom", () => {
isAlive = true;
});
// 定期的にPingを送信
const heartbeat = setInterval(() => {
if (!isAlive) {
console.log(`応答なし: ${socket.id} を切断`);
socket.disconnect(true);
return;
}
isAlive = false;
socket.emit("ping:custom");
}, 30000);
socket.on("disconnect", () => {
clearInterval(heartbeat);
});
});
// ===== クライアント側 =====
socket.on("ping:custom", () => {
socket.emit("pong:custom");
});まとめ
- WebSocket接続時にJWTトークンで認証し、不正なアクセスを防ぐ
- Socket.IOミドルウェアで認証・ログ記録などの共通処理を差し込める
- 再接続戦略を適切に設定し、ネットワーク不安定時のUXを向上させる
- クライアント・サーバー両方でエラーハンドリングを実装する
- ハートビートで接続の生存確認を行い、切断を検知する