PHP 业务安全架构:防刷、风控与数据安全的架构级落地实践

  下面把这件事拆成 10 个台阶,每一阶给「干什么」+「完整代码/配置」+「大白话」。目标:让一个 PHP
  业务系统从"被刷光优惠券都不知道"进化到"实时风控、自动拦截、数据全程脱敏可审计"---
  一、先讲清动机(为什么业务安全是另一种"安全")

  传统 Web 安全(技术安全)         业务安全(本系列重点)
  ─────────────────────────       ─────────────────────────
  SQL 注入、XSS、CSRF              黑产羊毛党、薅优惠券、刷单
  代码层防御                       业务规则层防御
  工具能扫出来                     需要建模 + 实时决策
  WAF 拦得住                       WAF 拦不住,看着像正常用户

  业务安全的核心命题:攻击者用合法的请求做出有害的事。每个 IP 都是真的、每次请求都符合协议、Header 都对——但100
  个账号薅同一张优惠券,这就是业务攻击。

  真实事故案例:

  2023年某新零售公司:
    上线 1 元秒杀活动
    10 分钟被脚本党扫光 5 万件
    追溯发现:同一设备指纹,500+ 账号
    损失:500 万 GMV + 品牌信任

  2024年某拼团 App:
    邀新返现活动
    脚本批量注册 + 互邀
    3 天被薅 12002025年某金融公司:
    实名认证接口被打
    攻击者撞库 + 接码平台
    泄漏 80 万实名信息 + 一年合规整改

  大白话:技术安全是**「防黑客拿权限」,业务安全是「防黑产薅羊毛 + 防数据泄漏」。前者写代码就能挡,后者必须「业务规则 +
  实时风控 + 数据全程加密」**三位一体——这是2026 年互联网产品的生死线。

  ---
  二、整体地图(10 阶段全景)

  ┌──────────────────────────────────────────────────────┐
  │ 第10:对抗演练与持续运营(红蓝对抗 / 漏洞奖励)     │
  │ 第9:数据安全(传输/存储/脱敏/审计)                │
  │ 第8:业务监控与应急响应(指标 / 告警 / 处置)       │
  │ 第7:风控决策引擎(规则引擎 + 模型)                │
  │ 第6:行为分析与画像(设备 + 行为 + 关系)           │
  │ 第5:身份与设备指纹(IP / UA / 设备 ID)            │
  │ 第4:防刷三件套(限流 / 验证码 / 幂等)             │
  │ 第3:接口加固(签名 / 时间戳 / nonce)              │
  │ 第2:威胁建模(STRIDE / 业务流量画像)              │
  │ 第1:心智模型(攻击面 / 攻防经济学)                │
  └──────────────────────────────────────────────────────┘

  ---
  三、第 1:心智模型(理解攻防经济学)

  黑产为什么打你

  黑产成本 < 黑产收益 →你就是目标
  黑产成本 > 黑产收益 →黑产换地方

  业务安全的本质 = 不断抬高黑产成本,直到他不划算为止

  攻防成本计算公式

  攻击者成本 = 账号成本 + 设备成本 + IP 成本 + 时间成本

  防御目标:让攻击者成本接近"诚实用户成本"

  例子:1元秒杀
    - 老老实实买:成本 1- 脚本刷    :成本(账号 0.5+IP 0.1+设备 0.05)= 0.65- 收益      :商品 100- 利润      :99.35元 →划算,大干特干

  防御后:
    - 黑产单次成本拉到 50(实名 + 高级设备指纹 + 风控)
    - 利润降到 50 元 →还行,但不爆
    - 再拉到 80 元 →利润 20 元 →不划算了

  攻击的 4 个典型类型

  1. 撞库 / 暴力破解
     用泄漏的密码库尝试登录
     防:MFA + 风控 + 滑动验证

  2. 薅羊毛
     多账号领取限领的福利
     防:设备指纹 + 行为分析 + 实名

  3. 爬虫 / 数据偷取
     批量抓取商品 / 价格 / 用户数据
     防:接口签名 + 限流 + 数据投毒

  4. 接口刷量(刷单 / 刷评论 / 刷阅读)
     伪造用户行为
     防:行为序列分析 + 关系图谱

  攻击面盘点(写下来)

  ✅ 必须列出:
    - 所有公开接口
    - 所有"涉及钱"的接口(优惠、积分、提现)
    - 所有"涉及实名"的接口
    - 所有"涉及好友关系"的接口
    - 所有"被爬重灾区"(商品列表、价格)

  ✅ 标注每个接口:
    - 风险等级(//)
    - 现有防御措施
    - 业务影响(被刷一次损失多少)

  大白话:业务安全是经济学,不是技术学。你不可能把黑产挡死(他们专业全职打这个),你能做的是让他们"不划算"。第一步先把家底盘
  清楚——你有哪些接口值得被刷?他们能薅多少?——没这个清单,后面所有防御都是盲打。

  ---
  四、第 2:威胁建模(STRIDE + 业务画像)

  STRIDE 六威胁(微软经典)

  S - Spoofing            身份伪造(假装别人)
  T - Tampering           数据篡改
  R - Repudiation         否认(我没干这事)
  I - Information disclose 信息泄漏
  D - Denial of service   拒绝服务
  E - Elevation of privilege 权限提升

  把 STRIDE 套到业务接口上

  # 接口:POST /api/coupons/claim
  threats:
    - type: Spoofing
      risk: 一个用户用多个账号
      防御: 设备指纹 + 实名

    - type: Tampering
      risk: 改请求里的 user_id 领他人的券
      防御: 签名 + Token 内身份

    - type: Repudiation
      risk: 用户领了说没领
      防御: 行为日志 + trace_id

    - type: Info disclose
      risk: 通过领取接口枚举有效优惠券
      防御: 鉴权 + 限频

    - type: DoS
      risk: 卡死活动,真用户领不到
      防御: 限流 + 队列

    - type: Elevation
      risk: 普通用户领 VIP 券
      防御: 服务端校验权限

  业务流量画像(知道"正常长什么样")

  // 从生产数据建立画像
  class BusinessProfile {
      public function dailyMetrics(): array {
          return [
              'login' => [
                  'qps_avg'         => 50,
                  'qps_p99'         => 200,
                  'unique_users_h'  => 5000,
                  'fail_rate_norm'  => 0.05,    // 正常 5% 登录失败
              ],
              'register' => [
                  'qps_avg'         => 5,
                  'qps_p99'         => 30,
                  'unique_ips_h'    => 100,
                  'phone_per_ip_h'  => 1.2,     // 平均一个 IP 1-2 个手机号
              ],
              'coupon_claim' => [
                  'unique_users_h'  => 1000,
                  'per_user_per_d'  => 1,       // 每天每用户最多领 1 次
              ],
          ];
      }
  }

  异常检测的"基线" idea

  偏离基线 →触发告警 + 拉风控

  例:
    正常:1 IP 1 小时注册 1.2 个手机号
    异常:1 IP 1 小时注册 200 个手机号 →风控拦截

    正常:登录失败率 5%
    异常:登录失败率突增到 60% →撞库攻击

  大白话:没建模 = 没防御。STRIDE 帮你把"安全风险"拆成具体的对策,业务画像让你知道"正常长什么样"——异常出现时立刻能感知。先
  建模,再写代码,这是企业级业务安全的工程方法。

  ---
  五、第 3:接口加固(签名 / 时间戳 / nonce)

  三件套的目的

  1. 签名     :防止参数篡改
  2. 时间戳   :防止重放攻击(老请求被回放)
  3. nonce    :防止同一请求多次执行

  标准签名算法(HMAC-SHA256)

  <?php
  class ApiSigner {
      public function __construct(
          private string $secret,
      ) {}

      /**
       * 客户端:生成签名
       */
      public function sign(array $params, int $timestamp, string $nonce): string {
          // 1. 加入时间戳和 nonce
          $params['timestamp'] = $timestamp;
          $params['nonce']     = $nonce;

          // 2. 按 key 排序
          ksort($params);

          // 3. 拼接成 canonical string
          $canonical = '';
          foreach ($params as $k => $v) {
              $canonical .= "{$k}={$v}&";
          }
          $canonical = rtrim($canonical, '&');

          // 4. HMAC-SHA256
          return hash_hmac('sha256', $canonical, $this->secret);
      }
  }

  // 客户端使用
  $timestamp = time();
  $nonce     = bin2hex(random_bytes(16));
  $signature = $signer->sign($params, $timestamp, $nonce);

  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      "X-Timestamp: {$timestamp}",
      "X-Nonce: {$nonce}",
      "X-Signature: {$signature}",
  ]);

  服务端校验

  <?php
  class ApiAuthMiddleware
  {
      public function __construct(
          private NonceStore $nonceStore,
          private array $secrets,                // app_id => secret
      ) {}

      public function handle(Request $req, Closure $next) {
          $appId     = $req->header('X-App-Id');
          $timestamp = (int)$req->header('X-Timestamp');
          $nonce     = $req->header('X-Nonce');
          $signature = $req->header('X-Signature');

          // 1. 时间戳校验(5 分钟时间窗)
          $now = time();
          if (abs($now - $timestamp) > 300) {
              throw new \Exception('请求已过期');
          }

          // 2. nonce 防重放(Redis SETNX,5 分钟过期)
          if (!$this->nonceStore->markIfNew($nonce, ttl: 300)) {
              throw new \Exception('重复请求');
          }

          // 3. 签名校验
          $secret = $this->secrets[$appId] ?? throw new \Exception('未知 App');
          $expected = $this->computeSignature($req->all(), $timestamp, $nonce, $secret);
          if (!hash_equals($expected, $signature)) {
              throw new \Exception('签名错误');
          }

          return $next($req);
      }
  }

  class NonceStore {
      public function markIfNew(string $nonce, int $ttl): bool {
          return Redis::set("nonce:{$nonce}", '1', 'NX', 'EX', $ttl) === true;
      }
  }

  JWT + 设备绑定

  <?php
  class TokenIssuer {
      public function issue(int $userId, string $deviceId): string {
          $now = time();
          $payload = [
              'iss' => 'acme',
              'sub' => $userId,
              'iat' => $now,
              'exp' => $now + 3600,
              'jti' => bin2hex(random_bytes(16)),
              'did' => $deviceId,                 // 绑定设备
              'fp'  => $this->fingerprint(),       // 浏览器指纹
          ];
          return JWT::encode($payload, $this->key, 'HS256');
      }

      public function verify(string $token, string $currentDevice): array {
          $payload = JWT::decode($token, new Key($this->key, 'HS256'));

          // 设备不一致直接拒
          if ($payload->did !== $currentDevice) {
              throw new \Exception('Token 与设备不匹配,可能被盗');
          }

          return (array)$payload;
      }
  }

  关键禁忌

  // ❌ 用 GET 传敏感参数
  GET /api/order?user_id=88&token=xxx

  // ❌ 在客户端校验签名(秘钥永远不能进客户端)
  function clientSign(...) { return hash_hmac(...); }

  // ❌ 时间窗太宽
  abs($now - $timestamp) > 86400      // 一天内都允许 →重放天堂

  // ❌ 用 == 比较签名
  if ($expected == $actual) ...        // 时序攻击!必须 hash_equals

  大白话:接口加固是业务安全的第一道墙。没有签名 + 时间戳 + nonce,任何抓包工具都能伪造请求。这三件套是 API
  的"安全卫生间"——任何对外接口都该有,内部接口也建议有。

  ---
  六、第 4:防刷三件套(限流 / 验证码 / 幂等)

  1. 限流(必须分维度)

  单 IP 限流       :防小作坊
  单用户限流      :防个人薅
  单接口限流      :防全局打挂
  组合维度限流    :防策略性攻击(分布式)

  滑动窗口限流(Redis + Lua)

  class SlidingWindowLimiter {
      private const SCRIPT = <<<'LUA'
          local key = KEYS[1]
          local now = tonumber(ARGV[1])
          local window = tonumber(ARGV[2])
          local limit = tonumber(ARGV[3])

          -- 清理过期记录
          redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)

          -- 当前数量
          local count = redis.call('ZCARD', key)
          if count >= limit then
              return 0
          end

          redis.call('ZADD', key, now, now)
          redis.call('EXPIRE', key, window)
          return 1
      LUA;

      public function allow(string $key, int $limit, int $windowSec): bool {
          $now = (int)(microtime(true) * 1000);
          $result = Redis::eval(
              self::SCRIPT, 1, $key, $now, $windowSec, $limit
          );
          return $result === 1;
      }
  }

  // 用法:多维度组合
  class CouponLimiter {
      public function checkBeforeClaim(int $userId, string $ip, string $couponId): void {
          if (!$this->limiter->allow("coupon:user:{$userId}:{$couponId}", 1, 86400)) {
              throw new \Exception('您已领取过');
          }
          if (!$this->limiter->allow("coupon:ip:{$ip}:{$couponId}", 5, 3600)) {
              throw new \Exception('IP 异常,稍后再试');
          }
          if (!$this->limiter->allow("coupon:global:{$couponId}", 10000, 86400)) {
              throw new \Exception('活动太火爆');
          }
      }
  }

  令牌桶(适合突发流量)

  class TokenBucket {
      private const SCRIPT = <<<'LUA'
          local key = KEYS[1]
          local capacity = tonumber(ARGV[1])
          local rate = tonumber(ARGV[2])
          local now = tonumber(ARGV[3])
          local cost = tonumber(ARGV[4])

          local data = redis.call('HMGET', key, 'tokens', 'last')
          local tokens = tonumber(data[1]) or capacity
          local last = tonumber(data[2]) or now

          -- 补充令牌
          local elapsed = now - last
          tokens = math.min(capacity, tokens + elapsed * rate)

          if tokens < cost then
              redis.call('HMSET', key, 'tokens', tokens, 'last', now)
              redis.call('EXPIRE', key, 60)
              return 0
          end

          tokens = tokens - cost
          redis.call('HMSET', key, 'tokens', tokens, 'last', now)
          redis.call('EXPIRE', key, 60)
          return 1
      LUA;

      public function take(string $key, float $capacity, float $ratePerSec, int $cost = 1): bool {
          $now = microtime(true);
          return Redis::eval(self::SCRIPT, 1, $key, $capacity, $ratePerSec, $now, $cost) === 1;
      }
  }

  2. 验证码(分级使用)

  正常用户       :不弹
  风险中等      :弹滑块
  风险高        :短信 / 人脸
  极高风险      :拒绝

  class RiskBasedCaptcha {
      public function challenge(string $action, RiskScore $score): ?string {
          return match (true) {
              $score->value < 30  => null,                    // 不挑战
              $score->value < 60  => 'slider',                // 滑块
              $score->value < 85  => 'sms',                   // 短信
              default             => 'reject',                // 直接拒
          };
      }
  }

  Google reCAPTCHA v3 接入(无感)

  class RecaptchaVerifier {
      public function verify(string $token, string $action): float {
          $resp = Http::post('https://www.google.com/recaptcha/api/siteverify', [
              'secret'   => env('RECAPTCHA_SECRET'),
              'response' => $token,
          ]);
          $data = $resp->json();

          if (!$data['success']) return 0.0;
          if ($data['action'] !== $action) return 0.0;

          return (float)($data['score'] ?? 0);    // 0-1,越大越像人
      }
  }

  // 用法
  $humanScore = $recaptcha->verify($req->captchaToken, 'login');
  if ($humanScore < 0.5) {
      throw new \Exception('请完成验证');
  }

  3. 幂等(写接口必备)

  class IdempotencyMiddleware {
      public function handle(Request $req, Closure $next) {
          $key = $req->header('X-Idempotency-Key');
          if (!$key) return $next($req);

          $lockKey = "idem:{$key}";

          // 已处理过的直接返回老结果
          $cached = Redis::get($lockKey);
          if ($cached !== false) {
              return response()->json(json_decode($cached, true));
          }

          // 抢锁
          if (!Redis::set("{$lockKey}:lock", '1', 'NX', 'EX', 30)) {
              throw new \Exception('请勿重复提交');
          }

          try {
              $resp = $next($req);
              Redis::set($lockKey, json_encode($resp->getData(true)), 'EX', 86400);
              return $resp;
          } finally {
              Redis::del("{$lockKey}:lock");
          }
      }
  }

  // 业务里用 Token 模式(更安全)
  class IdempotencyTokenIssuer {
      // 第一步:发 token
      public function issue(int $userId): string {
          $token = bin2hex(random_bytes(16));
          Redis::set("idem_token:{$token}", $userId, 'EX', 600);
          return $token;
      }

      // 第二步:用 token(原子 GETDEL)
      public function consume(string $token, int $userId): bool {
          $stored = Redis::eval(<<<'LUA'
              local v = redis.call('GET', KEYS[1])
              if v then redis.call('DEL', KEYS[1]) end
              return v
          LUA, 1, "idem_token:{$token}");

          return $stored == $userId;
      }
  }

  大白话:防刷三件套缺一不可。限流防"量",验证码防"机",幂等防"重"。多维度组合限流是关键——只锁单IP 的话,黑产用 1000 个 IP
  一拥而上;多维度(IP + 用户 + 设备 + 全局)才能挡住分布式攻击。

  ---
  七、第 5:身份与设备指纹

  设备指纹的多维度采集

  // 浏览器端(简化版,生产用 FingerprintJS Pro)
  async function fingerprint() {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      ctx.fillText('fingerprint', 0, 0);

      return {
          ua:          navigator.userAgent,
          lang:        navigator.language,
          screen:      `${screen.width}x${screen.height}`,
          timezone:    Intl.DateTimeFormat().resolvedOptions().timeZone,
          cores:       navigator.hardwareConcurrency,
          memory:      navigator.deviceMemory,
          webgl:       getWebGLFingerprint(),
          canvas:      hashStr(canvas.toDataURL()),
          fonts:       await detectFonts(),
          plugins:     getPluginsList(),
          cookies:     navigator.cookieEnabled,
          doNotTrack:  navigator.doNotTrack,
          // ...还能采几十个维度
      };
  }

  服务端聚合 + 哈希

  class DeviceFingerprint {
      public function generate(array $clientData, string $clientIp): string {
          $stable = [
              'ua'       => $clientData['ua'] ?? '',
              'screen'   => $clientData['screen'] ?? '',
              'timezone' => $clientData['timezone'] ?? '',
              'webgl'    => $clientData['webgl'] ?? '',
              'canvas'   => $clientData['canvas'] ?? '',
              'fonts'    => implode(',', $clientData['fonts'] ?? []),
          ];
          ksort($stable);

          return hash('sha256', json_encode($stable));
      }

      public function similarity(array $a, array $b): float {
          $matched = 0;
          foreach ($a as $k => $v) {
              if (isset($b[$k]) && $b[$k] === $v) {
                  $matched++;
              }
          }
          return $matched / max(count($a), 1);
      }
  }

  IP 信息富化

  class IpEnricher {
      public function enrich(string $ip): IpInfo {
          // 1. 地理位置(MaxMind GeoIP2)
          $geo = $this->maxmind->city($ip);

          // 2. 是否代理 / VPN / Tor
          $isProxy = $this->ipQs->checkProxy($ip);

          // 3. 是否数据中心 IP
          $isDatacenter = $this->ipQs->isDatacenter($ip);

          // 4. 历史风险标签
          $reputation = $this->reputationDb->lookup($ip);

          return new IpInfo(
              country:     $geo->country->isoCode,
              city:        $geo->city->name,
              isp:         $geo->traits->isp,
              isProxy:     $isProxy,
              isDatacenter:$isDatacenter,
              reputation:  $reputation,
          );
      }
  }

  黑产 IP 识别特征

  高风险 IP 特征:
  ✅ IDC 数据中心 IP(99% 是脚本)
  ✅ Tor 出口节点
  ✅ 已知代理池
  ✅ 地理位置和声明的不符
  ✅ 短时间内大量不同账号登录
  ✅ 行为类似机器(无鼠标移动、超快输入)

  账号关联识别(神器)

  // 通过设备指纹关联账号
  class AccountLinker {
      public function findRelated(int $userId): array {
          $devices = LoginLog::where('user_id', $userId)
              ->select('device_fingerprint')
              ->distinct()->pluck('device_fingerprint');

          // 同设备登录过的所有账号
          $relatedUsers = LoginLog::whereIn('device_fingerprint', $devices)
              ->where('user_id', '<>', $userId)
              ->select('user_id')
              ->distinct()->pluck('user_id');

          return [
              'devices'         => $devices->toArray(),
              'related_users'   => $relatedUsers->toArray(),
              'is_suspicious'   => count($relatedUsers) > 5,    // 同设备 >5 账号
          ];
      }
  }

  大白话:设备指纹是抓羊毛党的"杀手锏"。一个用户可以注册 100 个账号,但他的设备只有 1-2 台。关联同设备的多个账号 =
  一抓一个准。这就是为什么真正的风控,90% 的抓获是靠设备指纹 + 关系图谱。

  ---
  八、第 6:行为分析与画像

  行为序列采集

  class BehaviorTracker {
      public function track(string $userId, string $action, array $ctx = []): void {
          $event = [
              'user_id'    => $userId,
              'action'     => $action,
              'timestamp'  => microtime(true),
              'ip'         => request()->ip(),
              'device_fp'  => request()->header('X-Device-Fp'),
              'page'       => $ctx['page'] ?? null,
              'duration'   => $ctx['duration'] ?? null,    // 上一页停留时长
              'mouse_moves'=> $ctx['mouse_moves'] ?? null,  // 鼠标移动次数
              'keystrokes' => $ctx['keystrokes'] ?? null,
          ];

          // 写 Kafka,后台风控引擎消费
          $this->kafka->send('behavior-events', json_encode($event));

          // 同时实时维度计数
          $this->recordRealtimeMetrics($userId, $action);
      }
  }

  异常行为模式

  正常用户登录:
    访问首页 →看商品 →加购 →提单 →付款
    停留时间:每页 3-30 秒
    鼠标移动:有
    打字间隔:200-500ms

  机器人登录:
    直接访问登录接口
    无停留时间
    无鼠标移动
    打字间隔:< 50ms 或固定值

  实时行为计数器

  class BehaviorCounter {
      public function recordRealtimeMetrics(string $userId, string $action): void {
          // 用户每分钟操作数
          Redis::incr("user:ops:1m:{$userId}");
          Redis::expire("user:ops:1m:{$userId}", 60);

          // IP 每小时唯一用户数(HyperLogLog)
          Redis::pfadd("ip:users:1h:" . request()->ip(), $userId);
          Redis::expire("ip:users:1h:" . request()->ip(), 3600);
      }

      public function isOpsTooFast(string $userId): bool {
          return Redis::get("user:ops:1m:{$userId}") > 200;  // 1 分钟 200 次操作
      }
  }

  复杂模式:抢购脚本特征

  class FlashSaleDetector {
      public function detect(int $userId, int $productId): string {
          // 特征 1:活动开始 0-100ms 内提交
          $eventStart = $this->getEventStartTime($productId);
          $now = microtime(true) * 1000;
          if ($now - $eventStart < 100) {
              return 'super_fast';
          }

          // 特征 2:无浏览记录直接下单
          if (!ViewLog::where('user_id', $userId)
                      ->where('product_id', $productId)
                      ->exists()) {
              return 'no_browse';
          }

          // 特征 3:此前从无活动账号
          $oldOrders = Order::where('user_id', $userId)->count();
          if ($oldOrders === 0) {
              return 'fresh_account';
          }

          return 'normal';
      }
  }

  关系图谱(邀新返现的克星)

  // 用 Neo4j 或自建图模型
  class InvitationGraph {
      public function detectFraud(int $userId): array {
          // 找出该用户邀请的所有人
          $invitees = Invitation::where('inviter_id', $userId)->pluck('invitee_id');

          if (count($invitees) < 10) return ['fraud' => false];

          // 看这些被邀请人的设备指纹
          $fingerprints = LoginLog::whereIn('user_id', $invitees)
              ->select('device_fingerprint')
              ->distinct()->count();

          // 如果 50 个被邀请人只用了 3 个设备 →严重欺诈
          $deviceRatio = $fingerprints / count($invitees);
          if ($deviceRatio < 0.2) {
              return [
                  'fraud'   => true,
                  'reason'  => 'low_device_diversity',
                  'ratio'   => $deviceRatio,
              ];
          }

          return ['fraud' => false];
      }
  }

  大白话:行为分析是高级风控的核心。规则可以被绕过,但黑产很难模拟"真实用户的行为节奏"——浏览停留、鼠标移动、操作间隔。把这
  些行为序列化、特征化,机器学习就能识别 95% 的脚本。这就是大厂风控团队的核心 know-how。

  ---
  九、第 7:风控决策引擎(规则 + 模型)

  风控架构

  ┌────────────────────────────────────────┐
  │  业务请求(注册 / 登录 / 下单 / 领券)  │
  └────────────┬───────────────────────────┘
               ↓
          ┌────────────┐
          │  特征拉取  │ ←用户画像、设备、IP、历史
          └─────┬──────┘
                ↓
          ┌────────────┐
          │  规则引擎  │ ←同步,毫秒级
          └─────┬──────┘
                ↓拿不准的
          ┌────────────┐
          │   ML 模型   │ ←异步,秒级
          └─────┬──────┘
                ↓
          ┌────────────┐
          │  决策结果  │ →pass / challenge / reject
          └────────────┘

  规则引擎(开源选型)

  PHP 选项:
    - Symfony ExpressionLanguage(轻量)
    - hoa/ruler(老牌)
    - 自研 DSL(推荐企业级)

  跨语言:
    - Drools (Java)
    - OpenPolicyAgent (Go,推荐)

  自研规则引擎(简版)

  <?php
  class RiskRule {
      public function __construct(
          public string $name,
          public string $expression,           // SpEL / 简化 DSL
          public int $score,                   // 命中加分
          public string $action,               // pass / challenge / reject
      ) {}
  }

  class RuleEngine {
      /** @var RiskRule[] */
      private array $rules = [];

      public function evaluate(array $features): RiskDecision {
          $totalScore = 0;
          $hits = [];

          foreach ($this->rules as $rule) {
              if ($this->matches($rule->expression, $features)) {
                  $totalScore += $rule->score;
                  $hits[] = $rule->name;
              }
          }

          return new RiskDecision(
              score:  $totalScore,
              hits:   $hits,
              action: $this->decideAction($totalScore),
          );
      }

      private function matches(string $expr, array $features): bool {
          $lang = new \Symfony\Component\ExpressionLanguage\ExpressionLanguage();
          return (bool)$lang->evaluate($expr, $features);
      }

      private function decideAction(int $score): string {
          return match (true) {
              $score < 30  => 'pass',
              $score < 70  => 'challenge',
              default      => 'reject',
          };
      }
  }

  // 配置规则
  $engine->addRule(new RiskRule(
      name: 'datacenter_ip',
      expression: 'ip.isDatacenter == true',
      score: 50,
      action: 'reject',
  ));

  $engine->addRule(new RiskRule(
      name: 'fresh_account_high_value',
      expression: 'account.ageDays < 1 && order.amount > 1000',
      score: 60,
      action: 'challenge',
  ));

  $engine->addRule(new RiskRule(
      name: 'too_many_accounts_same_device',
      expression: 'device.relatedAccounts > 5',
      score: 80,
      action: 'reject',
  ));

  业务接入(下单为例)

  class OrderController {
      public function place(PlaceOrderRequest $req) {
          // 1. 收集特征
          $features = $this->featureCollector->collect($req->user(), $req);

          // 2. 风控决策
          $decision = $this->riskEngine->evaluate($features);

          // 3. 处置
          switch ($decision->action) {
              case 'reject':
                  $this->log->warn('order_rejected', $decision->hits);
                  throw new \Exception('订单存在风险,请联系客服');

              case 'challenge':
                  if (!$req->captchaPassed) {
                      return response()->json([
                          'need_captcha' => true,
                          'type' => 'sms',
                      ], 428);
                  }
                  break;

              case 'pass':
                  break;
          }

          // 4. 正常下单
          $order = $this->placeOrder($req);

          // 5. 异步上报模型(用于训练)
          $this->mq->send('risk-events', [
              'features' => $features,
              'decision' => $decision,
              'result'   => 'success',
          ]);

          return $order;
      }
  }

  灰名单 / 黑名单

  class BlacklistService {
      public function add(string $type, string $value, string $reason, int $ttl = 86400): void {
          Redis::setex("bl:{$type}:{$value}", $ttl, $reason);

          BlacklistRecord::create([
              'type'   => $type,
              'value'  => $value,
              'reason' => $reason,
              'expires_at' => now()->addSeconds($ttl),
          ]);
      }

      public function check(string $type, string $value): ?string {
          return Redis::get("bl:{$type}:{$value}");
      }
  }

  // 用法
  $bl->add('user', $userId, 'fraud_detected', ttl: 86400 * 30);
  $bl->add('device', $deviceFp, 'multiple_accounts', ttl: 86400 * 90);
  $bl->add('ip', $ip, 'datacenter', ttl: 3600);

  评分模型(简易版)

  class RiskScorer {
      public function score(array $features): float {
          $score = 0;

          // 设备维度
          if ($features['device']['isFresh']) $score += 20;
          if ($features['device']['relatedAccounts'] > 3) $score += 30;

          // IP 维度
          if ($features['ip']['isProxy']) $score += 40;
          if ($features['ip']['isDatacenter']) $score += 50;

          // 账号维度
          if ($features['account']['ageDays'] < 1) $score += 25;
          if (!$features['account']['phoneVerified']) $score += 15;

          // 行为维度
          if ($features['behavior']['opsPerMin'] > 100) $score += 50;
          if ($features['behavior']['mouseMovements'] === 0) $score += 30;

          return min($score, 100);
      }
  }

  大白话:风控引擎是业务安全的"大脑"。规则引擎处理 80% 的明确风险(IP 黑名单、设备多账号),ML 模型处理 20%
  的灰区。关键是"特征 + 规则 + 处置"三段式——任何一段缺都不行。从规则起步,数据多了再上模型——这0是到 1 的最佳路径。

  ---
  十、第 8:业务监控与应急响应

  业务安全的关键监控指标

  注册维度:
    - 注册成功率突变
    - 同 IP 注册数 / 小时
    - 异常手机号占比(170/171/接码平台号段)

  登录维度:
    - 登录失败率突增 →撞库
    - 登录国家 / 地区分布异常
    - 同账号多 IP 登录(账号被盗)

  业务维度:
    - 优惠券领取速度
    - 订单率异常
    - 客单价突变

  实时业务大盘

  panels:
    - name: 安全总览
      metrics:
        - 当前在线用户
        - 命中风控的请求数 / 分钟
        - 拒绝 / 挑战 / 通过比例

    - name: 注册防御
      metrics:
        - 注册 QPS
        - 同 IP 注册分布
        - 验证码失败率
        - 实名校验失败率

    - name: 优惠券防御
      metrics:
        - 实时领取数
        - 单用户领取数 TopN
        - 关联账号识别数

    - name: 订单防御
      metrics:
        - 风险订单数
        - 资损金额(估算)

  告警规则示例

  - alert: SuspiciousRegisterSpike
    expr: |
      increase(register_total[5m]) > 1000
      and
      avg_over_time(register_unique_ip_ratio[5m]) < 0.3
    annotations:
      summary: "5 分钟内注册 1000+ 但 IP 不到 30%"
      runbook: "https://wiki/runbook/register-attack"

  - alert: LoginFailureSpike
    expr: |
      rate(login_failures_total[5m]) / rate(login_total[5m]) > 0.6
    annotations:
      summary: "登录失败率 > 60%,可能撞库"

  - alert: CouponClaimAnomaly
    expr: |
      rate(coupon_claim_total[1m]) > 100
      and
      rate(coupon_claim_unique_devices[1m]) < 20
    annotations:
      summary: "1 分钟领券 100+,但设备 < 20"

  应急响应 SOP

  ## 业务安全应急 SOP

  ### Level 1:轻度异常(单接口被刷)
  触发:某接口流量异常,无资损
  响应时间:30 分钟
  处置:
    1. 限流提高
    2. 验证码升级
    3. 加监控

  ### Level 2:中度异常(资损可控)
  触发:资损 < 10,有疑似攻击
  响应时间:10 分钟
  处置:
    1. 拉风控应急组
    2. 全局加风控规则
    3. 黑名单近期攻击者
    4. 通知产品决策

  ### Level 3:重大异常(资损严重)
  触发:资损 > 10/ 影响主流程
  响应时间:5 分钟
  处置:
    1. 触发熔断 →暂停受影响接口
    2. 拉所有相关团队
    3. CEO / CTO 知会
    4. 准备公关稿
    5. 同步法务(若涉及报警)

  一键熔断开关

  class EmergencySwitch {
      public function killSwitch(string $feature): void {
          $this->configCenter->set("kill_switch:{$feature}", true);

          // 通知所有实例(Redis pub/sub)
          Redis::publish('config-update', json_encode([
              'key'   => "kill_switch:{$feature}",
              'value' => true,
          ]));

          Slack::send('#sec-alerts', "🚨 已熔断功能: {$feature}");
      }

      public function isKilled(string $feature): bool {
          return $this->configCenter->get("kill_switch:{$feature}", false);
      }
  }

  // 业务里
  if ($switch->isKilled('coupon_claim')) {
      return response()->json(['msg' => '活动维护中'], 503);
  }

  大白话:业务安全是 7×24的事——黑产凌晨3 点不睡觉。没有自动告警 + Runbook + 一键熔断,等出事再人工反应就晚了。告警精准 +
  处置快速 + 决策果断——这三件事到位,资损能从亿元级压到万元级。

  ---
  十一、第 9:数据安全(贯穿三态)

  数据三态保护

  传输中 (in-transit) :TLS 1.3、双向证书
  存储中 (at-rest)    :字段级加密、TDE、密钥管理
  使用中 (in-use)     :脱敏、最小权限、审计

  1. 传输加密(必须)

  # nginx 强制 TLS 1.3
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers off;

  # HSTS 强制 HTTPS
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

  2. 字段级加密

  <?php
  class EncryptedAttribute {
      public function __construct(
          private string $key,
      ) {}

      public function encrypt(string $plaintext): string {
          $iv = random_bytes(16);
          $cipher = openssl_encrypt(
              $plaintext, 'aes-256-gcm', $this->key,
              OPENSSL_RAW_DATA, $iv, $tag
          );
          return base64_encode($iv . $tag . $cipher);
      }

      public function decrypt(string $stored): string {
          $data = base64_decode($stored);
          $iv     = substr($data, 0, 16);
          $tag    = substr($data, 16, 16);
          $cipher = substr($data, 32);
          return openssl_decrypt(
              $cipher, 'aes-256-gcm', $this->key,
              OPENSSL_RAW_DATA, $iv, $tag
          );
      }
  }

  // Laravel Eloquent Cast
  class User extends Model {
      protected $casts = [
          'phone'     => EncryptedCast::class,
          'id_card'   => EncryptedCast::class,
          'real_name' => EncryptedCast::class,
      ];
  }

  3. 可搜索加密(关键)

  // 加密存储 + 哈希索引
  class SearchableEncryption {
      public function store(string $plaintext): array {
          return [
              'encrypted' => $this->encrypt($plaintext),                    // 加密存储
              'hash'      => hash_hmac('sha256', $plaintext, $this->salt), // 用于精确查询
          ];
      }

      public function findByPhone(string $phone): ?User {
          $hash = hash_hmac('sha256', $phone, $this->salt);
          return User::where('phone_hash', $hash)->first();
      }
  }

  4. 数据脱敏(展示层)

  class DataMasker {
      public function maskPhone(string $phone): string {
          return substr($phone, 0, 3) . '****' . substr($phone, 7);
      }

      public function maskIdCard(string $id): string {
          return substr($id, 0, 6) . '********' . substr($id, -4);
      }

      public function maskName(string $name): string {
          $len = mb_strlen($name);
          return mb_substr($name, 0, 1) . str_repeat('*', $len - 1);
      }

      public function maskEmail(string $email): string {
          [$user, $domain] = explode('@', $email);
          return substr($user, 0, 2) . '***@' . $domain;
      }
  }

  5. 接口脱敏(自动)

  class MaskingResource extends JsonResource {
      public function toArray($request): array {
          $isAdmin = $request->user()?->isAdmin() ?? false;

          return [
              'id'        => $this->id,
              'name'      => $isAdmin ? $this->real_name : $this->masker->maskName($this->real_name),
              'phone'     => $isAdmin ? $this->phone     : $this->masker->maskPhone($this->phone),
              'id_card'   => $isAdmin ? $this->id_card   : $this->masker->maskIdCard($this->id_card),
          ];
      }
  }

  6. 数据导出审计(防内鬼)

  class DataExportAuditor {
      public function audit(int $operatorId, string $type, int $count): void {
          ExportLog::create([
              'operator_id' => $operatorId,
              'data_type'   => $type,
              'row_count'   => $count,
              'ip'          => request()->ip(),
              'reason'      => request()->input('reason'),  // 必填理由
              'occurred_at' => now(),
          ]);

          // 大量导出 →自动告警
          if ($count > 1000) {
              Slack::send('#sec', "⚠️大量导出: {$operatorId} 导出 {$count} 条 {$type}");
          }
      }
  }

  7. 密钥管理(KMS)

  原则:
    - 密钥不进代码
    - 密钥不进 .env(明文)
    - 密钥进 KMS / Vault
    - 密钥定期轮换
    - 密钥访问审计

  阿里云 KMS / AWS KMS 用法:
    应用启动 →用 IAM 身份调 KMS →拿数据密钥 →解密字段
    →数据密钥不落盘

  class KmsEncryption {
      private string $dataKey;

      public function init(): void {
          // 启动时拉一次数据密钥(用 KMS 的主密钥解密)
          $encryptedDek = file_get_contents('/secrets/data_key.enc');
          $this->dataKey = $this->kms->decrypt($encryptedDek);
      }

      public function encrypt(string $plaintext): string {
          return openssl_encrypt($plaintext, 'aes-256-gcm', $this->dataKey, ...);
      }
  }

  8. SQL 注入永远不要靠"过滤"

  // ❌ 一辈子别这么写
  $name = addslashes($_GET['name']);
  DB::select("SELECT * FROM users WHERE name = '{$name}'");

  // ✅ 永远参数化
  DB::select('SELECT * FROM users WHERE name = ?', [$_GET['name']]);

  合规要求(国内必看)

  《个人信息保护法》(PIPL)2021
  《数据安全法》              2021
  《网络安全法》              2017
  等保 2.0(三级以上系统)

  关键合规点:
    - 个人信息收集"最小必要"
    - 用户同意 + 撤回权
    - 数据出境管控
    - 80% 用户的同意率
    - 数据泄漏 72 小时内报告

  大白话:数据安全的核心是"最小化原则"——不必要的字段不存、不必要的人不能看、不必要的导出有审计。字段级加密+ 脱敏 + KMS =
  数据安全的金字塔。80% 的内部数据泄漏不是黑客,是有权限的人滥用——所以审计比加密更重要。

  ---
  十二、第 10:对抗演练与持续运营

  红蓝对抗(GameDay)

  红队(攻击):
    - 模拟黑产攻击
    - 用真实工具(8 万账号库 + 设备模拟器)
    - 目标:刷光优惠券 / 撞库 / 拖库

  蓝队(防御):
    - 实时风控团队
    - 目标:发现 + 拦截 + 不误伤

  每季度一次,半天到一天

  红队工具箱(知己知彼)

  账号供应:
    - 接码平台(国内国外)
    - 实名号商
    - 撞库字典

  设备模拟:
    - Selenium / Puppeteer
    - 安卓模拟器 + Xposed
    - 改机软件(改设备指纹)

  IP 池:
    - 住宅代理
    - 4G 拨号池
    - 数据中心 IP

  漏洞奖励计划(SRC)

  小公司:HackerOne / 补天 / 漏洞盒子
  中大型:自建 SRC(安全应急响应中心)

  激励:
    P0(资损 100+)   :10P1(资损 10+)    :5P2(中危)            :5000-2P3(低危)            :1000-3000

  持续运营机制

  1. 每日:
     - 看业务安全大盘
     - 处理告警
     - 黑产新工具情报

  2. 每周:
     - 分析新攻击模式
     - 调风控规则
     - 复盘事故

  3. 每月:
     - 模型重训练
     - 阈值优化
     - 报表给 CEO

  4. 每季度:
     - 红蓝对抗
     - 战略复盘

  5. 每年:
     - 三方安全审计
     - 等保测评

  团队配置

  小团队(< 100):
    - 1 名安全工程师()+ 业务工程师协作

  中团队(100-500):
    - 1-2 名安全工程师
    - 1 名风控工程师
    - 接外部威胁情报

  大团队(500+):
    - 安全部门(5+)
    - 风控部门(10+)
    - 数据部门(数据安全)
    - 法务合规

  大白话:业务安全是持续战争,不是一次性项目。今天的规则,明天黑产就破解了。红蓝对抗 + 漏洞奖励 + 持续迭代 =
  跟黑产赛跑的耐力。做不到持续运营,再好的系统三个月就被打穿。

  ---
  十三、最佳落地节奏(6 个月路线图)

  ┌─────────────┬─────────────────────┬─────────────────────────────┬──────┐
  │    阶段     │        时间         │           干什么            │ 风险 │
  ├─────────────┼─────────────────────┼─────────────────────────────┼──────┤
  │ 第 1-2 周   │ 攻击面盘点          │ 列出所有公开接口 + 风险等级 │ 低   │
  ├─────────────┼─────────────────────┼─────────────────────────────┼──────┤
  │ 第 3-4 周   │ 接口加固            │ 签名 + 时间戳 + nonce       │ 低   │
  ├─────────────┼─────────────────────┼─────────────────────────────┼──────┤
  │ 第 5-8 周   │ 防刷三件套          │ 限流 + 验证码 + 幂等        │ 中   │
  ├─────────────┼─────────────────────┼─────────────────────────────┼──────┤
  │ 第 9-12 周  │ 设备指纹 + IP 信誉  │ 接 FingerprintJS + IP 库    │ 中   │
  ├─────────────┼─────────────────────┼─────────────────────────────┼──────┤
  │ 第 13-16 周 │ 行为分析 + 风控引擎 │ 规则引擎 + 黑名单           │ 高   │
  ├─────────────┼─────────────────────┼─────────────────────────────┼──────┤
  │ 第 17-20 周 │ 数据安全            │ 字段加密 + KMS + 审计       │ 高   │
  ├─────────────┼─────────────────────┼─────────────────────────────┼──────┤
  │ 第 21-24 周 │ 监控告警 + Runbook  │ 业务大盘 + 应急响应         │ 中   │
  ├─────────────┼─────────────────────┼─────────────────────────────┼──────┤
  │ 持续        │ 运营 + 对抗         │ 红蓝对抗 + 持续优化         │ 持续 │
  └─────────────┴─────────────────────┴─────────────────────────────┴──────┘

  ---
  十四、9 条铁律

  1. 永远假设黑产存在——防御 = 0 业务。
  2. 多维度组合限流——单维度永远会被绕过。
  3. 设备指纹必装——它能识别90% 的薅羊毛。
  4. 写接口必幂等——重试一次扣两次钱就翻车。
  5. 永远脱敏展示——内部员工也不能看明文身份证。
  6. 永远记审计——谁导出了什么,何时,为什么。
  7. 永远有熔断——出事一秒关闭功能。
  8. 永远有 Runbook——半夜告警没Runbook 就是抓瞎。
  9. 永远持续运营——一次性安全项目= 没安全。

  ---
  十五、最容易踩的 8 个坑

  1. 只防技术不防业务:WAF 装了一堆,优惠券还是被刷光。
  2. 限流只锁 IP:黑产 1000 个 IP 一拥而上,白搭。
  3. 没幂等就重试:支付重试一次扣两次钱,公关灾难。
  4. 设备指纹只看 UA:UA 伪造比换衣服还简单。
  5. 风控规则写代码里:出事改代码 + 发版 →黄花菜都凉了。
  6. 数据库存明文身份证:一次拖库,公司命运改写。
  7. 没有数据导出审计:内部员工卖用户数据,事发才知道。
  8. 没有红蓝对抗:你以为防得住,黑产实测 30 秒破。

  ---
  十六、最后三句真话

  1. 业务安全是经济学——目标不是绝对安全,是让黑产不划算。
  2. 数据安全比业务安全更致命——业务被薅几十万能扛,数据泄漏80 万实名信息能让公司倒闭。
  3. 安全不是技术问题,是组织文化——CE不重视、产品天天追功能、工程师只关心进度——这种公司迟早被黑产教做人。

  照这套抄完,你能把一个 PHP 业务系统从「被薅光都不知道」进化到「实时风控 + 自动拦截 + 数据全程可审计」。这是 2026 年面向
  C 端 PHP 系统的硬门槛——所有玩流量、玩活动、玩用户数据的产品,这套体系是生死线。

  需要我针对某一阶(比如基于 Flink 的实时风控引擎、机器学习模型在 PHP 风控的落地、KMS +
  字段级加密的高并发性能优化、红蓝对抗 GameDay 完整剧本)再深挖,随时说。

更多推荐