fix(payment): NHN KCP 비회원 영수증 조회 지원
This commit is contained in:
@@ -4,6 +4,12 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- 비회원 NHN KCP 주문완료/주문상세에서 매출전표 조회가 인증 사용자 전용 라우트에 막히지 않도록 게스트 주문 토큰과 결제 성공 후 임시 영수증 쿠키 접근을 지원했습니다.
|
||||
|
||||
## [1.0.0-beta.5] - 2026-06-23
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Plugins\Sirsoft\PayNhnkcp\Concerns;
|
||||
|
||||
use Illuminate\Cookie\CookieJar;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
|
||||
trait IssuesReceiptCookie
|
||||
{
|
||||
public const RECEIPT_COOKIE_NAME = 'nhnkcp_receipt_token';
|
||||
private const RECEIPT_COOKIE_TTL_MINUTES = 5;
|
||||
|
||||
protected function queueReceiptCookie(string $orderNumber): void
|
||||
{
|
||||
$expiresTs = time() + (self::RECEIPT_COOKIE_TTL_MINUTES * 60);
|
||||
$signature = $this->signReceiptToken($orderNumber, $expiresTs);
|
||||
$tokenValue = $orderNumber.'|'.$expiresTs.'|'.$signature;
|
||||
|
||||
/** @var CookieJar $jar */
|
||||
$jar = app(CookieJar::class);
|
||||
|
||||
$jar->queue(new Cookie(
|
||||
name: self::RECEIPT_COOKIE_NAME,
|
||||
value: $tokenValue,
|
||||
expire: $expiresTs,
|
||||
path: '/',
|
||||
domain: null,
|
||||
secure: request()->isSecure(),
|
||||
httpOnly: true,
|
||||
raw: false,
|
||||
sameSite: Cookie::SAMESITE_LAX,
|
||||
));
|
||||
}
|
||||
|
||||
protected function verifyReceiptCookie(?string $cookieValue, string $expectedOrderNumber): bool
|
||||
{
|
||||
if (! is_string($cookieValue) || $cookieValue === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parts = explode('|', $cookieValue);
|
||||
if (count($parts) !== 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[$orderNumber, $expiresTs, $signature] = $parts;
|
||||
|
||||
if ($orderNumber !== $expectedOrderNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! ctype_digit($expiresTs) || (int) $expiresTs < time()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! ctype_xdigit($signature) || strlen($signature) !== 64) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hash_equals($this->signReceiptToken($orderNumber, (int) $expiresTs), $signature);
|
||||
}
|
||||
|
||||
private function signReceiptToken(string $orderNumber, int $expiresTs): string
|
||||
{
|
||||
return hash_hmac('sha256', $orderNumber.'|'.$expiresTs, (string) config('app.key', ''));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use Modules\Sirsoft\Ecommerce\Helpers\DeviceDetector;
|
||||
use Modules\Sirsoft\Ecommerce\Models\Order;
|
||||
use Modules\Sirsoft\Ecommerce\Services\OrderProcessingService;
|
||||
use Plugins\Sirsoft\PayNhnkcp\Concerns\PreventsReplayCallback;
|
||||
use Plugins\Sirsoft\PayNhnkcp\Concerns\IssuesReceiptCookie;
|
||||
use Plugins\Sirsoft\PayNhnkcp\Concerns\RecordsPaymentWindowClosure;
|
||||
use Plugins\Sirsoft\PayNhnkcp\Concerns\ResolvesEasyPayDisplay;
|
||||
use Plugins\Sirsoft\PayNhnkcp\Concerns\SanitizesPgResponse;
|
||||
@@ -37,6 +38,7 @@ use Plugins\Sirsoft\PayNhnkcp\Services\NhnKcpApiService;
|
||||
class PaymentCallbackController
|
||||
{
|
||||
use PreventsReplayCallback;
|
||||
use IssuesReceiptCookie;
|
||||
use RecordsPaymentWindowClosure;
|
||||
use ResolvesEasyPayDisplay;
|
||||
use SanitizesPgResponse;
|
||||
@@ -217,6 +219,8 @@ class PaymentCallbackController
|
||||
'transaction_id' => $order->payment?->transaction_id,
|
||||
]);
|
||||
|
||||
$this->queueReceiptCookie($ordrIdxx);
|
||||
|
||||
return redirect($this->resolveSuccessUrl($ordrIdxx));
|
||||
}
|
||||
|
||||
@@ -302,6 +306,7 @@ class PaymentCallbackController
|
||||
// Replay 가드: 동일 tno 가 이미 paid 상태면 중복 처리하지 않고 성공 페이지로 복귀
|
||||
if ($this->wasAlreadyPaid($tno)) {
|
||||
$this->logReplayDetected($tno, $ordrIdxx, 'authCallback (card)');
|
||||
$this->queueReceiptCookie($ordrIdxx);
|
||||
|
||||
return redirect($this->resolveSuccessUrl($ordrIdxx));
|
||||
}
|
||||
@@ -359,6 +364,8 @@ class PaymentCallbackController
|
||||
// (기본 PG가 타 PG일 때 주문이 해당 PG provider로 생성되는 경우 대비)
|
||||
$order->payment()->update(['pg_provider' => 'nhnkcp']);
|
||||
|
||||
$this->queueReceiptCookie($ordrIdxx);
|
||||
|
||||
return redirect($this->resolveSuccessUrl($ordrIdxx));
|
||||
|
||||
} catch (LockTimeoutException $e) {
|
||||
@@ -369,6 +376,8 @@ class PaymentCallbackController
|
||||
$latestOrder = $this->orderService->findByOrderNumber($ordrIdxx);
|
||||
if (($latestOrder?->payment?->isPaid() ?? false)
|
||||
|| $latestOrder?->order_status === OrderStatusEnum::PAYMENT_COMPLETE) {
|
||||
$this->queueReceiptCookie($ordrIdxx);
|
||||
|
||||
return redirect($this->resolveSuccessUrl($ordrIdxx));
|
||||
}
|
||||
|
||||
@@ -740,6 +749,8 @@ class PaymentCallbackController
|
||||
]));
|
||||
}
|
||||
|
||||
$this->queueReceiptCookie($ordrIdxx);
|
||||
|
||||
return redirect($this->resolveSuccessUrl($ordrIdxx));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,14 @@ use App\Services\PluginSettingsService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\Sirsoft\Ecommerce\Services\GuestOrderAuthService;
|
||||
use Plugins\Sirsoft\PayNhnkcp\Concerns\IssuesReceiptCookie;
|
||||
use Plugins\Sirsoft\PayNhnkcp\Concerns\ResolvesEasyPayDisplay;
|
||||
use Plugins\Sirsoft\PayNhnkcp\Services\NhnKcpApiService;
|
||||
|
||||
class UserReceiptController
|
||||
{
|
||||
use IssuesReceiptCookie;
|
||||
use ResolvesEasyPayDisplay;
|
||||
|
||||
private const PLUGIN_IDENTIFIER = 'sirsoft-pay_nhnkcp';
|
||||
@@ -30,6 +33,7 @@ class UserReceiptController
|
||||
public function __construct(
|
||||
private readonly PluginSettingsService $pluginSettingsService,
|
||||
private readonly NhnKcpApiService $apiService,
|
||||
private readonly GuestOrderAuthService $guestOrderAuthService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -41,20 +45,37 @@ class UserReceiptController
|
||||
* 결제 영수증 URL(신용카드)과 현금영수증 URL(현금/계좌이체)을 반환한다.
|
||||
* receipt_url 이 비어있으면 transaction_id (tno) 로 KCP 영수증 URL 을 동적 생성.
|
||||
*
|
||||
* @param Request $request 인증된 사용자 요청
|
||||
* @param Request $request 회원 또는 비회원 영수증 요청
|
||||
* @param string $orderNumber 주문번호
|
||||
* @return JsonResponse receipt_url / cash_receipt_url 또는 404
|
||||
*/
|
||||
public function show(Request $request, string $orderNumber): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$payment = DB::table('ecommerce_order_payments as p')
|
||||
$query = DB::table('ecommerce_order_payments as p')
|
||||
->join('ecommerce_orders as o', 'o.id', '=', 'p.order_id')
|
||||
->where('o.order_number', $orderNumber)
|
||||
->where('o.user_id', $user->id)
|
||||
->where('p.pg_provider', 'nhnkcp')
|
||||
->whereNotNull('p.transaction_id')
|
||||
->whereNotNull('p.transaction_id');
|
||||
|
||||
if ($user) {
|
||||
$query->where('o.user_id', $user->id);
|
||||
} else {
|
||||
$order = $this->guestOrderAuthService->verifyToken($request->header('X-Guest-Order-Token'), $orderNumber);
|
||||
|
||||
if ($order) {
|
||||
$query->whereNull('o.user_id')->where('o.id', $order->id);
|
||||
} elseif ($this->verifyReceiptCookie($request->cookie(self::RECEIPT_COOKIE_NAME), $orderNumber)) {
|
||||
$query->whereNull('o.user_id')
|
||||
->where('o.id', function ($sub) use ($orderNumber) {
|
||||
$sub->select('id')->from('ecommerce_orders')->where('order_number', $orderNumber);
|
||||
});
|
||||
} else {
|
||||
return response()->json(['error' => 'Not found'], 404);
|
||||
}
|
||||
}
|
||||
|
||||
$payment = $query
|
||||
->select([
|
||||
'p.transaction_id',
|
||||
'p.payment_meta',
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Plugins\Sirsoft\PayNhnkcp\Providers;
|
||||
|
||||
use App\Extension\BasePluginServiceProvider;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
|
||||
class PayNhnkcpServiceProvider extends BasePluginServiceProvider
|
||||
{
|
||||
protected string $pluginIdentifier = 'sirsoft-pay_nhnkcp';
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// PG callback 이 발급하는 영수증 쿠키는 HMAC 으로 자체 무결성 보장하므로
|
||||
// EncryptCookies 대상에서 제외한다. 암호화 대상이면 callback 응답에서
|
||||
// 직접 발급한 평문 쿠키가 다음 API 요청에서 복호화 실패로 폐기될 수 있다.
|
||||
// 값 동기화: IssuesReceiptCookie::RECEIPT_COOKIE_NAME 과 동일해야 함.
|
||||
EncryptCookies::except(['nhnkcp_receipt_token']);
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,11 @@ Route::post('/payment/close-report', [PaymentCloseReportController::class, 'stor
|
||||
Route::post('/payment/retry', [PaymentRetryController::class, 'store'])
|
||||
->name('payment.retry');
|
||||
|
||||
Route::middleware(['auth:sanctum'])->group(function () {
|
||||
Route::get('/user/orders/{orderNumber}/receipt', [UserReceiptController::class, 'show'])
|
||||
->name('user.orders.receipt');
|
||||
Route::get('/user/orders/{orderNumber}/receipt', [UserReceiptController::class, 'show'])
|
||||
->middleware('optional.sanctum')
|
||||
->name('user.orders.receipt');
|
||||
|
||||
Route::middleware(['auth:sanctum'])->group(function () {
|
||||
// 테스트 모드 가상계좌 모의입금 정보 (입금대기 상태일 때만 데이터 반환)
|
||||
Route::get('/user/orders/{orderNumber}/vbank-mock-deposit-info', [UserVbankMockDepositController::class, 'show'])
|
||||
->name('user.orders.vbank-mock-deposit-info');
|
||||
|
||||
+128
@@ -5,15 +5,26 @@ declare(strict_types=1);
|
||||
namespace Plugins\Sirsoft\PayNhnkcp\Tests\Feature\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Modules\Sirsoft\Ecommerce\Database\Factories\OrderFactory;
|
||||
use Modules\Sirsoft\Ecommerce\Database\Factories\OrderPaymentFactory;
|
||||
use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum;
|
||||
use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum;
|
||||
use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum;
|
||||
use Modules\Sirsoft\Ecommerce\Services\GuestOrderAuthService;
|
||||
use Plugins\Sirsoft\PayNhnkcp\Tests\PluginTestCase;
|
||||
|
||||
class UserReceiptControllerTest extends PluginTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// 운영에서는 PayNhnkcpServiceProvider::boot 가 동일하게 등록한다.
|
||||
EncryptCookies::except(['nhnkcp_receipt_token']);
|
||||
}
|
||||
|
||||
public function test_receipt_response_includes_easy_pay_display_label(): void
|
||||
{
|
||||
app()->setLocale('ko');
|
||||
@@ -62,4 +73,121 @@ class UserReceiptControllerTest extends PluginTestCase
|
||||
$data['payment_method_display_label'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
public function test_guest_receipt_requires_valid_token(): void
|
||||
{
|
||||
$order = $this->createGuestOrder();
|
||||
$this->createKcpPayment($order, 'KCP_TNO_GUEST_001');
|
||||
|
||||
$this->getJson("/api/plugins/sirsoft-pay_nhnkcp/user/orders/{$order->order_number}/receipt")
|
||||
->assertNotFound();
|
||||
|
||||
$this->withHeader('X-Guest-Order-Token', (time() + 1800).'|'.str_repeat('0', 64))
|
||||
->getJson("/api/plugins/sirsoft-pay_nhnkcp/user/orders/{$order->order_number}/receipt")
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_guest_receipt_returns_receipt_url_with_valid_header_token(): void
|
||||
{
|
||||
app()->setLocale('ko');
|
||||
|
||||
$order = $this->createGuestOrder();
|
||||
$this->createKcpPayment($order, 'KCP_TNO_GUEST_002');
|
||||
|
||||
$response = $this->withHeader('X-Guest-Order-Token', $this->issueGuestToken($order))
|
||||
->getJson("/api/plugins/sirsoft-pay_nhnkcp/user/orders/{$order->order_number}/receipt")
|
||||
->assertOk()
|
||||
->assertJsonPath('receipt_url', 'https://testadmin8.kcp.co.kr/assist/bill.BillActionNew.do?cmd=card_bill&tno=KCP_TNO_GUEST_002&order_no='.$order->order_number.'&trade_mony=1000');
|
||||
|
||||
$this->assertContains($response->json('payment_method_label'), ['신용카드', 'Credit Card']);
|
||||
}
|
||||
|
||||
public function test_guest_receipt_returns_receipt_url_with_valid_cookie(): void
|
||||
{
|
||||
$order = $this->createGuestOrder();
|
||||
$this->createKcpPayment($order, 'KCP_TNO_GUEST_003');
|
||||
|
||||
$this->withUnencryptedCookie('nhnkcp_receipt_token', $this->issueReceiptCookie($order->order_number))
|
||||
->withCredentials()
|
||||
->getJson("/api/plugins/sirsoft-pay_nhnkcp/user/orders/{$order->order_number}/receipt")
|
||||
->assertOk()
|
||||
->assertJsonPath('receipt_url', 'https://testadmin8.kcp.co.kr/assist/bill.BillActionNew.do?cmd=card_bill&tno=KCP_TNO_GUEST_003&order_no='.$order->order_number.'&trade_mony=1000');
|
||||
}
|
||||
|
||||
public function test_guest_token_for_other_order_cannot_access_receipt(): void
|
||||
{
|
||||
$order1 = $this->createGuestOrder();
|
||||
$order2 = $this->createGuestOrder();
|
||||
$this->createKcpPayment($order1, 'KCP_TNO_GUEST_004');
|
||||
$this->createKcpPayment($order2, 'KCP_TNO_GUEST_005');
|
||||
|
||||
$this->withHeader('X-Guest-Order-Token', $this->issueGuestToken($order1))
|
||||
->getJson("/api/plugins/sirsoft-pay_nhnkcp/user/orders/{$order2->order_number}/receipt")
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_authenticated_user_cannot_access_guest_order_receipt_via_token(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$order = $this->createGuestOrder();
|
||||
$this->createKcpPayment($order, 'KCP_TNO_GUEST_006');
|
||||
|
||||
$this->actingAs($user)
|
||||
->withHeader('X-Guest-Order-Token', $this->issueGuestToken($order))
|
||||
->getJson("/api/plugins/sirsoft-pay_nhnkcp/user/orders/{$order->order_number}/receipt")
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
private function createGuestOrder()
|
||||
{
|
||||
return OrderFactory::new()->create([
|
||||
'user_id' => null,
|
||||
'guest_lookup_password_hash' => Hash::make('test1234'),
|
||||
'order_number' => 'ORD-KCP-GUEST-' . random_int(10000, 99999),
|
||||
'order_status' => OrderStatusEnum::PAYMENT_COMPLETE,
|
||||
'total_amount' => 1000,
|
||||
'total_due_amount' => 0,
|
||||
'total_paid_amount' => 1000,
|
||||
'paid_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createKcpPayment($order, string $transactionId): void
|
||||
{
|
||||
OrderPaymentFactory::new()->create([
|
||||
'order_id' => $order->id,
|
||||
'payment_status' => PaymentStatusEnum::PAID,
|
||||
'payment_method' => PaymentMethodEnum::CARD,
|
||||
'pg_provider' => 'nhnkcp',
|
||||
'transaction_id' => $transactionId,
|
||||
'paid_amount_local' => 1000,
|
||||
'paid_at' => now(),
|
||||
'payment_meta' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
private function issueGuestToken($order): string
|
||||
{
|
||||
$svc = app(GuestOrderAuthService::class);
|
||||
$rc = new \ReflectionClass($svc);
|
||||
$signMethod = $rc->getMethod('sign');
|
||||
$signMethod->setAccessible(true);
|
||||
$suffixMethod = $rc->getMethod('passwordHashSuffix');
|
||||
$suffixMethod->setAccessible(true);
|
||||
|
||||
$expiresTs = time() + 1800;
|
||||
$suffix = $suffixMethod->invoke($svc, $order);
|
||||
$sig = $signMethod->invoke($svc, $order->order_number, (int) $order->id, $expiresTs, $suffix);
|
||||
|
||||
return $expiresTs.'|'.$sig;
|
||||
}
|
||||
|
||||
private function issueReceiptCookie(string $orderNumber): string
|
||||
{
|
||||
$expiresTs = time() + 300;
|
||||
$payload = $orderNumber.'|'.$expiresTs;
|
||||
$signature = hash_hmac('sha256', $payload, (string) config('app.key', ''));
|
||||
|
||||
return $payload.'|'.$signature;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Plugins\Sirsoft\PayNhnkcp\Tests\Unit\Concerns;
|
||||
|
||||
use Plugins\Sirsoft\PayNhnkcp\Concerns\IssuesReceiptCookie;
|
||||
use Plugins\Sirsoft\PayNhnkcp\Tests\PluginTestCase;
|
||||
|
||||
/**
|
||||
* 영수증 cookie HMAC 검증 로직 단위 테스트.
|
||||
*
|
||||
* HTTP 통합(callback → Set-Cookie → 후속 호출에서 cookie 자동 첨부) 검증은
|
||||
* Playwright 시나리오로 다룬다 — Laravel 의 EncryptCookies 미들웨어와 PHPUnit
|
||||
* HTTP 테스트 환경의 cookie 처리 흐름이 production 과 일부 다르기 때문.
|
||||
*/
|
||||
class IssuesReceiptCookieTest extends PluginTestCase
|
||||
{
|
||||
private object $subject;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// IssuesReceiptCookie 는 trait — 익명 클래스로 인스턴스화
|
||||
$this->subject = new class {
|
||||
use IssuesReceiptCookie {
|
||||
verifyReceiptCookie as public;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public function test_verify_succeeds_with_fresh_token_matching_order_number(): void
|
||||
{
|
||||
$orderNumber = 'ORD-RECEIPT-12345';
|
||||
$cookieValue = $this->buildToken($orderNumber, time() + 300);
|
||||
|
||||
$this->assertTrue($this->subject->verifyReceiptCookie($cookieValue, $orderNumber));
|
||||
}
|
||||
|
||||
public function test_verify_fails_when_cookie_missing(): void
|
||||
{
|
||||
$this->assertFalse($this->subject->verifyReceiptCookie(null, 'ORD-RECEIPT-99999'));
|
||||
$this->assertFalse($this->subject->verifyReceiptCookie('', 'ORD-RECEIPT-99999'));
|
||||
}
|
||||
|
||||
public function test_verify_fails_when_token_format_invalid(): void
|
||||
{
|
||||
// 3-파트 미충족
|
||||
$this->assertFalse($this->subject->verifyReceiptCookie('ORD-X|123', 'ORD-X'));
|
||||
// 만료시각이 숫자 아님
|
||||
$this->assertFalse($this->subject->verifyReceiptCookie('ORD-X|abc|'.str_repeat('0', 64), 'ORD-X'));
|
||||
// 서명이 hex 아님
|
||||
$this->assertFalse($this->subject->verifyReceiptCookie('ORD-X|'.(time() + 300).'|invalid_sig', 'ORD-X'));
|
||||
}
|
||||
|
||||
public function test_verify_fails_when_token_expired(): void
|
||||
{
|
||||
$orderNumber = 'ORD-RECEIPT-EXPIRED';
|
||||
$expiredCookie = $this->buildToken($orderNumber, time() - 60);
|
||||
|
||||
$this->assertFalse($this->subject->verifyReceiptCookie($expiredCookie, $orderNumber));
|
||||
}
|
||||
|
||||
public function test_verify_fails_when_order_number_mismatched(): void
|
||||
{
|
||||
$tokenFor = 'ORD-RECEIPT-AAA';
|
||||
$accessing = 'ORD-RECEIPT-BBB';
|
||||
$cookieValue = $this->buildToken($tokenFor, time() + 300);
|
||||
|
||||
// cookie 의 payload 가 다른 주문번호이므로 reject
|
||||
$this->assertFalse($this->subject->verifyReceiptCookie($cookieValue, $accessing));
|
||||
}
|
||||
|
||||
public function test_verify_fails_when_signature_tampered(): void
|
||||
{
|
||||
$orderNumber = 'ORD-RECEIPT-TAMPER';
|
||||
$expiresTs = time() + 300;
|
||||
// 정상 서명 대신 0으로 가득 찬 위조 서명
|
||||
$tampered = $orderNumber.'|'.$expiresTs.'|'.str_repeat('0', 64);
|
||||
|
||||
$this->assertFalse($this->subject->verifyReceiptCookie($tampered, $orderNumber));
|
||||
}
|
||||
|
||||
private function buildToken(string $orderNumber, int $expiresTs): string
|
||||
{
|
||||
$payload = $orderNumber.'|'.$expiresTs;
|
||||
$signature = hash_hmac('sha256', $payload, (string) config('app.key', ''));
|
||||
|
||||
return $orderNumber.'|'.$expiresTs.'|'.$signature;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user