IpRiskDetection.php 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. <?php
  2. namespace App\Jobs;
  3. use App\Services\IpRiskService;
  4. use Illuminate\Bus\Queueable;
  5. use Illuminate\Contracts\Queue\ShouldQueue;
  6. use Illuminate\Foundation\Bus\Dispatchable;
  7. use Illuminate\Queue\InteractsWithQueue;
  8. use Illuminate\Queue\SerializesModels;
  9. use Illuminate\Support\Facades\Log;
  10. use Illuminate\Support\Facades\Redis;
  11. /**
  12. * IP风险检测异步任务
  13. *
  14. * 在用户注册或登录成功后异步执行,检测用户IP是否存在风险:
  15. * 1. 如果用户已在 Redis Hash 中有记录且 IP 未变 → 跳过
  16. * 2. IP 变更后重新检测
  17. * 3. 检测到风险 → 写入 Redis Hash: ip_risk_users → {UserID} → {可疑IP}
  18. * 4. 未检测到风险 → 从 Hash 中移除(如果之前存在)
  19. */
  20. class IpRiskDetection implements ShouldQueue
  21. {
  22. use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
  23. /**
  24. * Redis Hash key
  25. */
  26. const REDIS_HASH_KEY = 'ip_risk_users';
  27. /**
  28. * 上次检测 IP 缓存 —— 按日分片 Hash,3 天自动过期
  29. * key: ip_risk_last_detected:20260507
  30. * field: UserID
  31. * value: IP
  32. * 每天一个 Hash,查过去 3 天的记录,无需大量独立 key
  33. */
  34. const LAST_DETECTED_PREFIX = 'ip_risk_last_detected:';
  35. /**
  36. * 每次检测覆盖的天数(含当天)
  37. */
  38. const DETECTION_WINDOW_DAYS = 3;
  39. /**
  40. * @var int 用户ID
  41. */
  42. protected $userId;
  43. /**
  44. * @var string 待检测的 IP 地址
  45. */
  46. protected $ip;
  47. /**
  48. * Create a new job instance.
  49. *
  50. * @param int $userId
  51. * @param string $ip
  52. */
  53. public function __construct($userId, $ip)
  54. {
  55. $this->userId = $userId;
  56. $this->ip = $ip;
  57. }
  58. /**
  59. * 获取过去 N 天的日期 key 列表(当天在最前)
  60. *
  61. * @param int $days
  62. * @return array
  63. */
  64. private function getRecentDateKeys($days = 3)
  65. {
  66. $keys = [];
  67. for ($i = 0; $i < $days; $i++) {
  68. $keys[] = self::LAST_DETECTED_PREFIX . date('Ymd', strtotime("-{$i} days"));
  69. }
  70. return $keys;
  71. }
  72. /**
  73. * Execute the job.
  74. */
  75. public function handle()
  76. {
  77. $userId = $this->userId;
  78. $ip = $this->ip;
  79. if (empty($userId) || empty($ip)) {
  80. return;
  81. }
  82. // 1. 检查过去 3 天的每日 Hash 中是否已检测过同一 IP
  83. // 只要任意一天记录相同 IP → 跳过(IP 未变)
  84. $recentKeys = $this->getRecentDateKeys(self::DETECTION_WINDOW_DAYS);
  85. $todayKey = $recentKeys[0];
  86. $ipUnchanged = false;
  87. foreach ($recentKeys as $key) {
  88. $storedIp = Redis::hget($key, $userId);
  89. if ($storedIp === $ip) {
  90. $ipUnchanged = true;
  91. break;
  92. }
  93. }
  94. if ($ipUnchanged) {
  95. // IP 在检测窗口内未变,跳过
  96. return;
  97. }
  98. // 2. 执行风险检测
  99. $service = new IpRiskService();
  100. $result = $service->detect($ip);
  101. // 3. 根据检测结果更新风险标记 Hash
  102. if ($result['is_risky']) {
  103. $value = $ip . '|' . $result['reason'];
  104. Redis::hset(self::REDIS_HASH_KEY, $userId, $value);
  105. Log::info("IpRiskDetection: user flagged", [
  106. 'user_id' => $userId,
  107. 'ip' => $ip,
  108. 'reason' => $result['reason'],
  109. ]);
  110. }
  111. // 4. 记录本次检测结果到今天的 Hash,并确保 3 天过期
  112. $isNew = !Redis::exists($todayKey);
  113. Redis::hset($todayKey, $userId, $ip);
  114. if ($isNew) {
  115. Redis::expire($todayKey, 86400 * self::DETECTION_WINDOW_DAYS);
  116. }
  117. }
  118. }