CFSDK payment gateway 报错 Invalid token sent in request
结论
这段 CFViewController 初始化代码本身没有明显的结构问题。“Invalid token sent in request”通常与 WebView 或 UINavigationController 无关,更可能是传入的 tokenData 与当前支付环境、商户账号或订单信息不匹配。
失败地址是 https://test.cashfree.com/...,说明 SDK 运行在测试环境。对应的 token 也必须使用同一个 Cashfree 测试账号和测试环境凭据生成,并且生成 token 时使用的 orderId 要与客户端传入的值完全一致。生产环境的 token、其他 App ID 生成的 token,以及重复订单对应的旧 token,都会被拒绝。
返回页面无法关闭是另一个问题。CFViewController 是新建导航控制器的根节点,如果 SDK 调用 popViewController,导航栈中没有上一层页面,因此页面不会自动 dismiss。
排查 Invalid token
先在后端逐项核对:
- SDK 使用测试环境时,后端也必须调用 Cashfree 的测试环境接口。
- 生成 token 使用的 Client ID 和 Client Secret,必须与传入 SDK 的
appId属于同一个账号。 - 生成 token 时使用的
orderId必须与params["orderId"]完全一致,包括大小写和空格。 - 如果 token 接口还接收
orderAmount和orderCurrency,这些值也必须与 SDK 参数一致。 tokenData只能包含接口响应中的 token 字符串,不能传入完整 JSON、带引号的 JSON 字符串或经过 Base64 编码的响应。- 不要再次对 token 进行 URL 编码、转义或拼接。
- 检查后端响应和本地保存过程,去掉意外加入的换行符及首尾空格。
- 不要复用其他订单的 token。只要修改了
orderId,就应重新生成 token。 - 确认 token 创建请求已经成功,避免把错误响应中的空值或错误信息当作 token 返回给客户端。
客户端也可以先做基本校验:
func getPaymentParams() -> [String: String] {
let orderId = self.orderId.trimmingCharacters(in: .whitespacesAndNewlines)
let token = self.paymentToken.trimmingCharacters(in: .whitespacesAndNewlines)
precondition(!orderId.isEmpty, "orderId is empty")
precondition(!token.isEmpty, "paymentToken is empty")
return [
"orderId": orderId,
"tokenData": token,
"orderAmount": self.paymentValue,
"customerName": "name",
"orderNote": "health products",
"orderCurrency": "INR",
"customerPhone": "9876543210",
"customerEmail": "abc@gmail.com",
"notifyUrl": "https://your-domain.example/payment/cashfree/notify"
]
}
不要在日志中打印完整 token。调试时记录 token 长度和少量前缀即可:
let token = paymentToken.trimmingCharacters(in: .whitespacesAndNewlines)
print("Environment:", environmentCF)
print("App ID:", appId)
print("Order ID:", orderId)
print("Token length:", token.count)
print("Token prefix:", String(token.prefix(6)))
还要重点检查后端返回值的解析方式。如果 token 接口返回 JSON,应从中提取真正的 token 字段,不能把整个响应直接传给 SDK:
// 错误示例:把完整 JSON 当作 token
paymentToken = responseString
正确做法是先解析 JSON,再读取对应字段。旧版 Cashfree 接口使用的字段名可能是 cftoken,具体应以后端实际收到的响应为准:
struct TokenResponse: Decodable {
let cftoken: String
}
如果环境、App ID 和订单参数均一致,可以在后端记录一次脱敏后的 token 创建请求及响应状态。客户端收到“Invalid token”时,需要逐项对照以下内容:
token 创建环境
+
创建 token 的商户凭据
+
SDK 中的 appId
+
SDK environment
+
orderId
+
orderAmount / currency
notifyUrl 配置错误通常会影响支付结果通知,一般不会导致提交页面立即返回“Invalid token”。
正确处理返回操作
方案一:推入现有导航栈
如果当前页面已经在 UINavigationController 中,优先直接 push:
func initiateCFSDK() {
let cashfreeVC = CFViewController(
params: getPaymentParams(),
appId: self.appId,
env: self.environmentCF,
callBack: self
)
self.navigationController?.pushViewController(
cashfreeVC,
animated: true
)
}
这样一来,SDK 或用户执行返回操作时,popViewController 才能回到上一层页面。
方案二:模态展示并自行提供关闭按钮
如果必须使用 present,应由外层页面调用 dismiss:
func initiateCFSDK() {
let cashfreeVC = CFViewController(
params: getPaymentParams(),
appId: self.appId,
env: self.environmentCF,
callBack: self
)
cashfreeVC.navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .close,
target: self,
action: #selector(closeCashfreeCheckout)
)
let navVC = UINavigationController(rootViewController: cashfreeVC)
navVC.modalPresentationStyle = .fullScreen
present(navVC, animated: true)
}
@objc private func closeCashfreeCheckout() {
presentedViewController?.dismiss(animated: true)
}
支付成功、失败或用户取消后,也可以在 SDK delegate 回调中关闭页面:
private func closeCheckout() {
if presentedViewController != nil {
dismiss(animated: true)
} else {
navigationController?.popViewController(animated: true)
}
}
具体的 delegate 方法名取决于项目使用的 Cashfree SDK 版本。应在该版本提供的成功、失败或取消回调中调用 closeCheckout()。
其他注意事项
不能只根据客户端 WebView 回调认定支付成功。客户端回调可能中断或被伪造,最终订单状态应由后端通过 Cashfree 的订单查询接口或签名校验来确认。
示例中的 CFViewController、tokenData 和 cftoken 也可能属于旧版集成方式。如果项目使用较新的 Cashfree SDK,而后端仍通过旧接口生成 token,或者客户端使用旧 SDK、后端却采用新版接口,同样可能出现凭据无法识别的问题。iOS SDK、后端创建订单接口和客户端参数必须属于同一套 API 版本,不能混用旧版 token 与新版 payment session。
备注:内容仅供参考。
