app端集成支付宝SDK时服务端是否创建Session?
结论
App 集成支付宝 SDK 后,支付流程本身不要求服务端创建 HTTP Session。
服务端通常通过以下信息维护支付状态:
- 用户登录凭证,如 JWT、Access Token
- 服务端生成的商户订单号
out_trade_no - 数据库中的订单记录
- 支付宝异步通知中的交易号
trade_no和交易状态 - Redis 分布式锁、幂等记录等辅助机制
如果业务代码主动调用 request.getSession(),或者使用的框架、认证组件自动启用了 Session,服务端仍可能创建 Session。这属于业务系统或 Web 框架的行为,并不是支付宝 SDK 创建的。
为什么不适合用 Session 维护支付流程
HTTP Session 适合保存浏览器访问期间的短期会话状态。App 支付则需要跨越 App、业务服务端和支付宝等多个系统,而且支付结果通常异步产生:
- App 请求业务服务端创建订单。
- 服务端生成支付参数并签名。
- App 调用支付宝 SDK。
- 支付宝客户端或收银台完成支付。
- 支付宝向业务服务端发送异步通知。
- App 可能同时收到同步支付结果,并向服务端查询订单状态。
支付宝发送异步通知时,不会携带用户在业务服务端的 Session Cookie。因此,即使服务端创建过 Session,收到通知后也无法通过该 Session 定位原来的用户和订单。
用户还可能关闭 App、切换网络或支付超时,支付宝也可能重试异步通知。此时 Session 可能已经过期,但订单仍需继续处理。因此,支付状态必须持久化,不能只保存在 Session 中。
常见的状态维护方式
1. 使用 Token 识别当前用户
App 请求创建订单时,在请求头中携带登录 Token:
POST /api/orders
Authorization: Bearer <access_token>
Content-Type: application/json
服务端验证 Token 后取得 userId。不要直接信任客户端提交的用户编号,也不要只在 Session 中保存用户身份。
2. 创建业务订单并持久化
服务端生成全局唯一的商户订单号,然后把用户、金额和订单状态写入数据库:
CREATE TABLE payment_order (
id BIGINT PRIMARY KEY,
out_trade_no VARCHAR(64) NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
amount DECIMAL(12, 2) NOT NULL,
status VARCHAR(32) NOT NULL,
alipay_trade_no VARCHAR(64),
paid_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
订单的初始状态可以设为 PENDING。具体状态名称由业务系统定义,例如:
PENDING:等待支付PAID:支付成功CLOSED:订单关闭REFUNDING:退款处理中REFUNDED:已退款
out_trade_no 是业务系统关联支付宝支付请求和异步通知的主要字段。
3. 服务端生成支付宝下单参数
以 Java 为例,服务端创建订单后,可以调用支付宝开放平台的 App 支付接口,再把生成的 orderString 返回给客户端:
AlipayClient alipayClient = new DefaultAlipayClient(
gatewayUrl,
appId,
merchantPrivateKey,
"json",
"UTF-8",
alipayPublicKey,
"RSA2"
);
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setNotifyUrl(notifyUrl);
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo(order.getOutTradeNo());
model.setTotalAmount(order.getAmount().toPlainString());
model.setSubject(order.getSubject());
model.setProductCode("QUICK_MSECURITY_PAY");
request.setBizModel(model);
AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
if (!response.isSuccess()) {
throw new IllegalStateException("生成支付宝支付参数失败");
}
String orderString = response.getBody();
支付宝 Java SDK 的类名和调用方式可能因版本不同而变化,接入时应以项目实际使用版本的接口定义为准。无论使用哪个版本,都不能把商户私钥放在客户端,也不能由客户端完成商户签名。
4. App 调起支付宝 SDK
客户端拿到服务端返回的 orderString 后,调用支付宝 SDK 完成支付。客户端返回的结果可以用于界面提示,但不能作为服务端发货、充值或开通权益的唯一依据。
客户端可能被篡改,也可能因网络中断而无法把结果上传到服务端。
5. 通过异步通知更新订单
支付完成后,支付宝会请求服务端配置的 notify_url。服务端需要完成以下检查:
- 验证通知签名。
- 检查
app_id、商户身份等信息是否符合预期。 - 根据
out_trade_no查询本地订单。 - 校验通知金额与本地订单金额是否一致。
- 判断交易状态是否属于业务允许的支付成功状态。
- 通过幂等更新把订单改为已支付。
- 处理成功后,返回支付宝要求的成功响应。
示例逻辑:
@PostMapping("/api/payments/alipay/notify")
public String handleNotify(HttpServletRequest request) throws Exception {
Map<String, String> params = extractParams(request);
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
return "failure";
}
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String totalAmount = params.get("total_amount");
String notifiedAppId = params.get("app_id");
PaymentOrder order = orderRepository.findByOutTradeNo(outTradeNo)
.orElse(null);
if (order == null) {
return "failure";
}
if (!appId.equals(notifiedAppId)) {
return "failure";
}
if (new BigDecimal(totalAmount).compareTo(order.getAmount()) != 0) {
return "failure";
}
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
paymentService.markPaidIfPending(outTradeNo, tradeNo);
}
return "success";
}
extractParams 应按实际 SDK 的验签要求保留通知参数,字符集和签名类型也要与接入配置一致。正式实现还需校验商户身份相关字段,具体字段以当前支付宝接口文档和实际通知内容为准。
幂等更新可以使用带状态条件的 SQL:
UPDATE payment_order
SET status = 'PAID',
alipay_trade_no = :tradeNo,
paid_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE out_trade_no = :outTradeNo
AND status = 'PENDING';
这样即使支付宝重复发送通知,也不会造成重复发货或重复增加用户余额。
6. 客户端查询业务订单状态
App 从支付宝 SDK 返回后,可以携带 Token 查询服务端订单:
GET /api/orders/202609090001
Authorization: Bearer <access_token>
服务端还要校验订单是否属于当前用户:
@GetMapping("/api/orders/{outTradeNo}")
public OrderResponse getOrder(
@PathVariable String outTradeNo,
@AuthenticationPrincipal LoginUser loginUser) {
PaymentOrder order = orderRepository
.findByOutTradeNoAndUserId(outTradeNo, loginUser.getUserId())
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND, "订单不存在"));
return OrderResponse.from(order);
}
如果客户端显示支付成功,而服务端订单暂时仍为 PENDING,可能是异步通知还没有到达。客户端可以在短时间内轮询订单状态。必要时,服务端应调用支付宝交易查询接口主动确认,不能直接采信客户端提交的支付结果。
Session 什么时候可能出现
以下情况可能导致服务端创建 Session:
- 业务代码调用了
request.getSession()。 - Spring Security 等认证组件被配置为有状态会话。
- 登录模块把用户信息保存在
HttpSession中。 - 网关或旧版 Web 系统通过 Cookie 和 Session 进行身份认证。
即使系统通过 Session 登录,也只能用它识别当前 App 请求对应的用户。支付订单和支付结果仍要写入数据库,因为支付宝异步通知、订单补偿任务和主动查询都不依赖该 Session。
如果使用 Spring Security,并希望接口保持无状态,可以采用类似配置:
http.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
这项配置只表示 Spring Security 不使用 Session 保存安全上下文。如果项目中的其他代码主动创建 Session,仍可能产生 JSESSIONID。
需要特别注意的问题
- 商户私钥只能保存在服务端,不能打包进 App。
- 金额、商品信息和优惠结果应由服务端计算,不能直接信任客户端参数。
- 创建订单接口要处理重复提交,可以使用业务请求号或幂等键。
- 收到支付通知后,必须先验签,再处理订单。
- 除验签外,还要校验订单号、金额、应用及商户身份等重要信息。
- 发货、充值和权益开通必须支持幂等处理。
- 不能因为 App 显示”支付成功”就立即执行核心业务。
- 异步通知处理应尽量简短。耗时操作可以写入可靠消息或任务队列,再异步执行。
- 应保留支付通知和状态变更日志,但不要记录私钥、完整 Token 等敏感数据。
- 订单需要设置合理的超时时间,并通过定时任务或支付查询机制处理长期处于
PENDING状态的订单。
完整支付链路依赖的是登录 Token、商户订单号、持久化订单状态、验签通知和幂等处理。Session 即使存在,也不应成为保存支付状态的唯一载体。
备注:内容仅供参考。
