Next.js集成Easypaisa支付网关返回‘Request Rejected’无法支付
问题描述
我正在将Easypaisa支付网关集成到Next.js(App Router)应用中。前端支付按钮调用/api/easypaisa/initiate接口生成带哈希值的Easypay URL,但提交请求后并未跳转到Easypay支付页面,而是收到Easypaisa返回的"Request Rejected"页面。服务器无任何日志,说明请求从未返回至我的应用。尝试多种解决方案后仍提示「无效请求」或「Parameter Authentication failed(参数认证失败)」。
已尝试的操作
- 验证
storeId和hashKey为Easypaisa沙箱凭证且正确无误; - 确认
postBackURL为HTTPS且可公开访问; - 尝试使用Easypaisa的staging和production两种URL;
- 尝试开启和关闭
autoRedirect; - 检查哈希生成逻辑与官方文档一致。
核心问题
Easypaisa返回"Parameter Authentication failed"的原因是什么?在Next.js中生成支付请求时,哈希值或参数应如何正确构造?
相关代码
EasyPayButton.tsx
"use client"; import { useState } from "react"; export default function EasyPayButton() { const [loading, setLoading] = useState(false); async function handlePay() { setLoading(true); try { const res = await fetch("/api/easypaisa/initiate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amount: "100", orderRefNum: Date.now().toString(), }), }); const data = await res.json(); if (!res.ok) throw new Error(data.message || "Failed"); const form = document.createElement("form"); form.method = "POST"; form.action = data.easypayURL; form.target = "_self"; for (const key in data.params) { const input = document.createElement("input"); input.type = "hidden"; input.name = key; input.value = data.params[key]; form.appendChild(input); } document.body.appendChild(form); form.submit(); } catch (error) { console.error("Easypay Error:", error); alert("Payment initiation failed"); } finally { setLoading(false); } } return ( <button onClick={handlePay} disabled={loading}> {loading ? "Redirecting..." : "Pay with Easypaisa"} </button> ); }
/api/easypaisa/initiate.ts
import type { NextApiRequest, NextApiResponse } from "next"; import crypto from "crypto"; const storeId = process.env.NEXT_PUBLIC_EASYPAY_STORE_ID!; const hashKey = process.env.NEXT_PUBLIC_EASYPAY_HASH_KEY!; const postBackURL = "https://1shot.pk/api/easypaisa/response"; const autoRedirect = "1"; function formatExpiryDate(): string { const now = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes ahead const yyyyMMdd = now.toISOString().slice(0, 10).replace(/-/g, ''); // "20251028" const HHmmss = now.toTimeString().slice(0, 8).replace(/:/g, ''); // "072400" return `${yyyyMMdd} ${HHmmss}`; // "20251028 072400" } export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== "POST") { return res.status(405).json({ message: "Method not allowed" }); } try { const { amount, orderRefNum } = req.body as { amount: string; orderRefNum: string; }; if (!storeId || !hashKey) { return res.status(500).json({ message: "Easypay store ID or hash key not configured" }); } const expiryDate = formatExpiryDate(); const payload: Record<string, string> = { amount: Number(amount).toFixed(2), storeId, postBackURL, orderRefNum, expiryDate, autoRedirect, emailAddr: "test@example.com", mobileNum: "03001234567", }; const sortedKeys = Object.keys(payload).sort(); const rawString = sortedKeys.map(key => `${key}=${payload[key]}`).join("&") + `&${hashKey}`; const merchantHashedReq = crypto.createHash("sha256").update(rawString).digest("hex"); const paramsObj = { ...payload, merchantHashedReq, }; return res.status(200).json({ params: paramsObj }); } catch (error) { console.error("Easypay Initiate Error:", error); return res.status(500).json({ message: "Internal Server Error" }); } }
解决方案
参数认证失败的核心原因及修复步骤
1. 哈希生成逻辑的关键错误
你的代码中哈希拼接存在问题:Easypaisa要求将hashKey直接追加在拼接后的参数字符串末尾,不需要加&。当前代码里的rawString多了一个&符号,这会导致哈希值完全错误,直接触发参数认证失败。
修复代码:
将initiate.ts中的这一行:
const rawString = sortedKeys.map(key => `${key}=${payload[key]}`).join("&") + `&${hashKey}`;
改为:
const rawString = sortedKeys.map(key => `${key}=${payload[key]}`).join("&") + hashKey;
2. 环境变量的安全性与正确性
你把storeId和hashKey放在了NEXT_PUBLIC_前缀的环境变量中,这会导致这些敏感信息暴露在前端代码里,不仅有安全风险,还可能因为前端环境变量的处理方式导致值被意外篡改。
修复方式:
- 去掉环境变量的
NEXT_PUBLIC_前缀,改为EASYPAY_STORE_ID和EASYPAY_HASH_KEY; - 在
initiate.ts中直接读取这些非公开环境变量:const storeId = process.env.EASYPAY_STORE_ID!; const hashKey = process.env.EASYPAY_HASH_KEY!;
3. 表单提交的目标URL缺失
前端代码中form.action = data.easypayURL,但你的后端接口并未返回easypayURL字段,这会导致表单提交到空地址,实际并未发送到Easypaisa的网关。
修复方式:
在initiate.ts的返回结果中添加Easypaisa沙箱或生产环境的URL:
- 沙箱环境:
https://easypaystg.easypaisa.com.pk/easypay/Index.js - 生产环境:
https://easypay.easypaisa.com.pk/easypay/Index.js
修改后端返回代码:
return res.status(200).json({ easypayURL: "https://easypaystg.easypaisa.com.pk/easypay/Index.js", params: paramsObj });
4. 日期格式的潜在问题
formatExpiryDate函数中使用toTimeString()获取时间,这可能会受到服务器时区的影响。Easypaisa要求使用**UTC+5时区(巴基斯坦标准时间)**的时间戳,否则会导致过期时间验证失败。
修复代码:
function formatExpiryDate(): string { const now = new Date(); now.setMinutes(now.getMinutes() + 5); // 5分钟后过期 // 转换为巴基斯坦标准时间(UTC+5) const pstOffset = 5 * 60 * 60 * 1000; const pstTime = new Date(now.getTime() + pstOffset); const yyyyMMdd = pstTime.toISOString().slice(0, 10).replace(/-/g, ''); const HHmmss = pstTime.toTimeString().slice(0, 8).replace(/:/g, ''); return `${yyyyMMdd} ${HHmmss}`; }
5. 参数大小写与必填项验证
确认所有参数的大小写与Easypaisa官方文档完全一致,比如emailAddr、mobileNum等字段的拼写不能出错;同时确保orderRefNum是唯一值,不能重复提交相同的订单号。
验证步骤
- 修复上述问题后,在后端打印
rawString和merchantHashedReq,对比官方文档的哈希生成示例,确认哈希值正确; - 使用Postman直接调用
/api/easypaisa/initiate接口,获取参数后手动构造表单提交到Easypaisa沙箱地址,测试是否能跳转; - 检查Easypaisa沙箱后台的日志,查看具体的错误详情(如果有)。
内容的提问来源于stack exchange,提问作者Mubashir Khan

