Skip to content

Commit

Permalink
微信支付签名验证逻辑优化 #2656
Browse files Browse the repository at this point in the history
  • Loading branch information
overtrue committed Feb 17, 2023
1 parent a5dce32 commit a0eead0
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 57 deletions.
47 changes: 47 additions & 0 deletions docs/src/6.x/pay/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,53 @@ $account->getPlatformCerts();

### 一些可能会用到的

#### 签名验证

按官方说法,建议在拿到**微信接口响应****接收到微信支付的回调通知**时,对通知的签名进行验证,以确保通知是微信支付发送的。

你可以通过以下方式获取签名验证器:

```php
$app->getValidator();
```

##### 推送消息的签名验证

```php
$server = $app->getServer();

$server->handlePaid(function (Message $message, \Closure $next) use($app) {
// $message->out_trade_no 获取商户订单号
// $message->payer['openid'] 获取支付者 openid

try{
$app->getValidator()->validate($app->getRequest());
// 验证通过,业务处理
} catch(Exception $e){
// 验证失败
}

return $next($message);
});

// 默认返回 ['code' => 'SUCCESS', 'message' => '成功']
return $server->serve();
```

##### API返回值的签名验证

```php
// API 请求示例
$response = $app->getClient()->postJson("v3/pay/transactions/jsapi", [...]);

try{
$app->getValidator()->validate($response->toPsrResponse());
// 验证通过
} catch(Exception $e){
// 验证失败
}
```

#### 获取证书序列号

```bash
Expand Down
23 changes: 23 additions & 0 deletions src/Pay/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use EasyWeChat\Kernel\Traits\InteractWithConfig;
use EasyWeChat\Kernel\Traits\InteractWithHttpClient;
use EasyWeChat\Kernel\Traits\InteractWithServerRequest;
use EasyWeChat\Pay\Contracts\Validator as ValidatorInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class Application implements \EasyWeChat\Pay\Contracts\Application
Expand All @@ -21,6 +22,8 @@ class Application implements \EasyWeChat\Pay\Contracts\Application

protected ?ServerInterface $server = null;

protected ?ValidatorInterface $validator = null;

protected ?HttpClientInterface $client = null;

protected ?Merchant $merchant = null;
Expand Down Expand Up @@ -54,6 +57,26 @@ public function getMerchant(): Merchant
return $this->merchant;
}

/**
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
*/
public function getValidator(): ValidatorInterface
{
if (! $this->validator) {
$this->validator = new Validator($this->getMerchant());
}

return $this->validator;
}

public function setValidator(ValidatorInterface $validator): static
{
$this->validator = $validator;

return $this;
}

/**
* @throws \ReflectionException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
Expand Down
7 changes: 2 additions & 5 deletions src/Pay/Contracts/ResponseValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@

namespace EasyWeChat\Pay\Contracts;

use EasyWeChat\Kernel\Exceptions\BadResponseException;
use EasyWeChat\Kernel\HttpClient\Response;
use Psr\Http\Message\ResponseInterface;

interface ResponseValidator
{
/**
* @throws BadResponseException if the response is not successful.
*/
public function validate(ResponseInterface $response): void;
public function validate(ResponseInterface|Response $response): void;
}
15 changes: 15 additions & 0 deletions src/Pay/Contracts/Validator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace EasyWeChat\Pay\Contracts;

use Psr\Http\Message\MessageInterface;

interface Validator
{
/**
* @throws \EasyWeChat\Pay\Exceptions\InvalidSignatureException if signature validate failed.
*/
public function validate(MessageInterface $message): void;
}
9 changes: 9 additions & 0 deletions src/Pay/Exceptions/InvalidSignatureException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace EasyWeChat\Pay\Exceptions;

use EasyWeChat\Kernel\Exceptions\RuntimeException;

class InvalidSignatureException extends RuntimeException
{
}
62 changes: 10 additions & 52 deletions src/Pay/ResponseValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,75 +4,33 @@

namespace EasyWeChat\Pay;

use function base64_decode;
use EasyWeChat\Kernel\Exceptions\BadResponseException;
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
use EasyWeChat\Kernel\HttpClient\Response as HttpClientResponse;
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
use const OPENSSL_ALGO_SHA256;
use Psr\Http\Message\ResponseInterface;
use function strval;
use Psr\Http\Message\ResponseInterface as PsrResponse;

class ResponseValidator implements \EasyWeChat\Pay\Contracts\ResponseValidator
{
public const MAX_ALLOWED_CLOCK_OFFSET = 300;

public const HEADER_TIMESTAMP = 'Wechatpay-Timestamp';

public const HEADER_NONCE = 'Wechatpay-Nonce';

public const HEADER_SERIAL = 'Wechatpay-Serial';

public const HEADER_SIGNATURE = 'Wechatpay-Signature';

public function __construct(protected MerchantInterface $merchant)
{
}

/**
* @throws \EasyWeChat\Kernel\Exceptions\BadResponseException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\Pay\Exceptions\InvalidSignatureException
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
*/
public function validate(ResponseInterface $response): void
public function validate(PsrResponse|HttpClientResponse $response): void
{
if ($response->getStatusCode() !== 200) {
throw new BadResponseException('Request Failed');
}

foreach ([self::HEADER_SIGNATURE, self::HEADER_TIMESTAMP, self::HEADER_SERIAL, self::HEADER_NONCE] as $header) {
if (! $response->hasHeader($header)) {
throw new BadResponseException("Missing Header: {$header}");
}
if ($response instanceof HttpClientResponse) {
$response = $response->toPsrResponse();
}

[$timestamp] = $response->getHeader(self::HEADER_TIMESTAMP);
[$nonce] = $response->getHeader(self::HEADER_NONCE);
[$serial] = $response->getHeader(self::HEADER_SERIAL);
[$signature] = $response->getHeader(self::HEADER_SIGNATURE);

$body = (string) $response->getBody();

$message = "{$timestamp}\n{$nonce}\n{$body}\n";

if (\time() - \intval($timestamp) > self::MAX_ALLOWED_CLOCK_OFFSET) {
throw new BadResponseException('Clock Offset Exceeded');
}

$publicKey = $this->merchant->getPlatformCert($serial);

if (! $publicKey) {
throw new InvalidConfigException(
"No platform certs found for serial: {$serial},
please download from wechat pay and set it in merchant config with key `certs`."
);
if ($response->getStatusCode() !== 200) {
throw new BadResponseException('Request Failed');
}

if (false === \openssl_verify(
$message,
base64_decode($signature),
strval($publicKey),
OPENSSL_ALGO_SHA256
)) {
throw new BadResponseException('Invalid Signature');
}
(new Validator($this->merchant))->validate($response);
}
}
71 changes: 71 additions & 0 deletions src/Pay/Validator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace EasyWeChat\Pay;

use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
use EasyWeChat\Pay\Exceptions\InvalidSignatureException;
use Psr\Http\Message\MessageInterface;

class Validator implements \EasyWeChat\Pay\Contracts\Validator
{
public const MAX_ALLOWED_CLOCK_OFFSET = 300;

public const HEADER_TIMESTAMP = 'Wechatpay-Timestamp';

public const HEADER_NONCE = 'Wechatpay-Nonce';

public const HEADER_SERIAL = 'Wechatpay-Serial';

public const HEADER_SIGNATURE = 'Wechatpay-Signature';

public function __construct(protected MerchantInterface $merchant)
{
}

/**
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\Pay\Exceptions\InvalidSignatureException
*/
public function validate(MessageInterface $message): void
{
foreach ([self::HEADER_SIGNATURE, self::HEADER_TIMESTAMP, self::HEADER_SERIAL, self::HEADER_NONCE] as $header) {
if (! $message->hasHeader($header)) {
throw new InvalidSignatureException("Missing Header: {$header}");
}
}

[$timestamp] = $message->getHeader(self::HEADER_TIMESTAMP);
[$nonce] = $message->getHeader(self::HEADER_NONCE);
[$serial] = $message->getHeader(self::HEADER_SERIAL);
[$signature] = $message->getHeader(self::HEADER_SIGNATURE);

$body = (string) $message->getBody();

$message = "{$timestamp}\n{$nonce}\n{$body}\n";

if (\time() - \intval($timestamp) > self::MAX_ALLOWED_CLOCK_OFFSET) {
throw new InvalidSignatureException('Clock Offset Exceeded');
}

$publicKey = $this->merchant->getPlatformCert($serial);

if (! $publicKey) {
throw new InvalidConfigException(
"No platform certs found for serial: {$serial},
please download from wechat pay and set it in merchant config with key `certs`."
);
}

if (false === \openssl_verify(
$message,
base64_decode($signature),
strval($publicKey),
OPENSSL_ALGO_SHA256
)) {
throw new InvalidSignatureException('Invalid Signature');
}
}
}
23 changes: 23 additions & 0 deletions tests/Pay/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use EasyWeChat\Pay\Application;
use EasyWeChat\Pay\Client;
use EasyWeChat\Pay\Contracts\Merchant;
use EasyWeChat\Pay\Contracts\Validator;
use EasyWeChat\Pay\Server;
use EasyWeChat\Tests\TestCase;

Expand Down Expand Up @@ -59,4 +60,26 @@ public function test_get_server()
$this->assertInstanceOf(Server::class, $app->getServer());
$this->assertSame($app->getServer(), $app->getServer());
}

public function test_get_and_set_validator()
{
$app = new Application(
[
'mch_id' => 101111111,
'secret_key' => 'mock-secret-key',
'private_key' => 'mock-private-key',
'certificate' => '/path/to/certificate.cert',
'certificate_serial_no' => 'MOCK-CERTIFICATE-SERIAL-NO',
]
);

$this->assertInstanceOf(Validator::class, $app->getValidator());
$this->assertSame($app->getValidator(), $app->getValidator());

$validator = \Mockery::mock(Validator::class);

$app->setValidator($validator);

$this->assertSame($validator, $app->getValidator());
}
}

1 comment on commit a0eead0

@vercel
Copy link

@vercel vercel bot commented on a0eead0 Feb 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

easywechat – ./

easywechat-overtrue.vercel.app
easywechat.vercel.app
easywechat-git-6x-overtrue.vercel.app

Please sign in to comment.