PAYATHON 2026

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、业务服务端和支付宝等多个系统,而且支付结果通常异步产生:

  1. App 请求业务服务端创建订单。
  2. 服务端生成支付参数并签名。
  3. App 调用支付宝 SDK。
  4. 支付宝客户端或收银台完成支付。
  5. 支付宝向业务服务端发送异步通知。
  6. 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。服务端需要完成以下检查:

  1. 验证通知签名。
  2. 检查 app_id、商户身份等信息是否符合预期。
  3. 根据 out_trade_no 查询本地订单。
  4. 校验通知金额与本地订单金额是否一致。
  5. 判断交易状态是否属于业务允许的支付成功状态。
  6. 通过幂等更新把订单改为已支付。
  7. 处理成功后,返回支付宝要求的成功响应。

示例逻辑:

@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 即使存在,也不应成为保存支付状态的唯一载体。

备注:内容仅供参考。