こんにちは。現在、私はあるイベント出席用の打刻アプリのサーバサイド開発・保守を担当しています。この記事では、近年増えてきた「代返行為(=出席の不正代行)」を防ぐために行った対策について、背景から実装、そして結果までを紹介します。
背景:なぜ代返が問題なのか?
この打刻アプリは、イベントの出席確認を目的に使用されています。仕組みとしては、
- イベント会場に設置されたビーコンの信号をスマホ端末が受信
- スマホが「位置情報」と「打刻時刻」をサーバに送信
- サーバが打刻情報を記録
といった流れです。
ところが最近、代返(=別人が他人のアカウントで打刻する行為)が増加してきました。主な手口は次のようなものです:
イベントに出席している学生が、出席していない知人のアカウントに切り替えて、代理で打刻する。
対策方針:「1端末1アカウント」の制限
代返を防ぐには、「1台の端末に1アカウントしかログインできない」ようにすれば良いと考えました。この方針に基づいて、次の2案を検討・実装しました。
案1:サーバ側で端末識別子を検証(主にAndroid向け)
方法
- 端末から送られてくるリクエストに「端末識別子(identifier)」を追加してもらう。
- サーバは、その識別子とユーザーIDのペアをRedisに保存。
- 打刻時に、同じ識別子で過去に記録されたユーザーIDと異なる場合、切り替えが発生したと判断。
技術的な補足
- AndroidではWidevine IDという端末固有の識別子を利用できます。
- Redisに有効期限(TTL)を1週間設定することで、「中古スマホを譲渡した場合」などにも対応可能です。
- Widevine IDはセンシティブな情報なので、ヘッダーで送信したほうが無難です。
案2:クライアント(iOS)側で切り替え検知
方法
- 初回ログイン時に、ユーザーIDと日時を端末内に保存(localStorageなど)。
- 打刻時には保存されたユーザーIDと照合し、異なる場合は打刻を送信しない。
技術的な補足
- iOSは、アプリをアンインストールしても削除されない領域(Keychainなど)を使うことで、情報を持続的に保持できます。
- Androidはユーザー側で比較的簡単にlocalStorageのデータを削除できてしまうため、この方法は不向きです。
実装概要
条件
以下のユースケースも想定して、対策を設計しました:
- 複数端末でログインしたい人:端末識別子ごとにユーザーIDを記録すればOK
- スマホを譲渡された人:1週間以上ログインや打刻操作がなければ、切り替えを許可
サーバ側のコード(TypeScript + Redis)
1. 打刻時に識別子とユーザーIDを紐づける
public async updateAccessInfo(identifier: string | string[] | undefined, userObjectId: string): Promise<void> {
if (!this.isIdentifierValid(identifier)) return;
const identifierString = identifier as string;
const currentUserObjectId = await this.redisService.getUserIdByIdentifier(identifierString);
if (currentUserObjectId && currentUserObjectId !== userObjectId) {
console.warn('アカウント切り替え検出:', { identifier, userObjectId, currentUserObjectId });
return;
}
// 一週間(604800秒)の有効期限付きで保存
await this.redisService.updateIdentifierValue(identifierString, userObjectId);
}
2. ログイン時に切り替えをチェック
public async checkAccessInfo(
identifier: string | string[] | undefined,
userObjectId: string,
): Promise<{ result: boolean }> {
const identifierString = identifier as string;
try {
const currentUserObjectId = await this.redisService.getUserObjectIdByIdentifier(identifierString);
if (!currentUserObjectId) return { result: true }; // 初回や期限切れならOK
return { result: currentUserObjectId === userObjectId };
} catch (error) {
console.error('Redis query failed:', error);
return { result: true }; // Redisが落ちていても通す(誤検出防止)
}
}
iOS側の挙動
- ローカルに「最初にログインしたユーザーID」と「日付」を保存
- 打刻時に照合。異なる場合、日付が1週間以上前なら上書き。それ以外は拒否
結果と感想
今のところは、代返の抑止に一定の効果が出ており、安定して動作しています。
ただ、私自身もイベントに参加していて、もう自分の代返はできなくなりました(当たり前ですが…)。
最後に
今回の対策は、「完璧な防止策」ではありません。ですが、代返のハードルを上げることで、不正を大幅に減らすことはできます。
「不正に厳しくしつつ、正規ユーザーにはできる限り不便をかけない」そんなバランスを目指して、今後も改善を続けていきます。