为什么网上银行等安全应用很少全面使用 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;
SameSiteCookie;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,但必须具有独立且可追踪的交易语义:
- 创建或修改付款指令;
- 批准、拒绝或撤销付款;
- 调整客户余额;
- 修改收款账户;
- 批量导入付款数据;
- 确认对账结果;
- 导出包含敏感信息的报表;
- 改变用户权限或审批规则。
这些操作至少需要具备:
- 明确的确认页面或确认对话框;
- 由服务端重新读取并校验最新数据;
- 防止重复提交的幂等机制;
- 并发版本检查;
- 满足不可抵赖要求的审计记录;
- 请求关联 ID;
- 清楚区分成功、失败、处理中和结果未知;
- 重试之前先查询原交易状态。
“请求超时”不等于”交易失败”。服务端可能已经完成操作,只是响应没有到达浏览器。在金融系统中,遇到这种情况尤其不能直接重新提交。
推荐的实施步骤
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 是否安全”。需要判断的是,增加客户端复杂度能否换来足够的业务收益,以及团队能否证明每项关键操作在并发、重试、超时、会话失效和恶意请求下仍能得到正确结果。
参考资料
- OWASP Application Security Verification Standard:Web 应用身份认证、访问控制、输入验证、会话和 API 安全的验证要求。
- OWASP AJAX Security Cheat Sheet:AJAX 应用中的客户端信任、数据处理和接口安全原则。
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet:CSRF Token、SameSite Cookie 和来源校验等防护方式。
- OWASP Cross Site Scripting Prevention Cheat Sheet:输出编码、危险 DOM API 和 XSS 防护方法。
- OWASP Transaction Authorization Cheat Sheet:高风险交易授权、交易数据绑定和流程控制原则。
- OWASP REST Security Cheat Sheet:REST API 的访问控制、HTTP 方法、状态码和安全响应头建议。
- RFC 9110: HTTP Semantics:HTTP 方法的安全性、幂等性、缓存和条件请求语义。
- W3C Web Content Accessibility Guidelines:动态内容、焦点管理、状态通知和键盘可操作性要求。
备注:内容仅供参考。
