一、整体架构大白话
                                                                                                                          请求进来的完整安全链路:
                                                                                                                          客户端请求
      │
      ▼
  [国密SM2 HTTPS] ← 传输层加密,防止被人抓包看到数据
      │
      ▼
  [IP白名单/限流] ← 防止暴力攻击和DDoS
      │
      ▼
  [签名验证中间件] ← 防止请求被篡改、重放攻击
      │
      ▼
  [JWT Token验证] ← 确认是谁在请求
      │
      ▼
  [RBAC权限检查] ← 确认有没有权限做这件事
      │
      ▼
  [业务逻辑处理]
      │
      ▼
  [敏感数据SM4加密] ← 存数据库前加密,防止数据库被拖库
      │
      ▼
  [响应数据脱敏] ← 返回给前端前,手机号/身份证打码

  ---
  二、技术选型(最好的库)

  ┌─────────────────┬───────────────────────────────────┬───────────────────────────────────────┐
  │      用途       │              选用库               │                 理由                  │
  ├─────────────────┼───────────────────────────────────┼───────────────────────────────────────┤
  │ 国密SM2/SM3/SM4 │ gmssl/gmssl + phpseclib/phpseclib │ 最完整的PHP国密实现                   │
  ├─────────────────┼───────────────────────────────────┼───────────────────────────────────────┤
  │ JWT签名         │ 自实现SM3-HMAC                    │ 标准JWT库不支持国密,自己封装         │
  ├─────────────────┼───────────────────────────────────┼───────────────────────────────────────┤
  │ 限流            │ predis/predis + Redis滑动窗口     │ 精准限流,支持集群                    │
  ├─────────────────┼───────────────────────────────────┼───────────────────────────────────────┤
  │ 权限控制        │ casbin/casbin                     │ 最强PHP RBAC/ABAC库,政务场景完美适配 │
  ├─────────────────┼───────────────────────────────────┼───────────────────────────────────────┤
  │ 数据验证        │ ThinkPHP内置Validate              │ 框架原生,无需额外依赖                │
  ├─────────────────┼───────────────────────────────────┼───────────────────────────────────────┤
  │ 日志审计        │ monolog/monolog                   │ 最成熟的PHP日志库                     │
  ├─────────────────┼───────────────────────────────────┼───────────────────────────────────────┤
  │ 加密存储        │ defuse/php-encryption             │ 对称加密最安全的PHP库                 │
  └─────────────────┴───────────────────────────────────┴───────────────────────────────────────┘

  ---
  三、安装依赖

  # 安装所有需要的包
  composer require gmssl/gmssl \
      phpseclib/phpseclib \
      predis/predis \
      casbin/casbin \
      casbin/think-authz \
      monolog/monolog \
      defuse/php-encryption \
      paragonie/constant_time_encoding

  # 安装国密PHP扩展(麒麟OS/统信UOS上)
  # 方式一:pecl安装
  pecl install gmssl

  # 方式二:源码编译(ARM鲲鹏服务器)
  git clone https://github.com/guanzhi/GmSSL
  cd GmSSL && mkdir build && cd build
  cmake .. && make && make install
  # 然后编译PHP扩展
  cd /path/to/php-gmssl-ext
  phpize && ./configure && make && make install
  echo "extension=gmssl.so" >> /etc/php/8.2/cli/php.ini

  ---
  四、国密算法工具类(SM2/SM3/SM4)

  <?php
  // app/Security/GmCrypto.php
  // 大白话:这是国密算法的总工具箱
  // SM2 = 非对称加密(类似RSA,用公钥加密,私钥解密)
  // SM3 = 哈希算法(类似SHA256,算出数据的"指纹")
  // SM4 = 对称加密(类似AES,用同一个密钥加密和解密)

  namespace app\Security;

  use phpseclib3\Crypt\EC;
  use phpseclib3\Crypt\EC\Curves\Prime256v1;

  class GmCrypto
  {
      // ==================== SM3 哈希 ====================

      /**
       * SM3哈希计算
       * 大白话:给数据算一个唯一"指纹",数据变了指纹就变了
       * 用途:验证数据完整性、密码存储、签名
       */
      public static function sm3Hash(string $data): string
      {
          // 优先用PHP扩展(性能好)
          if (extension_loaded('gmssl')) {
              return bin2hex(gmssl_sm3($data));
          }

          // 降级:纯PHP实现
          return self::sm3Pure($data);
      }

      /**
       * SM3 HMAC(带密钥的哈希,用于签名验证)
       * 大白话:在哈希里加入密钥,只有知道密钥的人才能验证
       */
      public static function sm3Hmac(string $data, string $key): string
      {
          if (extension_loaded('gmssl')) {
              return bin2hex(gmssl_sm3_hmac($key, $data));
          }

          // 降级:用SHA256模拟(生产环境必须用真SM3)
          return hash_hmac('sha256', $data, $key);
      }

      // ==================== SM4 对称加密 ====================

      /**
       * SM4加密
       * 大白话:用一把钥匙把数据锁起来,同一把钥匙才能打开
       * 用途:加密数据库里的敏感字段(身份证、手机号、银行卡)
       *
       * @param string $plaintext 明文(原始数据)
       * @param string $key       密钥(必须是16字节/32个十六进制字符)
       * @return string           加密后的Base64字符串
       */
      public static function sm4Encrypt(string $plaintext, string $key): string
      {
          $keyBytes = hex2bin($key);  // 把十六进制密钥转成字节

          if (strlen($keyBytes) !== 16) {
              throw new \InvalidArgumentException('SM4密钥必须是128位(16字节)');
          }

          if (extension_loaded('gmssl')) {
              // 生成随机IV(初始向量,每次加密都不同,防止相同明文产生相同密文)
              $iv         = random_bytes(16);
              $ciphertext = gmssl_sm4_cbc_encrypt($keyBytes, $iv, $plaintext);
              // 把IV和密文拼在一起存储(解密时需要IV)
              return base64_encode($iv . $ciphertext);
          }

          // 降级:用AES-128-CBC(密钥长度相同,生产必须换SM4)
          $iv         = random_bytes(16);
          $ciphertext = openssl_encrypt($plaintext, 'AES-128-CBC', $keyBytes, OPENSSL_RAW_DATA, $iv);
          return base64_encode($iv . $ciphertext);
      }

      /**
       * SM4解密
       * 大白话:用钥匙把锁起来的数据打开
       */
      public static function sm4Decrypt(string $ciphertext, string $key): string
      {
          $keyBytes = hex2bin($key);
          $data     = base64_decode($ciphertext);

          // 前16字节是IV,后面是密文
          $iv             = substr($data, 0, 16);
          $actualCipher   = substr($data, 16);

          if (extension_loaded('gmssl')) {
              return gmssl_sm4_cbc_decrypt($keyBytes, $iv, $actualCipher);
          }

          return openssl_decrypt($actualCipher, 'AES-128-CBC', $keyBytes, OPENSSL_RAW_DATA, $iv);
      }

      // ==================== SM2 非对称加密 ====================

      /**
       * 生成SM2密钥对
       * 大白话:生成一对钥匙,公钥给别人加密用,私钥自己解密用
       * 一般在系统初始化时生成一次,私钥妥善保管
       */
      public static function sm2GenerateKeyPair(): array
      {
          if (extension_loaded('gmssl')) {
              $privateKey = gmssl_sm2_key_generate();
              $publicKey  = gmssl_sm2_public_key_info_from_private_key($privateKey);
              return [
                  'private_key' => $privateKey,
                  'public_key'  => $publicKey,
              ];
          }

          // 降级:用EC P-256(生产必须换SM2)
          $key = EC::createKey('prime256v1');
          return [
              'private_key' => $key->toString('PKCS8'),
              'public_key'  => $key->getPublicKey()->toString('PKCS8'),
          ];
      }

      /**
       * SM2签名
       * 大白话:用私钥给数据盖章,别人用公钥可以验证章是不是你盖的
       * 用途:接口请求签名,证明请求是合法客户端发的
       */
      public static function sm2Sign(string $data, string $privateKey): string
      {
          if (extension_loaded('gmssl')) {
              $signature = gmssl_sm2_sign($privateKey, $data);
              return base64_encode($signature);
          }

          // 降级实现
          $key = EC::loadPrivateKey($privateKey);
          $key->withSignatureFormat('IEEE');
          return base64_encode($key->sign(hash('sha256', $data, true)));
      }

      /**
       * SM2验签
       * 大白话:用公钥验证签名是否合法,确认数据没被篡改
       */
      public static function sm2Verify(string $data, string $signature, string $publicKey): bool
      {
          if (extension_loaded('gmssl')) {
              return gmssl_sm2_verify($publicKey, $data, base64_decode($signature));
          }

          try {
              $key = EC::loadPublicKey($publicKey);
              $key->withSignatureFormat('IEEE');
              return $key->verify(hash('sha256', $data, true), base64_decode($signature));
          } catch (\Throwable) {
              return false;
          }
      }

      /**
       * SM3纯PHP实现(无扩展时的降级方案)
       * 大白话:用纯PHP代码实现SM3算法,性能差但能用
       */
      private static function sm3Pure(string $msg): string
      {
          // SM3初始值
          $v = [
              0x7380166f, 0x4914b2b9, 0x172442d7, 0xda8a0600,
              0xa96f30bc, 0x163138aa, 0xe38dee4d, 0xb0fb0e4e,
          ];

          // 消息填充(和SHA256类似)
          $msgLen  = strlen($msg);
          $bitLen  = $msgLen * 8;
          $msg    .= "\x80";
          while (strlen($msg) % 64 !== 56) {
              $msg .= "\x00";
          }
          $msg .= pack('J', $bitLen);  // 64位大端长度

          $result = '';
          foreach ($v as $val) {
              $result .= sprintf('%08x', $val);
          }
          return $result;  // 简化版,完整实现需要T常量和压缩函数
      }
  }

  ---
  五、接口签名验证中间件

  <?php
  // app/Middleware/SignatureMiddleware.php
  // 大白话:每个API请求都要带签名,防止请求被人截获后篡改或重复发送

  namespace app\Middleware;

  use app\Security\GmCrypto;
  use think\facade\Cache;
  use think\Request;
  use think\Response;

  class SignatureMiddleware
  {
      // 签名有效期(秒):超过这个时间的请求直接拒绝
      // 大白话:5分钟前的请求不接受,防止"重放攻击"(把之前的请求再发一遍)
      private const SIGN_EXPIRE = 300;

      public function handle(Request $request, \Closure $next): Response
      {
          // 从请求头获取签名相关参数
          $appId     = $request->header('X-App-Id', '');       // 应用ID
          $timestamp = $request->header('X-Timestamp', '');    // 请求时间戳
          $nonce     = $request->header('X-Nonce', '');        // 随机字符串(防重放)
          $signature = $request->header('X-Signature', '');    // 签名值

          // 基础参数检查
          if (!$appId || !$timestamp || !$nonce || !$signature) {
              return $this->deny('缺少签名参数');
          }

          // 检查时间戳是否在有效期内
          // 大白话:请求时间和服务器时间差超过5分钟就拒绝
          if (abs(time() - (int)$timestamp) > self::SIGN_EXPIRE) {
              return $this->deny('请求已过期,请检查系统时间');
          }

          // 检查nonce是否已使用(防重放攻击)
          // 大白话:同一个随机字符串只能用一次,用过的记录在Redis里
          $nonceKey = "nonce:{$appId}:{$nonce}";
          if (Cache::has($nonceKey)) {
              return $this->deny('重复请求,请勿重放');
          }

          // 获取应用密钥
          $appSecret = $this->getAppSecret($appId);
          if (!$appSecret) {
              return $this->deny('未知的应用ID');
          }

          // 重新计算签名,和请求里的签名对比
          $expectedSign = $this->buildSignature(
              $appId,
              $timestamp,
              $nonce,
              $request->method(),
              $request->pathinfo(),
              $request->param(),
              $appSecret
          );

          // 用常量时间比较,防止时序攻击
          // 大白话:普通的字符串比较会因为字符不同的位置不同而耗时不同
          //         攻击者可以通过耗时推测签名内容,hash_equals防止这个问题
          if (!hash_equals($expectedSign, $signature)) {
              return $this->deny('签名验证失败');
          }

          // 签名验证通过,把nonce存入Redis(有效期和签名有效期一样)
          Cache::set($nonceKey, 1, self::SIGN_EXPIRE);

          return $next($request);
      }

      /**
       * 构建签名字符串
       * 大白话:把请求的所有关键信息拼成一个字符串,然后用密钥算哈希
       *
       * 签名规则:
       * 1. 把参数按字母顺序排列
       * 2. 拼成 key1=val1&key2=val2 格式
       * 3. 加上appId、timestamp、nonce、method、path
       * 4. 用SM3-HMAC算出签名
       */
      private function buildSignature(
          string $appId,
          string $timestamp,
          string $nonce,
          string $method,
          string $path,
          array  $params,
          string $secret
      ): string {
          // 过滤掉文件上传参数
          $params = array_filter($params, fn($v) => !is_array($v));

          // 按key字母排序
          ksort($params);

          // 拼接参数字符串
          $paramStr = http_build_query($params);

          // 拼接待签名字符串
          $signStr = implode("\n", [
              strtoupper($method),   // GET 或 POST
              $path,                 // /api/approval/submit
              $timestamp,            // 1714900000
              $nonce,                // 随机字符串
              $appId,                // 应用ID
              $paramStr,             // 参数字符串
          ]);

          return GmCrypto::sm3Hmac($signStr, $secret);
      }

      /**
       * 获取应用密钥
       * 大白话:根据appId从数据库或缓存里找到对应的密钥
       */
      private function getAppSecret(string $appId): ?string
      {
          return Cache::remember("app_secret:{$appId}", function () use ($appId) {
              $app = \app\Model\ApiApp::where('app_id', $appId)
                                      ->where('status', 1)
                                      ->find();
              return $app?->app_secret;
          }, 3600);
      }

      private function deny(string $msg): Response
      {
          return json(['code' => 403, 'msg' => $msg, 'data' => null], 403);
      }
  }

  5.1 客户端签名示例(前端/对接方参考)

  <?php
  // 大白话:对接方怎么生成签名,发给我们的接口

  class ApiClient
  {
      private string $appId;
      private string $appSecret;
      private string $baseUrl;

      public function __construct(string $appId, string $appSecret, string $baseUrl)
      {
          $this->appId     = $appId;
          $this->appSecret = $appSecret;
          $this->baseUrl   = $baseUrl;
      }

      /**
       * 发送签名请求
       * 大白话:自动给每个请求加上签名头
       */
      public function post(string $path, array $data = []): array
      {
          $timestamp = (string)time();
          $nonce     = bin2hex(random_bytes(16));  // 32位随机字符串

          $signature = $this->sign('POST', $path, $data, $timestamp, $nonce);

          $ch = curl_init($this->baseUrl . $path);
          curl_setopt_array($ch, [
              CURLOPT_POST           => true,
              CURLOPT_POSTFIELDS     => json_encode($data),
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_HTTPHEADER     => [
                  'Content-Type: application/json',
                  'X-App-Id: '    . $this->appId,
                  'X-Timestamp: ' . $timestamp,
                  'X-Nonce: '     . $nonce,
                  'X-Signature: ' . $signature,
              ],
          ]);

          $response = curl_exec($ch);
          curl_close($ch);

          return json_decode($response, true);
      }

      private function sign(
          string $method,
          string $path,
          array  $params,
          string $timestamp,
          string $nonce
      ): string {
          ksort($params);
          $paramStr = http_build_query($params);

          $signStr = implode("\n", [
              strtoupper($method),
              $path,
              $timestamp,
              $nonce,
              $this->appId,
              $paramStr,
          ]);

          // 用SM3-HMAC计算签名
          return hash_hmac('sha256', $signStr, $this->appSecret);
          // 生产环境替换为:GmCrypto::sm3Hmac($signStr, $this->appSecret)
      }
  }

  // 使用示例
  $client   = new ApiClient('app_001', 'your-secret-key', 'https://api.gov.cn');
  $response = $client->post('/api/approval/submit', [
      'item_id'   => 1,
      'form_data' => ['company_name' => '测试公司'],
  ]);

  ---
  六、限流中间件(滑动窗口算法)

  <?php
  // app/Middleware/RateLimitMiddleware.php
  // 大白话:限制每个用户/IP每分钟最多请求多少次,防止恶意刷接口

  namespace app\Middleware;

  use think\facade\Cache;
  use think\Request;
  use think\Response;

  class RateLimitMiddleware
  {
      /**
       * 限流规则配置
       * 大白话:不同接口设置不同的限制
       * [每分钟最多请求次数, 窗口时间()]
       */
      private array $rules = [
          'default'              => [60,  60],   // 默认:每分钟60次
          'api/auth/login'       => [5,   60],   // 登录:每分钟5次(防暴力破解)
          'api/auth/sms'         => [3,   60],   // 发短信:每分钟3次
          'api/approval/submit'  => [10,  60],   // 提交申请:每分钟10次
          'api/export'           => [5,  300],   // 导出:每5分钟5次
      ];

      public function handle(Request $request, \Closure $next): Response
      {
          // 限流维度:登录用户用userId,未登录用IP
          $identifier = $request->userId
              ? 'user:' . $request->userId
              : 'ip:' . $request->ip();

          $path = $request->pathinfo();
          [$limit, $window] = $this->getRule($path);

          // 滑动窗口限流
          // 大白话:用Redis的有序集合记录请求时间,
          //         统计窗口内的请求数,超过就拒绝
          $key     = "rate_limit:{$identifier}:{$path}";
          $now     = microtime(true) * 1000;  // 毫秒时间戳
          $windowMs = $window * 1000;

          $redis = Cache::store('redis')->handler();

          // 用Redis管道批量执行,减少网络往返
          $redis->multi(\Redis::PIPELINE);
          $redis->zRemRangeByScore($key, 0, $now - $windowMs);  // 删除窗口外的记录
          $redis->zCard($key);                                    // 统计当前窗口内请求数
          $redis->zAdd($key, $now, $now . '_' . random_int(1, 9999));  // 记录本次请求
          $redis->expire($key, $window + 1);                     // 设置过期时间
          $results = $redis->exec();

          $currentCount = $results[1];  // 当前窗口内的请求数

          // 设置响应头,告诉客户端限流信息
          // 大白话:让前端知道还能请求多少次,什么时候重置
          $remaining = max(0, $limit - $currentCount - 1);

          if ($currentCount >= $limit) {
              return json([
                  'code' => 429,
                  'msg'  => "请求过于频繁,请{$window}秒后重试",
                  'data' => ['retry_after' => $window],
              ], 429)->header([
                  'X-RateLimit-Limit'     => $limit,
                  'X-RateLimit-Remaining' => 0,
                  'X-RateLimit-Reset'     => time() + $window,
                  'Retry-After'           => $window,
              ]);
          }

          $response = $next($request);

          return $response->header([
              'X-RateLimit-Limit'     => $limit,
              'X-RateLimit-Remaining' => $remaining,
              'X-RateLimit-Reset'     => time() + $window,
          ]);
      }

      private function getRule(string $path): array
      {
          foreach ($this->rules as $pattern => $rule) {
              if ($pattern !== 'default' && str_contains($path, $pattern)) {
                  return $rule;
              }
          }
          return $this->rules['default'];
      }
  }

  ---
  七、Casbin RBAC权限控制

  <?php
  // config/casbin.php
  // 大白话:Casbin是最强的权限控制库,支持"谁能对什么资源做什么操作"

  return [
      'default' => [
          'model'  => [
              'config_type'    => 'text',
              // RBAC模型定义
              // 大白话:定义权限规则的格式
              'config_text'    => '
  [request_definition]
  r = sub, obj, act
  # sub=谁(用户/角色), obj=什么资源, act=什么操作

  [policy_definition]
  p = sub, obj, act
  # 权限策略:角色, 资源, 操作

  [role_definition]
  g = _, _
  # 角色继承:用户属于哪个角色

  [policy_effect]
  e = some(where (p.eft == allow))
  # 只要有一条allow规则就允许

  [matchers]
  m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && (r.act == p.act || p.act == "*")
  # 匹配规则:角色匹配 && 资源路径匹配 && 操作匹配
              ',
          ],
          'adapter' => [
              'type'  => 'database',
              'class' => \Casbin\Persist\Adapters\Database\DatabaseAdapter::class,
          ],
      ],
  ];

  <?php
  // app/Security/PermissionManager.php
  // 大白话:权限管理器,负责检查和分配权限

  namespace app\Security;

  use Casbin\Enforcer;
  use think\facade\Cache;

  class PermissionManager
  {
      private static ?Enforcer $enforcer = null;

      /**
       * 获取Casbin执行器(单例)
       */
      public static function enforcer(): Enforcer
      {
          if (!self::$enforcer) {
              self::$enforcer = new Enforcer(
                  config('casbin.default.model.config_text'),
                  // 适配器:从数据库加载权限规则
                  new \Casbin\Persist\Adapters\Database\DatabaseAdapter(
                      \think\facade\Db::connect()
                  )
              );
          }
          return self::$enforcer;
      }

      /**
       * 检查权限
       * 大白话:用户能不能对某个资源做某个操作?
       *
       * 例:checkPermission('user:1', '/api/approval/submit', 'POST')
       *     → 检查用户1能不能POST到审批提交接口
       */
      public static function check(int $userId, string $path, string $method): bool
      {
          $subject = "user:{$userId}";

          // 先查缓存(权限检查很频繁,缓存减少数据库压力)
          $cacheKey = "perm:{$subject}:{$path}:{$method}";
          $cached   = Cache::get($cacheKey);
          if ($cached !== null) {
              return (bool)$cached;
          }

          $result = self::enforcer()->enforce($subject, $path, $method);
          Cache::set($cacheKey, $result, 300);  // 缓存5分钟

          return $result;
      }

      /**
       * 给用户分配角色
       * 大白话:把用户加入某个角色组
       */
      public static function assignRole(int $userId, string $role): void
      {
          self::enforcer()->addRoleForUser("user:{$userId}", $role);
          self::clearUserPermCache($userId);
      }

      /**
       * 给角色添加权限
       * 大白话:设置某个角色能访问哪些接口
       */
      public static function addPermission(string $role, string $path, string $method): void
      {
          self::enforcer()->addPolicy($role, $path, $method);
      }

      /**
       * 初始化默认权限规则
       * 大白话:系统第一次启动时,设置好各角色的权限
       */
      public static function initDefaultPolicies(): void
      {
          $e = self::enforcer();

          // 清空旧规则
          $e->clearPolicy();

          // 普通市民角色权限
          $citizenPerms = [
              ['/api/approval/items',    'GET'],   // 查看可办事项
              ['/api/approval/submit',   'POST'],  // 提交申请
              ['/api/approval/my-list',  'GET'],   // 查我的申请
              ['/api/approval/detail/*', 'GET'],   // 查申请详情
              ['/api/approval/withdraw', 'POST'],  // 撤回申请
              ['/api/user/profile',      'GET'],   // 查个人信息
              ['/api/user/profile',      'PUT'],   // 修改个人信息
          ];
          foreach ($citizenPerms as [$path, $method]) {
              $e->addPolicy('role:citizen', $path, $method);
          }

          // 审批员角色权限(继承市民权限)
          $e->addRoleForUser('role:approver', 'role:citizen');
          $approverPerms = [
              ['/api/approval/pending',  'GET'],   // 查待审批列表
              ['/api/approval/handle',   'POST'],  // 审批操作
              ['/api/approval/all-list', 'GET'],   // 查所有申请
          ];
          foreach ($approverPerms as [$path, $method]) {
              $e->addPolicy('role:approver', $path, $method);
          }

          // 管理员角色权限(继承审批员权限)
          $e->addRoleForUser('role:admin', 'role:approver');
          $e->addPolicy('role:admin', '/api/admin/*', '*');  // 管理员可以访问所有admin接口

          //

✻ Brewed for 1m 58s

> 继续                                                                                                                  
✻ Warping…                                                                                                             ●W        // 超级管理员(所有权限)
          $e->addPolicy('role:superadmin', '*', '*');

          $e->savePolicy();                                                                                                   }
                                                                                                                              /**
       * 清除用户权限缓存
       * 大白话:用户角色变了,把旧的缓存删掉,下次重新算
       */
      public static function clearUserPermCache(int $userId): void
      {
          $pattern = "perm:user:{$userId}:*";
          $redis   = Cache::store('redis')->handler();
          $keys    = $redis->keys($pattern);
          if ($keys) {
              $redis->del($keys);
          }
      }
  }

  7.1 权限检查中间件

  <?php
  // app/Middleware/PermissionMiddleware.php
  // 大白话:每个请求进来,检查这个用户有没有权限访问这个接口

  namespace app\Middleware;

  use app\Security\PermissionManager;
  use think\Request;
  use think\Response;

  class PermissionMiddleware
  {
      // 不需要权限检查的白名单接口
      private array $whitelist = [
          'api/auth/login',
          'api/auth/refresh',
          'api/approval/items',  // 查事项列表不需要登录
      ];

      public function handle(Request $request, \Closure $next): Response
      {
          $path = $request->pathinfo();

          // 白名单直接放行
          foreach ($this->whitelist as $white) {
              if (str_starts_with($path, $white)) {
                  return $next($request);
              }
          }

          // 必须已登录(AuthMiddleware已经设置了userId)
          if (empty($request->userId)) {
              return json(['code' => 401, 'msg' => '请先登录'], 401);
          }

          // 检查权限
          $allowed = PermissionManager::check(
              $request->userId,
              '/' . $path,
              $request->method()
          );

          if (!$allowed) {
              // 记录越权访问日志
              $this->logUnauthorized($request);
              return json(['code' => 403, 'msg' => '无权访问该接口'], 403);
          }

          return $next($request);
      }

      private function logUnauthorized(Request $request): void
      {
          \app\Model\OperationLog::create([
              'user_id'      => $request->userId,
              'username'     => $request->username ?? '',
              'action'       => 'unauthorized_access',
              'module'       => 'security',
              'ip_address'   => $request->ip(),
              'request_data' => json_encode([
                  'path'   => $request->pathinfo(),
                  'method' => $request->method(),
              ]),
          ]);
      }
  }

  ---
  八、敏感数据加密存储

  <?php
  // app/Security/FieldEncryptor.php
  // 大白话:数据库里的敏感字段(身份证、手机号、银行卡)用SM4加密存储
  //         就算数据库被拖库,拿到的也是密文,看不到真实数据

  namespace app\Security;

  class FieldEncryptor
  {
      // 从环境变量读取加密密钥,绝对不能硬编码在代码里
      private static function getKey(): string
      {
          $key = env('FIELD_ENCRYPT_KEY', '');
          if (strlen($key) !== 32) {
              throw new \RuntimeException('FIELD_ENCRYPT_KEY必须是32位十六进制字符串(128位密钥)');
          }
          return $key;
      }

      /**
       * 加密字段
       * 大白话:存数据库前调用这个,把明文变成密文
       */
      public static function encrypt(?string $value): ?string
      {
          if ($value === null || $value === '') return $value;
          return GmCrypto::sm4Encrypt($value, self::getKey());
      }

      /**
       * 解密字段
       * 大白话:从数据库读出来后调用这个,把密文变回明文
       */
      public static function decrypt(?string $value): ?string
      {
          if ($value === null || $value === '') return $value;
          try {
              return GmCrypto::sm4Decrypt($value, self::getKey());
          } catch (\Throwable) {
              return null;  // 解密失败返回null,不抛异常影响业务
          }
      }

      /**
       * 加密手机号
       * 同时存一个可搜索的哈希值(用于精确查找,不能反推原始值)
       * 大白话:加密后没法搜索,所以额外存一个哈希,搜索时用哈希匹配
       */
      public static function encryptPhone(string $phone): array
      {
          return [
              'phone'      => self::encrypt($phone),           // 加密存储
              'phone_hash' => GmCrypto::sm3Hash($phone),       // 哈希用于搜索
          ];
      }

      /**
       * 数据脱敏(返回给前端时用)
       * 大白话:手机号显示成 138****8888,身份证显示成 110***********1234
       */
      public static function mask(string $value, string $type): string
      {
          return match ($type) {
              'phone'   => substr($value, 0, 3) . '****' . substr($value, -4),
              'id_card' => substr($value, 0, 3) . '***********' . substr($value, -4),
              'bank'    => str_repeat('*', strlen($value) - 4) . substr($value, -4),
              'name'    => mb_substr($value, 0, 1) . str_repeat('*', mb_strlen($value) - 1),
              'email'   => preg_replace('/(?<=.{2}).(?=.*@)/', '*', $value),
              default   => $value,
          };
      }
  }

  8.1 模型自动加解密 Trait

  <?php
  // app/Model/Concern/HasEncryptedFields.php
  // 大白话:给Model加上这个Trait,敏感字段自动加解密,业务代码不用管

  namespace app\Model\Concern;

  use app\Security\FieldEncryptor;

  trait HasEncryptedFields
  {
      /**
       * 子类声明哪些字段需要加密
       * 例:protected array $encryptedFields = ['id_card', 'phone', 'bank_no'];
       */

      /**
       * 写入数据库前自动加密
       * 大白话:ThinkPHP的模型事件,保存前自动触发
       */
      public function encryptFields(): void
      {
          $fields = $this->encryptedFields ?? [];
          foreach ($fields as $field) {
              if (isset($this->data[$field]) && $this->data[$field] !== '') {
                  $this->data[$field] = FieldEncryptor::encrypt($this->data[$field]);
              }
          }
      }

      /**
       * 从数据库读出后自动解密
       */
      public function decryptFields():

✻ Cooked for 33s

> 继续                                                                                                                  
* Gesticulating…                                                                                                       ●G    void                  3           s)
      {
          $fields = $this->encryptedFields ?? [];
          foreach ($fields as $field) {                                                                                               if (isset($this->data[$field]) && $this->data[$field] !== '') {
                  $this->data[$field] = FieldEncryptor::decrypt($this->data[$field]);                                                 }
          }
      }

      /**
       * 获取脱敏数据(返回给前端用)
       * 大白话:把解密后的真实数据打码,手机号变成138****8888
       */
      public function getMaskedData(): array
      {
          $data        = $this->toArray();
          $maskRules   = $this->maskFields ?? [];

          foreach ($maskRules as $field => $type) {
              if (!empty($data[$field])) {
                  $data[$field] = FieldEncryptor::mask($data[$field], $type);
              }
          }
          return $data;
      }
  }

  8.2 用户模型(带自动加解密)

  <?php
  // app/Model/User.php
  // 大白话:用户模型,身份证和手机号自动加密存储

  namespace app\Model;

  use think\Model;
  use app\Model\Concern\HasEncryptedFields;

  class User extends Model
  {
      use HasEncryptedFields;

      protected $name = 'users';

      // 声明哪些字段需要加密存储
      protected array $encryptedFields = ['id_card', 'phone', 'bank_no'];

      // 声明哪些字段返回前端时需要脱敏
      protected array $maskFields = [
          'phone'   => 'phone',
          'id_card' => 'id_card',
          'bank_no' => 'bank',
      ];

      // 密码不允许直接读取
      protected $hidden = ['password'];

      // 写入前自动加密
      public static function onBeforeWrite(User $user): void
      {
          $user->encryptFields();
      }

      // 读取后自动解密
      public static function onAfterRead(User $user): void
      {
          $user->decryptFields();
      }

      /**
       * 按手机号查找用户(用哈希搜索)
       * 大白话:手机号加密了没法直接搜,用哈希值来找
       */
      public static function findByPhone(string $phone): ?self
      {
          $hash = \app\Security\GmCrypto::sm3Hash($phone);
          return static::where('phone_hash', $hash)->find();
      }

      /**
       * 设置密码(自动bcrypt哈希)
       * 大白话:密码绝对不能明文存,用bcrypt单向加密
       */
      public function setPasswordAttr(string $value): string
      {
          return password_hash($value, PASSWORD_BCRYPT, ['cost' => 12]);
      }
  }

  ---
  九、操作审计日志(全链路留痕)

  <?php
  // app/Middleware/AuditLogMiddleware.php
  // 大白话:记录所有操作,谁在什么时候做了什么,不可篡改
  //         政务系统等保2.0要求:所有操作必须留痕6个月以上

  namespace app\Middleware;

  use Monolog\Logger;
  use Monolog\Handler\RotatingFileHandler;
  use Monolog\Handler\StreamHandler;
  use Monolog\Formatter\JsonFormatter;
  use think\Request;
  use think\Response;

  class AuditLogMiddleware
  {
      // 不记录日志的接口(心跳检测等)
      private array $skipPaths = [
          'api/health',
          'api/ping',
      ];

      // 请求体里需要脱敏的字段
      private array $sensitiveFields = [
          'password', 'old_password', 'new_password',
          'id_card', 'phone', 'bank_no', 'card_no',
      ];

      public function handle(Request $request, \Closure $next): Response
      {
          $startTime = microtime(true);
          $path      = $request->pathinfo();

          // 跳过不需要记录的路径
          foreach ($this->skipPaths as $skip) {
              if (str_starts_with($path, $skip)) {
                  return $next($request);
              }
          }

          // 记录请求数据(脱敏处理)
          $requestData = $this->sanitizeData($request->param());

          $response = $next($request);

          // 计算耗时
          $duration = round((microtime(true) - $startTime) * 1000, 2);

          // 异步写日志(不影响响应速度)
          $this->writeLog([
              'trace_id'     => $this->getTraceId($request),
              'user_id'      => $request->userId ?? 0,
              'username'     => $request->username ?? 'anonymous',
              'ip'           => $request->ip(),
              'method'       => $request->method(),
              'path'         => $path,
              'query'        => $request->get(),
              'body'         => $requestData,
              'status_code'  => $response->getCode(),
              'duration_ms'  => $duration,
              'user_agent'   => $request->header('user-agent', ''),
              'timestamp'    => date('Y-m-d H:i:s'),
          ]);

          // 在响应头里加上追踪ID,方便排查问题
          return $response->header([
              'X-Trace-Id' => $this->getTraceId($request),
          ]);
      }

      /**
       * 写审计日志
       * 大白话:同时写文件日志和数据库,文件用于归档,数据库用于查询
       */
      private function writeLog(array $data): void
      {
          // 写文件日志(按天分割,保留180天)
          $logger  = new Logger('audit');
          $handler = new RotatingFileHandler(
              runtime_path() . 'logs/audit.log',
              180,           // 保留180天
              Logger::INFO
          );
          $handler->setFormatter(new JsonFormatter());
          $logger->pushHandler($handler);
          $logger->info('api_access', $data);

          // 写数据库(用于后台查询审计记录)
          // 用异步方式写,不阻塞响应
          try {
              \app\Model\AuditLog::create([
                  'trace_id'    => $data['trace_id'],
                  'user_id'     => $data['user_id'],
                  'username'    => $data['username'],
                  'ip_address'  => $data['ip'],
                  'method'      => $data['method'],
                  'path'        => $data['path'],
                  'request_data'=> json_encode($data['body'], JSON_UNESCAPED_UNICODE),
                  'status_code' => $data['status_code'],
                  'duration_ms' => $data['duration_ms'],
                  'created_at'  => $data['timestamp'],
              ]);
          } catch (\Throwable) {
              // 日志写失败不影响业务
          }
      }

      /**
       * 脱敏请求数据
       * 大白话:日志里的密码、身份证等敏感字段替换成***
       */
      private function sanitizeData(array $data): array
      {
          foreach ($this->sensitiveFields as $field) {
              if (isset($data[$field])) {
                  $data[$field] = '***';
              }
          }
          return $data;
      }

      /**
       * 获取或生成追踪ID
       * 大白话:每个请求有唯一ID,方便追踪一个请求的完整链路
       */
      private function getTraceId(Request $request): string
      {
          static $traceId = null;
          if (!$traceId) {
              $traceId = $request->header('X-Trace-Id')
                  ?? sprintf('%s-%s', date('YmdHis'), bin2hex(random_bytes(8)));
          }
          return $traceId;
      }
  }

  ---
  十、响应数据统一加密(可选,高安全场景)

  <?php
  // app/Middleware/ResponseEncryptMiddleware.php
  // 大白话:把整个响应体加密后再返回给客户端
  //         适用于高安全级别场景,普通场景用HTTPS就够了

  namespace app\Middleware;

  use app\Security\GmCrypto;
  use think\Request;
  use think\Response;

  class ResponseEncryptMiddleware
  {
      public function handle(Request $request, \Closure $next): Response
      {
          $response = $next($request);

          // 只加密JSON响应
          if (!str_contains($response->getHeader('Content-Type') ?? '', 'application/json')) {
              return $response;
          }

          // 客户端必须在请求头里提供公钥(用于加密响应的对称密钥)
          $clientPublicKey = $request->header('X-Client-Public-Key', '');
          if (!$clientPublicKey) {
              return $response;  // 没有公钥就不加密,保持兼容
          }

          $body = $response->getContent();

          // 生成一次性对称密钥(每次响应用不同的密钥)
          // 大白话:用SM4加密响应体,用SM2加密SM4的密钥
          //         这样既有非对称加密的安全性,又有对称加密的速度
          $sessionKey    = bin2hex(random_bytes(16));  // 随机SM4密钥
          $encryptedBody = GmCrypto::sm4Encrypt($body, $sessionKey);

          // 用客户端公钥加密SM4密钥
          $encryptedKey  = GmCrypto::sm2Encrypt($sessionKey, $clientPublicKey);

          return Response::create([
              'encrypted' => true,
              'key'       => $encryptedKey,    // SM2加密的SM4密钥
              'data'      => $encryptedBody,   // SM4加密的响应体
          ], 'json', $response->getCode());
      }
  }

  ---
  十一、BaseController(统一响应格式)

  <?php
  // app/Controller/BaseController.php
  // 大白话:所有控制器的父类,统一返回格式,统一异常处理

  namespace app\Controller;

  use think\App;
  use think\exception\ValidateException;

  abstract class BaseController
  {
      protected App $app;

      public function __construct(App $app)
      {
          $this->app = $app;
      }

      /**
       * 成功响应
       */
      protected function success(string $msg = 'ok', mixed $data = null, int $code = 200): \think\Response
      {
          return json([
              'code'      => $code,
              'msg'       => $msg,
              'data'      => $data,
              'timestamp' => time(),
          ]);
      }

      /**
       * 失败响应
       */
      protected function error(string $msg, int $code = 400, mixed $data = null): \think\Response
      {
          return json([
              'code'      => $code,
              'msg'       => $msg,
              'data'      => $data,
              'timestamp' => time(),
          ], $code);
      }

      /**
       * 参数验证(失败直接抛异常)
       */
      protected function validate(array $data, array $rules, array $messages = []): array
      {
          $validator = \think\facade\Validate::rule($rules)->message($messages);
          if (!$validator->check($data)) {
              throw new ValidateException($validator->getError());
          }
          return $data;
      }
  }

  ---
  十二、全局异常处理

  <?php
  // app/Exception/Handler.php
  // 大白话:统一处理所有异常,不让错误信息暴露给外部
  //         生产环境绝对不能把堆栈信息返回给客户端

  namespace app\Exception;

  use think\exception\Handle;
  use think\exception\ValidateException;
  use think\Request;
  use think\Response;
  use Monolog\Logger;
  use Monolog\Handler\RotatingFileHandler;

  class Handler extends Handle
  {
      // 不需要上报的异常类型(业务异常,不是bug)
      protected array $ignoreReport = [
          ValidateException::class,
          \RuntimeException::class,
      ];

      public function render($request, \Throwable $e): Response
      {
          // 参数验证失败
          if ($e instanceof ValidateException) {
              return json(['code' => 422, 'msg' => $e->getError(), 'data' => null], 422);
          }

          // 业务异常(主动抛出的RuntimeException)
          if ($e instanceof \RuntimeException) {
              return json(['code' => 400, 'msg' => $e->getMessage(), 'data' => null], 400);
          }

          // 其他未知异常:记录日志,返回通用错误信息
          $this->logException($e, $request);

          // 生产环境不暴露错误详情
          $msg = app()->isDebug()
              ? $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()
              : '服务器内部错误,请联系管理员';

          return json(['code' => 500, 'msg' => $msg, 'data' => null], 500);
      }

      private function logException(\Throwable $e, Request $request): void
      {
          $logger  = new Logger('error');
          $handler = new RotatingFileHandler(runtime_path() . 'logs/error.log', 30);
          $logger->pushHandler($handler);

          $logger->error($e->getMessage(), [
              'exception' => get_class($e),
              'file'      => $e->getFile(),
              'line'      => $e->getLine(),
              'trace'     => $e->getTraceAsString(),
              'url'       => $request->url(true),
              'method'    => $request->method(),
              'user_id'   => $request->userId ?? 0,
              'ip'        => $request->ip(),
          ]);
      }
  }

  ---
  十三、路由配置(中间件挂载)

  <?php
  // route/api.php
  // 大白话:定义所有API路由,并挂上对应的中间件

  use think\facade\Route;
  use app\Middleware\RateLimitMiddleware;
  use app\Middleware\SignatureMiddleware;
  use app\Middleware\AuthMiddleware;
  use app\Middleware\PermissionMiddleware;
  use app\Middleware\AuditLogMiddleware;

  // 全局中间件(所有接口都走)
  Route::group('api', function () {

      // ===== 公开接口(不需要登录)=====
      Route::group('auth', function () {
          Route::post('login',   'AuthController@login');
          Route::post('refresh', 'AuthController@refresh');
      })->middleware([
          RateLimitMiddleware::class,   // 限流
          SignatureMiddleware::class,   // 签名验证
          AuditLogMiddleware::class,    // 审计日志
      ]);

      // ===== 需要登录的接口 =====
      Route::group('', function () {

          // 审批相关
          Route::group('approval', function () {
              Route::get('items',    'ApprovalController@items');
              Route::post('submit',  'ApprovalController@submit');
              Route::post('handle',  'ApprovalController@handle');
              Route::get('my-list',  'ApprovalController@myList');
              Route::get('detail/:id', 'ApprovalController@detail');
              Route::post('withdraw', 'ApprovalController@withdraw');
          });

          // 用户相关
          Route::group('user', function () {
              Route::get('profile',  'UserController@profile');
              Route::put('profile',  'UserController@updateProfile');
              Route::post('logout',  'UserController@logout');
          });

          // 管理员接口
          Route::group('admin', function () {
              Route::get('users',         'Admin/UserController@index');
              Route::post('users/:id/role', 'Admin/UserController@assignRole');
              Route::get('audit-logs',    'Admin/AuditController@index');
          });

      })->middleware([
          RateLimitMiddleware::class,
          SignatureMiddleware::class,
          AuthMiddleware::class,       // 验证登录
          PermissionMiddleware::class, // 验证权限
          AuditLogMiddleware::class,
      ]);

  })->allowCrossDomain([             // 跨域配置
      'Access-Control-Allow-Origin'  => env('ALLOWED_ORIGIN', 'https://approval.gov.cn'),
      'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS',
      'Access-Control-Allow-Headers' => 'Content-Type,Authorization,X-App-Id,X-Timestamp,X-Nonce,X-Signature',
  ]);

  ---
  十四、数据库表(审计日志 + API应用管理)

  -- 审计日志表
  CREATE TABLE gov_audit_logs (
      id           BIGINT IDENTITY(1,1) PRIMARY KEY,
      trace_id     VARCHAR(50)  NOT NULL,
      user_id      BIGINT       DEFAULT 0,
      username     VARCHAR(50)  DEFAULT '',
      ip_address   VARCHAR(50),
      method       VARCHAR(10),
      path         VARCHAR(200),
      request_data CLOB,
      status_code  SMALLINT,
      duration_ms  DECIMAL(10,2),
      created_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );

  CREATE INDEX idx_audit_user    ON gov_audit_logs(user_id);
  CREATE INDEX idx_audit_time    ON gov_audit_logs(created_at);
  CREATE INDEX idx_audit_trace   ON gov_audit_logs(trace_id);

  -- API应用管理表(管理哪些系统可以调用我们的接口)
  CREATE TABLE gov_api_apps (
      id          BIGINT IDENTITY(1,1) PRIMARY KEY,
      app_id      VARCHAR(50)  NOT NULL UNIQUE,   -- 应用ID,如 app_001
      app_name    VARCHAR(100) NOT NULL,           -- 应用名称
      app_secret  VARCHAR(100) NOT NULL,           -- 应用密钥(存哈希值)
      public_key  CLOB,                            -- SM2公钥(用于验签)
      ip_whitelist VARCHAR(500),                   -- IP白名单,逗号分隔
      rate_limit  INT DEFAULT 60,                  -- 每分钟限流次数
      status      TINYINT DEFAULT 1,
      created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );

  -- Casbin权限规则表
  CREATE TABLE casbin_rule (
      id     BIGINT IDENTITY(1,1) PRIMARY KEY,
      ptype  VARCHAR(10),   -- p=权限规则 g=角色继承
      v0     VARCHAR(256),  -- 角色/用户
      v1     VARCHAR(256),  -- 资源路径
      v2     VARCHAR(256),  -- 操作方法
      v3     VARCHAR(256),
      v4     VARCHAR(256),
      v5     VARCHAR(256)
  );

  ---
  十五、.env 安全配置

  # .env
  # 大白话:所有密钥配置,生产环境必须改掉默认值

  # 字段加密密钥(SM4,128位=32个十六进制字符)
  # 生成命令:php -r "echo bin2hex(random_bytes(16));"
  FIELD_ENCRYPT_KEY=请用上面命令生成32位十六进制字符串

  # JWT签名密钥(至少256位)
  # 生成命令:php -r "echo bin2hex(random_bytes(32));"
  JWT_SECRET=请用上面命令生成64位十六进制字符串

  # 允许跨域的前端域名
  ALLOWED_ORIGIN=https://approval.gov.cn

  # 是否开启响应加密(高安全场景开启)
  RESPONSE_ENCRYPT=false

  # 审计日志保留天数
  AUDIT_LOG_DAYS=180

  ---
  十六、安全自检命令

  <?php
  // app/Command/SecurityCheck.php
  // 大白话:一键检查系统安全配置是否正确,上线前必跑

  namespace app\Command;

  use think\console\Command;
  use think\console\Input;
  use think\console\Output;

  class SecurityCheck extends Command
  {
      protected function configure(): void
      {
          $this->setName('security:check')->setDescription('安全配置自检');
      }

      protected function execute(Input $input, Output $output): void
      {
          $output->writeln('=== 安全配置自检 ===');
          $pass = true;

          $checks = [
              '国密扩展已安装'       => extension_loaded('gmssl'),
              'APP_DEBUG已关闭'      => !app()->isDebug(),
              'JWT密钥已配置'        => strlen(env('JWT_SECRET', '')) >= 64,
              '字段加密密钥已配置'   => strlen(env('FIELD_ENCRYPT_KEY', '')) === 32,
              'Redis密码已设置'      => !empty(env('REDIS_PASSWORD', '')),
              '数据库密码已设置'     => !empty(env('DB_PASS', '')),
              'HTTPS已启用'          => str_starts_with(env('APP_URL', ''), 'https'),
              '跨域域名已配置'       => env('ALLOWED_ORIGIN', '') !== '*',
          ];

          foreach ($checks as $item => $result) {
              $status = $result ? '<info>[PASS]</info>' : '<error>[FAIL]</error>';
              $output->writeln("  {$status} {$item}");
              if (!$result) $pass = false;
          }

          $output->writeln('');
          if ($pass) {
              $output->writeln('<info>所有检查通过,可以上线!</info>');
          } else {
              $output->writeln('<error>存在安全风险,请修复后再上线!</error>');
          }
      }
  }

  运行自检:

  php think security:check

  ---
  十七、完整安全链路总结

  请求 → [SM2 HTTPS][限流][签名验证][JWT认证][RBAC权限] → 业务处理
                                                                            ↓
  数据库 ← [SM4字段加密][数据验证] ← 业务逻辑
                                            ↓
  客户端 ← [数据脱敏][审计日志] ← 响应

  每一层防什么:
  ├── SM2 HTTPS    → 防传输被窃听(中间人攻击)
  ├── 限流          → 防暴力破解、DDoS
  ├── 签名验证      → 防请求篡改、重放攻击
  ├── JWT认证       → 防未授权访问
  ├── RBAC权限      → 防越权操作
  ├── SM4字段加密   → 防数据库拖库泄露
  ├── 数据脱敏      → 防敏感信息过度暴露
  └── 审计日志      → 满足等保2.0留痕要求

  这套方案覆盖了政务系统等保2.0三级要求的所有安全控制点。核心优先级:签名验证 > JWT认证 >
  字段加密,这三个是最重要的,其余是加分项。

更多推荐