PAYATHON 2026

支付宝个人协议页面签约接口如何生成签约二维码?

支付小周

结论

支付宝个人协议页面签约接口是页面类接口,返回用于跳转到支付宝签约页面的 HTML 表单,并非 JSON 业务数据,因此不能用 JSON 工具将返回内容转换为实体类。

正确的处理方式如下:

  1. 使用支付宝 SDK 的 pageExecute 调用接口。
  2. 读取响应中的 HTML 内容。
  3. 服务端将 HTML 原样输出给浏览器,由表单自动提交并跳转到支付宝签约页面。
  4. 如果产品需要二维码,应将本系统的签约入口 URL 生成二维码。用户扫码访问该 URL 后,服务端再调用支付宝接口并输出 HTML。

转换实体类为什么会报错

页面类接口通常返回类似下面的内容:

<form name="punchout_form"
      method="post"
      action="https://openapi.alipay.com/gateway.do?...">
    <input type="hidden" name="biz_content" value="...">
</form>
<script>
    document.forms[0].submit();
</script>

这是一段可以执行页面跳转的内容,不是下面这种 JSON:

{
  "code": "10000",
  "msg": "Success"
}

如果对这段 HTML 执行 JSON.parseObject()、Jackson readValue() 或类似的实体转换操作,解析器会在 <html>、<form> 等标签处报错。

推荐实现方式

1. 创建本系统的签约入口

前端请求本系统接口,例如:

https://example.com/agreement/sign?token=一次性业务令牌

二维码中也应保存这个地址,而不是支付宝接口返回的整段 HTML。

token 需要关联用户和签约业务信息。建议为它设置有效期和使用次数限制,不要在二维码中直接暴露用户编号、协议号等敏感参数。

2. 服务端调用支付宝页面签约接口

下面以支付宝 Java SDK 的常见调用方式为例。具体请求类和业务参数,需要以当前接入产品的接口文档及项目使用的 SDK 版本为准。

@GetMapping(value = "/agreement/sign", produces = "text/html;charset=UTF-8")
@ResponseBody
public String sign(@RequestParam String token) throws AlipayApiException {
    // 1. 校验一次性 token,并从服务端查询对应的用户和签约信息
    AgreementContext context = agreementService.verifyToken(token);

    // 2. 构造个人协议页面签约请求
    AlipayUserAgreementPageSignRequest request =
            new AlipayUserAgreementPageSignRequest();

    request.setNotifyUrl("https://example.com/alipay/agreement/notify");
    request.setReturnUrl("https://example.com/agreement/result");

    AlipayUserAgreementPageSignModel model =
            new AlipayUserAgreementPageSignModel();

    // 以下字段仅表示设置业务参数的位置。
    // 实际必填字段、字段名称和取值规则以对应接口文档为准。
    // model.setPersonalProductCode(...);
    // model.setSignScene(...);
    // model.setExternalAgreementNo(...);

    request.setBizModel(model);

    // 3. 页面类接口应使用 pageExecute
    AlipayUserAgreementPageSignResponse response =
            alipayClient.pageExecute(request);

    if (response == null || response.getBody() == null) {
        throw new IllegalStateException("支付宝签约页面生成失败");
    }

    // 4. 直接返回 HTML,不要转换为实体类
    return response.getBody();
}

部分支付宝 Java SDK 版本的 pageExecute 可能直接返回 String,对应写法如下:

String html = alipayClient.pageExecute(request);
return html;

具体应以项目实际引入的 SDK 方法签名为准。无论使用哪个版本,处理原则都一样:取得 HTML 后以 text/html 输出,不要按 JSON 解析。

如果需要直接操作 HttpServletResponse,可以这样处理:

@GetMapping("/agreement/sign")
public void sign(
        @RequestParam String token,
        HttpServletResponse response
) throws Exception {
    AgreementContext context = agreementService.verifyToken(token);

    AlipayUserAgreementPageSignRequest request =
            new AlipayUserAgreementPageSignRequest();

    request.setNotifyUrl("https://example.com/alipay/agreement/notify");
    request.setReturnUrl("https://example.com/agreement/result");

    AlipayUserAgreementPageSignModel model =
            new AlipayUserAgreementPageSignModel();

    // 按接口文档设置实际业务参数
    request.setBizModel(model);

    AlipayUserAgreementPageSignResponse alipayResponse =
            alipayClient.pageExecute(request);

    String html = alipayResponse.getBody();

    response.setCharacterEncoding("UTF-8");
    response.setContentType("text/html;charset=UTF-8");
    response.getWriter().write(html);
    response.getWriter().flush();
}

3. 将本系统签约入口生成二维码

可以使用 ZXing 将入口 URL 转成二维码:

public BufferedImage createQrCode(String signEntryUrl) throws WriterException {
    Map<EncodeHintType, Object> hints = new HashMap<>();
    hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name());
    hints.put(EncodeHintType.MARGIN, 1);
    hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);

    BitMatrix matrix = new QRCodeWriter().encode(
            signEntryUrl,
            BarcodeFormat.QR_CODE,
            320,
            320,
            hints
    );

    return MatrixToImageWriter.toBufferedImage(matrix);
}

输出 PNG:

@GetMapping(value = "/agreement/qrcode", produces = MediaType.IMAGE_PNG_VALUE)
public void qrcode(HttpServletResponse response) throws Exception {
    String token = agreementService.createOneTimeToken();

    String signEntryUrl =
            "https://example.com/agreement/sign?token="
            + URLEncoder.encode(token, StandardCharsets.UTF_8);

    BufferedImage image = createQrCode(signEntryUrl);

    response.setContentType(MediaType.IMAGE_PNG_VALUE);
    ImageIO.write(image, "PNG", response.getOutputStream());
}

完整流程如下:

生成一次性 token
        ↓
组成本站签约入口 URL
        ↓
将 URL 生成二维码
        ↓
用户使用支付宝或浏览器扫码
        ↓
访问本站签约入口
        ↓
服务端调用 pageExecute
        ↓
向浏览器输出支付宝返回的 HTML
        ↓
自动跳转到支付宝签约页面

为什么不建议解析 HTML 后生成支付宝地址

理论上可以从 HTML 中提取 <form> 的 action 和隐藏字段,再自行拼接请求地址,但不建议这样处理,原因如下:

  • 表单可能通过 POST 提交,无法简单转换成二维码 URL。
  • 参数可能包含特殊字符,手动拼接容易出现编码错误。
  • HTML 结构是 SDK 和网关的输出细节,后续可能变化。
  • 漏传、修改或重复编码参数,都可能导致验签失败。

除非接口文档明确提供了可直接扫码访问的签约 URL 字段,否则应将本站入口 URL 放入二维码,再由本站输出支付宝返回的 HTML。

注意事项

  • notify_url 用于接收支付宝服务端的异步通知,不能只根据 return_url 判断签约是否成功。
  • 收到异步通知后必须验签,并核对应用 ID、协议号、外部业务号和签约状态等信息。
  • 二维码入口应使用 HTTPS,并通过一次性令牌关联业务数据。
  • 不要将应用私钥、完整业务参数或用户敏感信息放入二维码。
  • 令牌需要设置有效期和防重放机制,并在使用后失效或标记状态。
  • 不要把支付宝返回的 HTML 存进 JSON 字段,再交给前端解析。如果由前端发起请求,应让浏览器直接访问签约入口。
  • 如果前端通过 Ajax 请求签约接口,即使取得了 HTML,页面通常也不会自动完成顶层跳转。应使用 window.location.href、普通链接或表单访问本站签约入口。
  • 沙箱环境和生产环境的支付宝网关、应用配置、回调地址及密钥必须相互对应,不能混用。

备注:内容仅供参考。