【Django】同一人物による工作(再生数の水増しなど)をいかにして防ぐか、方法と対策【UniqueConstraint,Recaptcha,UA,IPアドレス等】
例えば、動画サイトを運営していて、動画の再生回数を記録する機能を実装したとする。
動画の検索で、再生数の多い順に並び替えて表示する機能が既にある場合、再生回数の水増しによって、全く面白くない動画が検索の上位に表示されるなどの問題が発生する。
それだけでなく、再生回数の水増しを放置してしまうと、ランキングも荒れ果て、結果的にユーザーが離反してしまう恐れもある。(※ただし、ランキングの集計方法によってある程度は対策は可能)
よって、公正で平等なサイト運営を行うためにも、このような不正行為の対策は確実に行わなければならない。本記事ではDjangoを使用して、方法論と具体的なコードを元に解説を行う。
状況(動画共有サイトの再生回数記録)
動画の個別サイトにアクセスすればVideoモデルクラスのviewsフィールドに数値が1加算される形式である。これでは同一人物でもF5を連打するだけで簡単に水増しできる。
class Video(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
#===========省略=====================
views = models.IntegerField(verbose_name="再生回数",default=0,validators=[MinValueValidator(0)])
def __str__(self):
return self.title
そこで、viewsを多対多モデルに仕立て、中間テーブルとして、VideoViewモデルクラスを作る。
class Video(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
#===========省略=====================
view = models.ManyToManyField(settings.AUTH_USER_MODEL,verbose_name="再生",through="VideoView",related_name="video_view")
def __str__(self):
return self.title
多対多の中間テーブルに当たるVideoViewモデルが下記。
class VideoView(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
date = models.DateField(verbose_name="再生日")
target = models.ForeignKey(Video,verbose_name="再生する動画",on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL,verbose_name="再生した人",on_delete=models.CASCADE,null=True,blank=True)
ip = models.GenericIPAddressField(verbose_name="再生した人のIPアドレス")
再生日、再生対象の動画、再生した人のアカウントのユーザーID、リクエストを送信した(再生した)人のIPアドレスを記録する。ユーザーIDに関しては、未ログインユーザでも再生回数にカウントする必要があるため、null可blank可として、空欄入力を許可した。
参照:Djangoで数値型もしくはUUID型等のフィールドに、クライアント側から未入力を許可するにはnull=Trueとblank=Trueのオプションを
問題は、このフィールドのどれにユニーク制約を付与して、同一人物による工作を拒否すれば良いのかという問題である。
起こりうる再生数水増しのパターン
ユニーク制約を付与を考える前に、まずは起こりうる水増しのパターンを考える。
いずれも同一人物による工作とする。
- パターン1:複数のアカウントで、同一のIPアドレス
- パターン2:同一のアカウントで、複数のIPアドレス
- パターン3:複数のアカウントで、複数のIPアドレス
パターン1:複数のアカウントで、同一のIPアドレス
一人で複数のアカウントを作っているパターン。この場合はIPアドレスを元にして判定を行えば良い。
また、アカウント作成時にRecaptchaを用意して、ボットなどを使用して簡単に作れないようにする対策が有効である。
パターン2:同一のアカウントで、複数のIPアドレス
プロキシサーバーなどを悪用しているパターン。アカウントは同じなので判定は容易。
ただし、プロバイダから割り当てられるグローバルIPアドレスが変わっているパターン、携帯電話回線や公衆無線LANを経由しているパターン等もあるため、全てが工作行為であるとは言えない。
ユーザーモデルにユーザーエージェントやIPアドレスを記録するフィールドを用意するのも有効であると思う。UAやIPがいつもと違っていればメールで通知するなど、不正アクセスされた際の対策にもなる。
パターン3:複数のアカウントで、複数のIPアドレス
複数のアカウントを使用し、複数のプロキシサーバーなどを経由して行われているパターン。
ここまで来ると、対策は非常に困難。正常な動画再生と見分けが付かないので、下手な対策をしてしまうと、正常利用者にも影響を及ぼす。
一定期間当たりに不自然な再生回数の向上が見られた場合に手動で対策をするなど、柔軟な対策が必要と思われる。それでもSNSでバズった際など急激に再生回数が上昇する可能性もあるため、判断はとてもむずかしい。
【余談】IPアドレスを元にして判定する問題
プロバイダから割り当てられているグローバルIPアドレスは、基本的に一定期間(リース期間)を経過すると切り替わってしまう。故に、IPアドレスだけで判定を行うのは簡単ではあるものの、完全とは言い難い。
例えば、Aさんが動画にアクセスして、数分後にグローバルIPアドレスが変化し、もう一度同じ動画にアクセスすると、2回再生されたことになる。逆に、1度動画にアクセスされたAさんのIPアドレスがBさんに割り振られた状態で、Bさんが動画にアクセスしても、既に再生したことになるため再生回数にカウントされない。
とは言え、この状況が起こりうるのは極めて稀と思われる。プロバイダにもよるが、グローバルIPアドレスが変わるタイミングは1日から3日、1ヶ月から3ヶ月、全く変わらないという場合もある。
よって、この影響によって発生する再生回数の誤差は軽微で無視できるレベルと思われる。
複合ユニーク制約にするフィールドは?
ここで、date,target,user,ip
の内、どれを複合ユニーク制約に仕立てればよいかが問題である。
結論から言うと、date,target,user
とdate,target,ip
の2通りの複合ユニーク制約が有効である。つまり、複数の複合ユニーク制約である。
まず、アクセスした人全てに必ずIPアドレスが付与される。そのため、IPアドレスを元にして複合ユニーク制約を作る。
class VideoView(models.Model):
class Meta:
# unique_together はDjango 2.2から非推奨
#unique_together = ("target","date","ip")
# constraints を使う
constraints = [
models.UniqueConstraint(fields=['target', 'date', 'ip'], name='unique_target_date_ip')
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
#↓ バリデーション時ユニーク判定に失敗するので、再生日はビューが代入すること。
date = models.DateField(verbose_name="再生日")
target = models.ForeignKey(Video,verbose_name="再生する動画",on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL,verbose_name="再生した人",on_delete=models.CASCADE,null=True,blank=True)
ip = models.GenericIPAddressField(verbose_name="再生した人のIPアドレス")
これで特定のIPアドレスは一日に一回しか特定の動画の再生回数がカウントされない。
しかし、これで問題は解決ではない。このIPアドレスの複合ユニーク属性は前項のパターン1に対しては有効であるが、パターン2の複数のIPアドレスを使用した方法には無効。
そこで、userを含めた複合ユニーク制約を作る。ipを含めたものと両立させる必要があるので、複数の複合ユニーク制約になる。
class VideoView(models.Model):
class Meta:
#unique_together = (("target","date","user"),("target","date","ip"))
constraints = [
models.UniqueConstraint(fields=['target', 'date', 'user'], name='unique_target_date_user'),
models.UniqueConstraint(fields=['target', 'date', 'ip'], name='unique_target_date_ip'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
#↓ バリデーション時ユニーク判定に失敗するので、再生日はビューが代入すること。
date = models.DateField(verbose_name="再生日")
target = models.ForeignKey(Video,verbose_name="再生する動画",on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL,verbose_name="再生した人",on_delete=models.CASCADE,null=True,blank=True)
ip = models.GenericIPAddressField(verbose_name="再生した人のIPアドレス")
これで、工作に対してある程度は対策できる。流石にパターン3の対策には不十分だが。
ちなみに、date,target,user,ip
の4つの複合ユニーク制約を1つ作る方法は誤りである。userとipが一緒くたになってしまうと、会員ユーザーがログアウトした上で、もう一度動画にアクセスすれば、いずれもユニーク扱いになるからだ。
再生回数水増し工作の対策全般の方法論
おそらく前項の複数の複合ユニーク制約を付与しただけでは、まだ工作対策としては不十分かと思われる。
そこで、他の対策もここでまとめる。下記の対策は不正アクセス等のセキュリティ対策も兼ねることができる。
- 対策1:ユーザーモデルにユーザーエージェントとIPアドレスを記録するフィールドを追加
- 対策2:新規アカウント作成フォームにRecaptchaを設置
- 対策3:一定期間当たりに不自然な再生数の増加を確認したらメールで通知
- 対策4:プロキシサーバーやTorを使用したアクセスの禁止
- 対策5:アクセス時、ボットではないと判定されれば一定期間有効なセッションを配布
対策1:ユーザーモデルにユーザーエージェントとIPアドレスを記録するフィールドを追加
まず、ユーザーエージェントとIPアドレスをユーザーモデルと紐付けることで、同一人物による複数アカウントの作成を見抜くことができる。
それからセキュリティ対策として、いつもとは違う端末(ユーザーエージェント)を使用したり、違うIPアドレスを使用したりしてログインをした場合、メールで警告を行うことで仮に不正アクセスされた場合、正規利用者はすぐに気づくことができる。
よくある、『登録されていない端末からログインが行われました、あなたはこの端末に見覚えがありますか』等の文言で、ログイン時にメールで通知が行くのは、ユーザーモデルとUA、IPアドレスが紐付いているから。
対策2:新規アカウント作成フォームにRecaptchaを設置
できれば、本格的に運用を開始するのであれば、新規アカウント作成フォームにはRecaptchaを設置したい。
Recaptchaを設置することで、ボットを使用したアカウント大量作成が一気に難しくなる。
また、アカウント大量作成の抑止によって、ソーシャルエンジニアリングの抑止にもつながる。
例えば、犯罪者が自分でアカウントを大量に作成したとする。その後、犯罪者は管理者にメールで警告をする。『あなたのサイトから顧客情報を頂いた。顧客情報を流出させたくなければ、指定口座に〇〇を支払え』というメッセージと同時に、犯罪者が先ほど大量に作成したアカウント情報をCSVで添付する。自分で作ったアカウントなのだから、パスワード等の情報は知ってて当たり前だ。
しかし、事情を知らない一部の管理者は流出を恐れて、犯罪者に金銭を支払ってしまう可能性がある。
もちろん、これは手動でアカウント作成しても実現はできるが、大抵は効率重視であるため、よほどの怨恨でもない限り事案の発生をある程度は低減できると思われる。
ただし、この対策をすると、アカウントを持っていない一般利用者は気軽にアカウントを作成しづらくなるため、利用者拡大に悪影響が及ぶだろう。
対策3:一定期間当たりに不自然な再生数の増加を確認したらメールで通知
常駐スクリプトを作成して、一定期間当たりに一定の再生数の増加を確認したら、管理者にメールで通知する。
爆発的な再生数増加の原因は、SNSでバズったか、炎上したか、あるいは工作を行ったかのいずれかである。
負の要素が多い反面、少なからず正の要素もあるため、通知だけ自動で行い、措置は手動で行う。
『一定期間』『一定回数』をいじれるように管理サイトから設定できるようにすると尚良いだろう。
ただ、常駐スクリプトは運用面でもコストがかかる点を考慮しておいたほうがよい。
対策4:プロキシサーバーやTorを使用したアクセスの禁止
プロキシサーバーやTor(多段プロキシ)を使用したアクセスを禁止することで工作の効果をある程度は低減できると思われる。
特にTorを使用したアクセスは、よほどデリケートなテーマを扱ったサイトでもない限りメリットが薄いので、拒否しておくのが定石。具体的な方法は検索すれば出てくる。
一般的なプロキシサーバーの拒否に関しては方法論が確立されていない。
海外からのアクセスを拒否するなどの方法があるが、海外プロキシサーバー経由ではない海外一般利用者も一括で拒否されてしまう上、日本国内のプロキシサーバーを使われてしまえば意味がない。プロキシサーバーのグローバルIPアドレスを指定しても一定期間ごとに変化してしまうため、効果は限られると思われる。
リクエストからプロキシサーバーを使用したかどうかを判定できるようだが、効果は不明。
対策5:アクセス時、ボットではないと判定されれば一定期間有効なセッションを配布
通常アクセス時、まずRecaptchaなどを使用してボットであるかどうかを判定する。その上で、ボットではないと判定されれば一定期間有効のセッションを配布する。もし、ボットであれば別ページへリダイレクトする等の処理を実行。
ログイン時だけでなく、他のページにもボット判定を追加することでさらに工作対策を強化した状態。
結論
ただの再生回数水増し対策ではあるが、それがセキュリティ対策にもなることがよくわかる。
特にユーザーモデルへのIPアドレスとUAの記録、新規アカウント作成フォームにRecaptchaもセットで配置することで、工作や不正アクセス等の抑止、早期解決につながるだろう。