[self::PKEY_PEM_FORMAT, 'RSA PRIVATE', 16], 'private.pkcs8' => [self::PKEY_PEM_FORMAT, 'PRIVATE', 16], 'public.pkcs1' => [self::PKEY_PEM_FORMAT, 'RSA PUBLIC', 15], 'public.spki' => [self::PKEY_PEM_FORMAT, 'PUBLIC', 14], ]; const ASN1_OID_RSAENCRYPTION = '300d06092a864886f70d0101010500'; const ASN1_SEQUENCE = 48; const CHR_NUL = "\0"; const CHR_ETX = "\3"; class WxV3PaymentNotify extends SmallProgram { use WxV3PaymentTait; /** * @param string $id * @param string $create_time * @param string $resource_type * @param string $event_type * @param string $summary * @param array $resource * @throws Exception */ public function __construct( public string $id = "EV-2018022511223320873", public string $create_time = "2015-05-20T13:29:35+08:00", public string $resource_type = "encrypt-resource", public string $event_type = "TRANSACTION.SUCCESS", public string $summary = "支付成功", public array $resource = [] ) { } /** * @var NotifyModel */ public NotifyModel $notifyModel; /** * @param RequestInterface $request * @return bool * @throws Exception */ public function verify(RequestInterface $request): bool { $platformPublicKeyInstance = $this->rsaFrom('file:///path/to/wechatpay/inWechatpaySerial.pem', KEY_TYPE_PUBLIC); $inWechatpaySignature = $request->getHeaderLine('Wechatpay-Signature');// 请根据实际情况获取 $inWechatpayTimestamp = $request->getHeaderLine('Wechatpay-Timestamp');// 请根据实际情况获取 $inWechatpayNonce = $request->getHeaderLine('Wechatpay-Nonce');// 请根据实际情况获取 $inBody = $request->getBody()->getContents();// 请根据实际情况获取,例如: file_get_contents('php://input'); $timeOffsetStatus = 300 >= abs(time() - (int)$inWechatpayTimestamp); $verifiedStatus = $this->notifyVerify( $this->lineFeed([$inWechatpayTimestamp, $inWechatpayNonce, $inBody]), $inWechatpaySignature, $platformPublicKeyInstance); if (!$timeOffsetStatus || !$verifiedStatus) { return false; } $this->decode(); return true; } /** * @param ...$pieces * @return string */ protected function lineFeed(...$pieces): string { return implode("\n", array_merge($pieces, [''])); } /** * @return string|bool */ protected function body(): string|bool { return json_encode(['id' => $this->id, 'create_time' => $this->create_time, 'resource_type' => $this->resource_type, 'event_type' => $this->event_type, 'summary' => $this->summary, 'resource' => $this->resource]); } /** * @param string $message * @param string $signature * @param $publicKey * @return bool */ protected function notifyVerify(string $message, string $signature, $publicKey): bool { if (($result = openssl_verify($message, base64_decode($signature), $publicKey, OPENSSL_ALGO_SHA256)) === false) { throw new \UnexpectedValueException('Verified the input $message failed, please checking your $publicKey whether or nor correct.'); } return $result === 1; } /** * @param $thing * @param string $type * @return \OpenSSLAsymmetricKey */ protected function rsaFrom($thing, string $type = KEY_TYPE_PRIVATE): \OpenSSLAsymmetricKey { $pkey = ($isPublic = $type === KEY_TYPE_PUBLIC) ? openssl_pkey_get_public($this->parse($thing, $type)) : openssl_pkey_get_private($this->parse($thing)); if (false === $pkey) { throw new \UnexpectedValueException(sprintf( 'Cannot load %s from(%s), please take care about the \$thing input.', $isPublic ? 'publicKey' : 'privateKey', gettype($thing) )); } return $pkey; } /** * @param $thing * @param string $type * @return mixed|string */ protected function parse($thing, string $type = KEY_TYPE_PRIVATE): mixed { $src = $thing; if (is_string($src) && is_int(strpos($src, PKEY_PEM_NEEDLE)) && $type === KEY_TYPE_PUBLIC && preg_match(PKEY_PEM_FORMAT_PATTERN, $src, $matches)) { [, $kind, $base64] = $matches; $mapRules = (array)array_combine(array_column(RULES, 1/*column*/), array_keys(RULES)); $protocol = $mapRules[$kind] ?? ''; if ('public.pkcs1' === $protocol) { $src = sprintf('%s://%s', $protocol, str_replace([CHR_CR, CHR_LF], '', $base64)); } } if (is_string($src) && is_bool(strpos($src, LOCAL_FILE_PROTOCOL)) && is_int(strpos($src, '://'))) { $protocol = parse_url($src, PHP_URL_SCHEME); [$format, $kind, $offset] = RULES[$protocol] ?? [null, null, null]; if ($format && $kind && $offset) { $src = substr($src, $offset); if ('public.pkcs1' === $protocol) { $src = $this->pkcs1ToSpki($src); [$format, $kind] = RULES['public.spki']; } return sprintf($format, $kind, wordwrap($src, 64, CHR_LF, true)); } } return $src; } /** * @param string $thing * @return string */ protected function pkcs1ToSpki(string $thing): string { $raw = CHR_NUL . base64_decode($thing); $new = pack('H*', ASN1_OID_RSAENCRYPTION) . CHR_ETX . self::encodeLength($raw) . $raw; return base64_encode(pack('Ca*a*', ASN1_SEQUENCE, self::encodeLength($new), $new)); } /** * @param string $thing * @return string */ protected function encodeLength(string $thing): string { $num = strlen($thing); if ($num <= 0x7F) { return sprintf('%c', $num); } $tmp = ltrim(pack('N', $num), CHR_NUL); return pack('Ca*', strlen($tmp) | 0x80, $tmp); } /** * @return void * @throws Exception */ public function decode(): void { $data = $this->decrypt($this->resource['ciphertext'], $this->resource['nonce'], $this->resource['associated_data']); $this->notifyModel = new NotifyModel(); $this->notifyModel->amount = $data['amount']; $this->notifyModel->payer = $data['payer']; $this->notifyModel->scene_info = $data['payer']; $this->notifyModel->appid = $data['appid']; $this->notifyModel->mchid = $data['mchid']; $this->notifyModel->out_trade_no = $data['out_trade_no']; $this->notifyModel->transaction_id = $data['transaction_id']; $this->notifyModel->trade_type = $data['trade_type']; $this->notifyModel->trade_state = $data['trade_state']; $this->notifyModel->trade_state_desc = $data['trade_state_desc']; $this->notifyModel->bank_type = $data['bank_type']; $this->notifyModel->attach = $data['attach']; $this->notifyModel->success_time = $data['success_time']; $this->notifyModel->promotion_detail = []; foreach ($data['promotion_detail'] as $datum) { $detail = new PromotionDetail(); $detail->amount = $datum['amount']; $detail->wechatpay_contribute = $datum['wechatpay_contribute']; $detail->coupon_id = $datum['coupon_id']; $detail->scope = $datum['scope']; $detail->merchant_contribute = $datum['merchant_contribute']; $detail->name = $datum['name']; $detail->other_contribute = $datum['other_contribute']; $detail->currency = $datum['currency']; $detail->stock_id = $datum['stock_id']; $detail->goods_detail = []; foreach ($datum['goods_detail'] as $value) { $detail->goods_detail[] = new GoodsDetail($value); } $this->notifyModel->promotion_detail[] = $detail; } } }