0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

打刻アプリにおける代返対策をやってみた話

Posted at

こんにちは。現在、私はあるイベント出席用の打刻アプリのサーバサイド開発・保守を担当しています。この記事では、近年増えてきた「代返行為(=出席の不正代行)」を防ぐために行った対策について、背景から実装、そして結果までを紹介します。


背景:なぜ代返が問題なのか?

この打刻アプリは、イベントの出席確認を目的に使用されています。仕組みとしては、

  1. イベント会場に設置されたビーコンの信号をスマホ端末が受信
  2. スマホが「位置情報」と「打刻時刻」をサーバに送信
  3. サーバが打刻情報を記録

といった流れです。

ところが最近、代返(=別人が他人のアカウントで打刻する行為)が増加してきました。主な手口は次のようなものです:

イベントに出席している学生が、出席していない知人のアカウントに切り替えて、代理で打刻する。


対策方針:「1端末1アカウント」の制限

代返を防ぐには、「1台の端末に1アカウントしかログインできない」ようにすれば良いと考えました。この方針に基づいて、次の2案を検討・実装しました。


案1:サーバ側で端末識別子を検証(主にAndroid向け)

方法

  1. 端末から送られてくるリクエストに「端末識別子(identifier)」を追加してもらう。
  2. サーバは、その識別子とユーザーIDのペアをRedisに保存。
  3. 打刻時に、同じ識別子で過去に記録されたユーザーIDと異なる場合、切り替えが発生したと判断。

技術的な補足

  • AndroidではWidevine IDという端末固有の識別子を利用できます。
  • Redisに有効期限(TTL)を1週間設定することで、「中古スマホを譲渡した場合」などにも対応可能です。
  • Widevine IDはセンシティブな情報なので、ヘッダーで送信したほうが無難です。

案2:クライアント(iOS)側で切り替え検知

方法

  1. 初回ログイン時に、ユーザーIDと日時を端末内に保存(localStorageなど)。
  2. 打刻時には保存されたユーザー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週間以上前なら上書き。それ以外は拒否

結果と感想

今のところは、代返の抑止に一定の効果が出ており、安定して動作しています
ただ、私自身もイベントに参加していて、もう自分の代返はできなくなりました(当たり前ですが…)。


最後に

今回の対策は、「完璧な防止策」ではありません。ですが、代返のハードルを上げることで、不正を大幅に減らすことはできます。

「不正に厳しくしつつ、正規ユーザーにはできる限り不便をかけない」そんなバランスを目指して、今後も改善を続けていきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?