PAYATHON 2026

为什么网上银行等安全应用很少全面使用 AJAX?

支付阿杰

结论

AJAX 本身不会降低网上银行或支付系统的安全性。系统是否安全,取决于服务端的身份认证、权限校验、交易完整性、CSRF/XSS 防护、会话管理、审计和异常恢复,与请求是通过页面跳转还是 XMLHttpRequest/fetch() 发起没有直接关系。

不过,对于付款、余额调整、对账确认等高风险业务,不建议把所有功能塞进一个单页,并让全部操作依赖 AJAX。更稳妥的做法是:

  • 使用独立 URL 和服务端业务边界组织 Accounts、Payments、Reports 等主要模块;
  • 在搜索、筛选、联动校验、局部刷新、进度查询等确实能改善体验的环节使用 AJAX;
  • 为关键交易保留清楚的”录入、复核、确认、提交、结果”流程;
  • 所有安全控制都由服务端执行,不信任浏览器保存或提交的状态;
  • JavaScript、网络请求或局部组件失败后,用户仍能恢复操作并确认最终状态。

这更接近方案 2,但并不排斥 SPA。如果团队具备成熟的前端工程能力、自动化测试体系和安全治理能力,SPA 同样可以用于金融系统,前提是其额外复杂度能带来明确收益。

为什么传统网上银行常使用页面跳转

先澄清一点:现代网上银行未必很少使用 AJAX。许多系统会在后台大量调用接口,只是仍保留独立 URL、完整页面框架和传统导航,所以用户未必能察觉。

传统页面跳转长期占主流,主要有以下几个原因。

1. 故障边界更清楚

当 Accounts、Payments、Reports 分别使用独立页面时,一个页面的脚本异常通常不会影响整个应用。刷新页面也能重新取得由服务端生成的可靠状态。

如果全部功能都运行在一个长期存活的页面中,某个组件可能污染全局状态、重复注册事件、泄漏内存,甚至导致后续模块失效。对于可能连续打开数小时的操作后台,这类问题比普通网站更突出。

2. 交易流程更容易表达和审计

支付系统通常不是简单的增删改查,而是一套状态机:

Draft → PendingApproval → Approved → Submitted → Reconciled

不同状态可能对应不同的角色、权限和可执行操作。采用独立路由和明确的服务端状态后,以下问题更容易回答:

  • 用户当时看到了什么?
  • 用户确认的是哪个版本的数据?
  • 哪个请求改变了交易状态?
  • 请求是否被重复提交?
  • 操作失败后,交易究竟成功还是失败?

全 AJAX 架构也可以做到这些,但需要另外建立严格的路由、状态管理、请求关联和审计机制。

3. 安全团队更容易评估

传统请求响应模型的入口比较明确,每个页面和表单通常都有固定的控制器、权限规则和输入验证。

SPA 会增加接口数量和客户端状态。安全评审需要同时检查:

  • 页面路由权限;
  • API 对象级权限;
  • DOM XSS;
  • CSRF;
  • 跨域配置;
  • Token 存储;
  • 客户端缓存;
  • 请求重放;
  • 第三方脚本;
  • 前端依赖供应链。

这不代表 SPA 不安全,只是它的攻击面更加分散,评审成本通常也更高。

4. 可访问性和兼容性风险较低

完整页面导航原生支持标题更新、焦点重置、浏览器历史记录和屏幕阅读器通知。局部替换内容时,开发者需要主动实现这些行为。

银行系统可能需要兼容企业浏览器、辅助技术或受控终端环境。在这些场景中,传统页面通常更容易获得稳定且一致的结果。

5. 历史技术和风险偏好

银行系统的使用寿命通常很长,许多后端在 SPA 普及之前就已建立。把整套系统改造成单页应用,会影响认证、审计、交易流程、浏览器兼容和自动化测试,而实际收益可能只是少几次页面刷新。

因此,保守架构更多源于迁移成本和风险控制,而不是因为 AJAX 无法满足银行的安全要求。

AJAX 对安全性的实际影响

AJAX 请求没有特殊的信任级别

浏览器中的 JavaScript、隐藏字段、按钮状态和前端路由都可以被修改。攻击者也可以绕过页面,直接构造 HTTP 请求。

因此,服务端必须针对每个请求重新执行:

  • 身份认证;
  • 功能级权限校验;
  • 对象级权限校验;
  • 输入验证;
  • 业务状态校验;
  • 金额、币种和账户关系校验;
  • 并发及重复提交检查;
  • 审计记录。

例如,页面隐藏了”批准付款”按钮,并不意味着普通录入员无法发起批准请求:

@PostMapping("/api/payments/{paymentId}/approve")
public Payment approve(
        @PathVariable UUID paymentId,
        Principal principal) {

    Payment payment = paymentService.findRequired(paymentId);

    authorizationService.requirePermission(
        principal,
        "PAYMENT_APPROVE",
        payment.getClientId()
    );

    paymentService.requireCurrentState(
        payment,
        PaymentState.PENDING_APPROVAL
    );

    return paymentService.approve(payment, principal.getName());
}

权限检查必须落实到具体付款和客户,不能只检查用户是否已经登录。

XSS 的影响可能更大

SPA 经常把接口响应插入 DOM。如果使用 innerHTML 拼接未经处理的数据,容易引发 DOM XSS:

// 不安全:description 可能包含可执行 HTML
panel.innerHTML = payment.description;

应优先使用文本 API:

panel.textContent = payment.description;

如果业务确实需要显示富文本,应使用经过验证的 HTML 清理组件,并配置严格的 Content Security Policy。不要自行使用正则表达式过滤 HTML。

XSS 对金融应用尤其危险。恶意脚本可能读取页面数据、替换收款账户、伪造确认界面,或以当前用户的身份调用接口。

CSRF 仍然存在

如果认证依赖浏览器自动携带的 Cookie,AJAX 请求同样可能受到 CSRF 攻击。可以综合采用以下措施:

  • CSRF Token;
  • SameSite Cookie;
  • Origin 或 Referer 校验;
  • 禁止使用 GET 改变业务状态;
  • 对关键操作再次确认或进行交易授权。

示例:

async function approvePayment(paymentId, version) {
  const response = await fetch(
    `/api/payments/${encodeURIComponent(paymentId)}/approve`,
    {
      method: "POST",
      credentials: "same-origin",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-Token": getCsrfToken(),
        "Idempotency-Key": crypto.randomUUID()
      },
      body: JSON.stringify({ version })
    }
  );

  if (!response.ok) {
    throw new Error(`Approval failed: HTTP ${response.status}`);
  }

  return response.json();
}

CSRF Token、幂等键和版本号都必须由服务端验证。只在请求中加入自定义请求头,并不会自动形成有效的安全控制。

客户端状态可能过期

单页应用可能长时间保存账户余额、付款状态和用户权限。在此期间,其他操作员可能已经修改数据,用户权限也可能被撤销。

提交时应携带版本信息,由服务端执行乐观锁检查:

PATCH /api/payments/6f94...
If-Match: "payment-version-17"
Content-Type: application/json

{
  "amount": "1250.00",
  "currency": "CNY"
}

如果版本已经改变,服务端应返回 409 Conflict 或 412 Precondition Failed,要求用户重新加载并核对数据,不能静默覆盖。

敏感数据更容易滞留

SPA 可能把客户资料、余额和交易信息保存在以下位置:

  • JavaScript 全局变量;
  • 状态管理容器;
  • localStorage;
  • sessionStorage;
  • IndexedDB;
  • 浏览器调试日志;
  • 前端错误监控平台。

敏感业务数据应尽量只保存在内存中,并在退出、会话过期或切换客户时清理。认证令牌通常不宜直接存入 localStorage,因为页面发生 XSS 后,脚本可以读取其中的数据。使用受保护的 Cookie 时,应根据架构设置 Secure、HttpOnly 和适当的 SameSite 属性。

响应还需要考虑缓存策略,例如:

Cache-Control: no-store
Pragma: no-cache

no-store 可以降低敏感响应被浏览器或中间缓存保存的风险,但具体策略仍需结合报表下载、企业代理和离线需求来设计。

会话超时更难察觉

传统页面跳转会自然发起新的完整请求。SPA 页面则可能继续显示,即使服务器会话已经失效。用户可能填写了大量数据,直到提交时才发现登录已经过期。

系统应统一处理 401 Unauthorized 和 403 Forbidden,并明确区分以下情况:

  • 未登录或会话过期;
  • 已登录但没有权限;
  • 操作需要重新认证;
  • 后端暂时不可用。

高风险操作可以要求近期认证或额外的交易确认。页面上的倒计时不能代替服务端会话控制。

AJAX 对性能的影响

AJAX 不一定更快,它主要改变数据传输和页面更新的方式。

可能的优势包括:

  • 无需重复下载页面外壳、导航和公共资源;
  • 局部更新需要传输的数据较少;
  • 输入校验、筛选和分页的响应更自然;
  • 可以并行获取互不依赖的数据;
  • 用户不必在每次操作后等待整个页面刷新。

相应的成本包括:

  • 首次加载需要下载较大的 JavaScript 包;
  • 初始化、解析和执行脚本会占用 CPU;
  • 单个页面可能触发大量细碎请求;
  • 客户端状态管理会增加内存消耗;
  • 慢速网络下可能发生加载顺序错乱或局部空白;
  • 监控、缓存和性能分析更加复杂;
  • 页面长时间运行后,可能积累事件监听器或出现内存泄漏。

评估性能时,不能只比较完整 HTML 和 JSON 的大小,还要测量:

  • 首次可交互时间;
  • 关键业务操作的完成时间;
  • 请求数量及总传输量;
  • JavaScript 执行时间;
  • 慢速网络和高延迟环境下的表现;
  • 长时间使用后的内存占用;
  • API 失败或超时后的恢复时间。

两种方案的取舍

维度单页并全面使用 AJAX按需使用 AJAX
安全性可以做到安全,但 API、客户端状态、XSS 和 Token 管理的评审范围更大安全边界通常更直观,仍需完整保护所有 AJAX 接口
性能后续导航可能更快,但首次加载和脚本执行成本较高首次加载和故障恢复较简单,页面间可能重复传输少量内容
可维护性需要成熟的路由、状态管理、接口契约和前端测试体系模块相对独立,服务端渲染与局部增强可以渐进维护
用户体验适合高频操作、复杂工作台和大量局部交互关键流程清晰,刷新和深链接更稳定,但页面切换可能略慢
故障恢复必须自行处理局部失败、状态失效和重试刷新或重新导航通常可以恢复到可靠状态
测试成本端到端测试、接口测试和前端状态测试都很重要页面级测试通常更简单,但混合交互仍需端到端测试
发布风险前端包可能同时影响多个模块模块能够相对独立地发布和回滚

适合使用 AJAX 的场景

在支付管理系统中,以下功能通常适合使用 AJAX:

  • 客户或账户搜索;
  • 付款列表筛选、排序和分页;
  • 收款账户联动查询;
  • 表单中的非最终性校验;
  • 展开或收起对账差异;
  • 查询批量任务进度;
  • 查询报表生成状态;
  • 更新通知和待办数量;
  • 不改变资金状态的局部刷新。

即使前端已经完成校验,服务端仍要重新验证所有输入。

应谨慎设计的关键操作

以下操作不一定要禁用 AJAX,但必须具有独立且可追踪的交易语义:

  • 创建或修改付款指令;
  • 批准、拒绝或撤销付款;
  • 调整客户余额;
  • 修改收款账户;
  • 批量导入付款数据;
  • 确认对账结果;
  • 导出包含敏感信息的报表;
  • 改变用户权限或审批规则。

这些操作至少需要具备:

  1. 明确的确认页面或确认对话框;
  2. 由服务端重新读取并校验最新数据;
  3. 防止重复提交的幂等机制;
  4. 并发版本检查;
  5. 满足不可抵赖要求的审计记录;
  6. 请求关联 ID;
  7. 清楚区分成功、失败、处理中和结果未知;
  8. 重试之前先查询原交易状态。

“请求超时”不等于”交易失败”。服务端可能已经完成操作,只是响应没有到达浏览器。在金融系统中,遇到这种情况尤其不能直接重新提交。

推荐的实施步骤

1. 先划分业务模块和安全边界

为主要模块建立稳定路由,例如:

/accounts
/payments
/payments/{id}
/reconciliation
/reports
/admin

无论完整页面是否由服务端渲染,都不应影响这些业务边界。即使采用 SPA,也应保证路由可以直接访问、刷新和恢复。

2. 把接口设计成不信任客户端

每个写接口都应验证用户、客户范围、业务权限、对象状态和版本。金额应使用十进制定点类型,不要直接使用 JavaScript 浮点数完成最终的资金计算。

例如,接口可以使用字符串表示金额:

{
  "amount": "1250.00",
  "currency": "CNY"
}

服务端再使用 BigDecimal 或数据库的精确数值类型处理。

3. 为写操作提供幂等机制

客户端为一次业务意图生成幂等键,服务端保存该键和操作结果。再次提交同一个键时,服务端返回原结果,而不是重复执行操作。

幂等键不能代替业务唯一约束。例如,同一付款文件能否重复导入,仍需根据文件摘要、业务编号或其他规则判断。

4. 统一处理错误和未知状态

前端应区分:

400:输入不符合接口要求
401:会话无效
403:没有权限
409/412:数据已被其他操作修改
422:业务规则不允许
429:请求过于频繁
5xx:服务端或依赖系统异常
Network error:结果可能未知

发生网络错误后,应通过查询接口确认原操作的状态,不能自动重放高风险请求。

5. 建立安全响应头和前端约束

根据实际部署方式配置:

  • Content Security Policy;
  • frame-ancestors 或 X-Frame-Options;
  • X-Content-Type-Options: nosniff;
  • HTTPS 和 HSTS;
  • 合理的 Cookie 属性;
  • 敏感响应的缓存策略;
  • 严格受控的 CORS。

CORS 不是身份认证机制,也不能代替 CSRF 防护。

6. 用真实业务流程做测试

除了单元测试,还应覆盖以下场景:

  • 同一付款被两名用户同时审批;
  • 用户打开页面后权限被撤销;
  • 会话在填写过程中到期;
  • 提交成功但响应丢失;
  • 用户连续点击提交;
  • 银行接口超时;
  • 页面脚本只加载了一部分;
  • 旧版本前端访问新版本 API;
  • 恶意修改客户 ID、付款 ID、金额和状态;
  • 批量操作只完成一部分。

与”是否使用 AJAX”相比,这些场景更能检验系统是否可靠。

关于禁用后退按钮

不建议把禁用后退按钮作为安全措施。浏览器历史记录属于用户代理行为,很难可靠禁用,也无法阻止攻击者直接调用接口。

正确的处理方式包括:

  • 使用 Post/Redirect/Get 或清晰的前端路由;
  • 防止重复提交;
  • 对过期页面重新检查服务端状态;
  • 为敏感响应设置适当的缓存策略;
  • 确保后退和刷新不会重复执行交易;
  • 恢复页面时,明确提示数据是否已经过期。

如果用户返回旧页面后仍能再次批准已经批准的付款,问题出在服务端状态校验,而不是后退按钮。

选择原则

如果系统主要用于频繁处理大量列表、对账差异和数据录入,SPA 可能显著提高操作效率。前提是团队能够长期维护前端路由、状态管理、API 版本、端到端测试、安全响应头、依赖升级和可观测性。

如果系统以低频、高风险流程为主,或者团队的前端工程和安全能力还不成熟,服务端页面配合局部 AJAX 通常更合适。这种方案能提供足够好的体验,也更容易控制复杂度和故障范围。

因此,架构决策不该停留在”AJAX 是否安全”。需要判断的是,增加客户端复杂度能否换来足够的业务收益,以及团队能否证明每项关键操作在并发、重试、超时、会话失效和恶意请求下仍能得到正确结果。

参考资料

备注:内容仅供参考。