AngularJS http post 后跳转支付网关时报错
支付老李
结论
这两个错误的原因不同:
$interpolate:interr通常出现在模板插值form的action时。AngularJS 会把外部支付地址作为受保护的资源 URL 进行检查,检查失败后便会报错。具体原因要看错误中嵌套的$sce信息。element.submit is not a function是因为指令中的element是 AngularJS/jqLite(或 jQuery)包装对象,并非原生<form>元素。
建议不要在模板中动态插值支付网关地址。收到事件后,直接设置原生表单属性,创建隐藏字段,再调用原生表单的提交方法。
推荐修改
先在页面中放置一个用于跳转的表单:
<form auto-submit-form event="gateway.redirect"></form>
控制器只需广播一次事件,并传入完整数据:
$scope.transaction = function () {
var data = $scope.details;
var formData = {
redirectUrl: 'https://payment-gateway-url',
redirectMethod: 'POST',
redirectData: {
id: data.id,
para1: data.para1
}
};
$rootScope.$broadcast('gateway.redirect', formData);
};
对应的指令可以这样写:
app.directive('autoSubmitForm', ['$timeout', function ($timeout) {
return {
restrict: 'A',
link: function ($scope, element, $attrs) {
var form = element[0];
$scope.$on($attrs.event, function (event, formData) {
if (!formData || !formData.redirectUrl) {
return;
}
// 实际项目中应限制为受信任的支付网关地址。
form.action = formData.redirectUrl;
form.method =
String(formData.redirectMethod || 'POST').toUpperCase() === 'GET'
? 'GET'
: 'POST';
// 删除上一次提交生成的隐藏字段。
while (form.firstChild) {
form.removeChild(form.firstChild);
}
angular.forEach(formData.redirectData, function (value, name) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = value == null ? '' : String(value);
form.appendChild(input);
});
// 等当前 AngularJS 更新结束后提交。
$timeout(function () {
HTMLFormElement.prototype.submit.call(form);
}, 0, false);
});
}
};
}]);
为什么不能直接调用 element.submit()
AngularJS 指令传入的 element 是类似下面这样的包装对象:
angular.element(formElement)
真正的 DOM 节点在下标 0 中:
element[0]
所以最简单的修改方式是:
element[0].submit();
更稳妥的写法则是:
HTMLFormElement.prototype.submit.call(element[0]);
如果表单中有一个名为 submit 的控件,例如:
<input name="submit">
这个控件可能会覆盖表单自身的 submit 方法。直接调用原型方法可以避开这一问题。
$interpolate:interr 的常见原因
如果原模板包含类似代码:
<form action="{{formData.redirectUrl}}"
method="{{formData.redirectMethod}}">
AngularJS 会对 action 执行插值和安全检查。遇到外部支付网关 URL 时,某些 AngularJS 版本或安全配置还可能报告 $sce:insecurl,外层则显示为 $interpolate:interr。
不要为了消除错误就直接信任任意 URL。支付跳转地址通常应由后端生成,或者至少限制在明确的支付网关域名内。例如:
function isAllowedGateway(url) {
return /^https:\/\/payment-gateway-url(?:\/|$)/.test(url);
}
验证通过后,再设置表单地址:
form.action = formData.redirectUrl;
事件发送也需要修正
原代码连续执行了:
$rootScope.$broadcast('gateway.redirect', formData);
$scope.$emit('gateway.redirect');
第二次调用 $emit() 时没有传入 formData。而且 $emit() 只沿作用域树向上传递,通常无法通知兄弟作用域中的指令。这可能导致重复处理,也可能让监听器收到 undefined。
保留一次广播即可:
$rootScope.$broadcast('gateway.redirect', formData);
如果控制器和指令之间有明确的父子作用域关系,也可以使用局部作用域事件。对于更大的项目,通过服务协调会更合适,可以避免大量依赖 $rootScope。
注意事项
原生 HTML 表单提交会让浏览器离开当前页面,适合用来跳转支付网关。它和 $http.post() 不同,通常不受 Ajax CORS 限制。
还需要注意以下几点:
redirectUrl必须来自可信来源,不能直接使用用户输入。- 支付金额、订单号和签名必须由服务端校验,不能信任前端隐藏字段。
- 如果支付网关要求嵌套对象、数组或特定的签名格式,需要按照接口文档展开字段。
- 如果页面启用了严格的 Content Security Policy,支付网关域名还必须符合页面的
form-action策略。 replace: true已被弃用,也没有必要使用。建议把指令直接挂载到真实的<form>元素上。
备注:内容仅供参考。
