Skip to content

PHP示例代码

1. 核心安全组件 (SudSecurityUtil.php)

php
<?php

namespace Sud\Security;

use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\RSA;
use Exception;
use RuntimeException;

class SudSecurityUtil
{
    /**
     * 1. 请求业务数据加密 (AES-256-GCM)
     *
     * @param string $urlPath      包含域名的全路径
     * @param string $appId        平台应用 AppId
     * @param string $keySn        对称密钥编号
     * @param string $symKeyBase64 对称密钥 Base64 字符串
     * @param array  $reqData      原始业务请求数据数组
     * @return array 包含请求时间戳 (req_ts) 和 JSON 密文 (req_data) 的关联数组
     */
    public static function encryptRequest(string $urlPath, string $appId, string $keySn, string $symKeyBase64, array $reqData): array
    {
        $timestamp = time();
        // 生成 16 字节随机数,并转为 URL 安全的 Base64(无等号填充)
        $nonceBytes = random_bytes(16);
        $nonce = rtrim(strtr(base64_encode($nonceBytes), '+/', '-_'), '=');

        // 补齐安全校验字段
        $reqData['_n'] = $nonce;
        $reqData['_appid'] = $appId;
        $reqData['_timestamp'] = $timestamp;

        // 使用 JSON_UNESCAPED_UNICODE 和 JSON_UNESCAPED_SLASHES 保证格式紧凑且不转义中文/斜杠
        $plaintext = json_encode($reqData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        // 拼接 AAD: urlpath|appid|timestamp|sn
        $aad = "{$urlPath}|{$appId}|{$timestamp}|{$keySn}";
        
        $realKey = base64_decode($symKeyBase64);
        $iv = random_bytes(12); // GCM 推荐 12 字节 IV
        $authTag = ''; // 将由 openssl_encrypt 引用赋值

        // 执行 AES-256-GCM 加密
        $ciphertext = openssl_encrypt(
            $plaintext,
            'aes-256-gcm',
            $realKey,
            OPENSSL_RAW_DATA,
            $iv,
            $authTag,
            $aad,
            16
        );

        if ($ciphertext === false) {
            throw new RuntimeException("AES-GCM 加密失败");
        }

        $httpBody = [
            'iv' => base64_encode($iv),
            'data' => base64_encode($ciphertext),
            'authtag' => base64_encode($authTag)
        ];

        return [
            'req_ts' => $timestamp,
            'req_data' => json_encode($httpBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
        ];
    }

    /**
     * 2. 请求报文签名 (RSA-PSS)
     */
    public static function signRequest(string $urlPath, string $appId, int $timestamp, string $reqDataJson, string $privateKeyPem): string
    {
        // 严格按顺序拼接
        $payload = "{$urlPath}\n{$appId}\n{$timestamp}\n{$reqDataJson}";

        // 加载私钥(phpseclib 自动兼容 PKCS#1 和 PKCS#8 格式)
        $privateKey = PublicKeyLoader::load($privateKeyPem)
            ->withHash('sha256')
            ->withMGFHash('sha256')
            ->withSaltLength(32) // PSS 盐长度需对齐 SHA256 (32字节)
            ->withPadding(RSA::SIGNATURE_PSS);

        $signature = $privateKey->sign($payload);
        return base64_encode($signature);
    }

    /**
     * 3. 响应报文验签 (RSA-PSS)
     */
    public static function verifyResponseSignature(
        string $urlPath, string $expectedAppId, string $localCertSn, string $platformCertPem,
        string $respAppId, int $respTs, string $respSn, string $respSig,
        ?string $respDeprecatedSn, ?string $respDeprecatedSig, string $respDataJson
    ): bool {
        // 基础安全校验
        if ($expectedAppId !== $respAppId) {
            throw new Exception("安全校验失败:响应的 AppId 与本地不匹配");
        }

        // 证书序列号匹配与平滑轮换支持
        $signatureBase64 = '';
        if ($localCertSn === $respSn) {
            $signatureBase64 = $respSig;
        } elseif (!empty($respDeprecatedSn) && $localCertSn === $respDeprecatedSn) {
            $signatureBase64 = (!empty($respDeprecatedSig)) ? $respDeprecatedSig : $respSig;
        } else {
            throw new Exception("验签失败:本地证书编号({$localCertSn})与平台当前编号({$respSn})不匹配");
        }

        $payload = "{$urlPath}\n{$respAppId}\n{$respTs}\n{$respDataJson}";
        $signatureBytes = base64_decode($signatureBase64);

        // 加载平台公钥证书
        $publicKey = PublicKeyLoader::load($platformCertPem)
            ->withHash('sha256')
            ->withMGFHash('sha256')
            ->withSaltLength(32)
            ->withPadding(RSA::SIGNATURE_PSS);

        return $publicKey->verify($payload, $signatureBytes);
    }

    /**
     * 4. 响应密文解密 (AES-256-GCM)
     */
    public static function decryptResponse(string $urlPath, string $appId, int $respTs, string $keySn, string $symKeyBase64, string $respDataJson): array
    {
        $encBody = json_decode($respDataJson, true);
        if (!$encBody || !isset($encBody['iv'], $encBody['data'], $encBody['authtag'])) {
            throw new Exception("密文 JSON 格式非法");
        }

        $iv = base64_decode($encBody['iv']);
        $ciphertext = base64_decode($encBody['data']);
        $authTag = base64_decode($encBody['authtag']);

        $aad = "{$urlPath}|{$appId}|{$respTs}|{$keySn}";
        $realKey = base64_decode($symKeyBase64);

        // 执行解密(如果 AuthTag 验证失败,或者密文被篡改,将返回 false)
        $plaintext = openssl_decrypt(
            $ciphertext,
            'aes-256-gcm',
            $realKey,
            OPENSSL_RAW_DATA,
            $iv,
            $authTag,
            $aad
        );

        if ($plaintext === false) {
            throw new Exception("解密失败:AuthTag 验证未通过或密文被篡改");
        }

        $realResp = json_decode($plaintext, true);

        // 内部安全字段强校验
        if (!isset($realResp['_appid'], $realResp['_timestamp']) ||
            $realResp['_appid'] !== $appId ||
            (int)$realResp['_timestamp'] !== $respTs) {
            throw new Exception("安全字段校验失败:响应明文内部的 _appid 或 _timestamp 被非法篡改");
        }

        // 联调时可解开此注释进行防重放校验
        // if (abs(time() - (int)$realResp['_timestamp']) > 300) {
        //     throw new Exception("安全校验失败:响应数据距离当前时间误差超过 5 分钟");
        // }

        return $realResp;
    }
}

2. 业务调用演示 (demo.php)

php
<?php

require __DIR__ . '/vendor/autoload.php';

use Sud\Security\SudSecurityUtil;

// 模拟应用全局配置
$config = [
    'appId' => 'cp_10010',
    'urlPath' => 'https://api.sud.com/v1/game/login',
    'symKeySn' => 'key_v1',
    'symKey' => 'otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY=',
    'privateKeyPem' => "-----BEGIN PRIVATE KEY-----\nMIIEvQIB... (应用私钥) ...W6w=\n-----END PRIVATE KEY-----",
    'platformCertSn' => 'sud_cert_v1',
    'platformCertPem' => "-----BEGIN CERTIFICATE-----\nMIID0j... (SUD平台公钥证书) ...Q==\n-----END CERTIFICATE-----"
];

try {
    echo "=== 1. 发起请求:业务数据加密与签名 ===\n";

    // 原始业务请求参数
    $bizReq = [
        'appid' => $config['appId'],
        'sud_uid' => 'user_889900',
        'scene' => 0,
        'client_ip' => '127.0.0.1'
    ];

    // 步骤 A:加密
    $encResult = SudSecurityUtil::encryptRequest(
        $config['urlPath'], $config['appId'], $config['symKeySn'], $config['symKey'], $bizReq
    );
    
    $reqTs = $encResult['req_ts'];
    $reqDataJson = $encResult['req_data'];

    // 步骤 B:签名
    $reqSig = SudSecurityUtil::signRequest(
        $config['urlPath'], $config['appId'], $reqTs, $reqDataJson, $config['privateKeyPem']
    );

    echo "HTTP Header X-Sud-Timestamp: {$reqTs}\n";
    echo "HTTP Header X-Sud-Signature: {$reqSig}\n";
    echo "HTTP Body: {$reqDataJson}\n";


    echo "\n=== 2. 收到响应:签名验证与密文解密 ===\n";

    // 模拟从 HTTP 响应中获取的数据
    $respTs = $reqTs;
    $respHeaderAppId = $config['appId'];
    $respHeaderSn = $config['platformCertSn'];
    $respHeaderSig = $reqSig; // 演示用,实际为接收的签名
    $respBodyJson = '{"iv":"r2WDQt56rEAmMuoR","data":"HExs...","authtag":"z2BF..."}';

    // 步骤 C:验签
    $isSigValid = SudSecurityUtil::verifyResponseSignature(
        $config['urlPath'], $config['appId'], $config['platformCertSn'], $config['platformCertPem'],
        $respHeaderAppId, $respTs, $respHeaderSn, $respHeaderSig,
        null, null, $respBodyJson
    );

    if ($isSigValid) {
        echo "响应验签成功!\n";

        // 步骤 D:解密
        $decryptedBizData = SudSecurityUtil::decryptResponse(
            $config['urlPath'], $config['appId'], $respTs, $config['symKeySn'], $config['symKey'], $respBodyJson
        );
        
        echo "响应解密成功,业务数据:\n";
        print_r($decryptedBizData);
    }

} catch (Exception $e) {
    echo "流程中断: " . $e->getMessage() . "\n";
}