重要提示:JSP 和微信支付政策

在开始之前,必须强调一个关键点:

微信 jsp网页支付demo
(图片来源网络,侵删)

微信官方早已不再推荐甚至限制在纯 HTML/JSP 页面中直接调起微信支付。

  • 原因:微信支付的安全校验是基于微信浏览器的特定 JS-SDK (jssdk),它要求页面必须在微信内置浏览器中打开,纯 HTML/JSP 页面没有后端能力来获取 appId, timestamp, nonceStr, signature 等必要参数来调用 JS-SDK。
  • 官方推荐方案:微信官方推荐的 H5 支付,其流程是:
    1. 用户在手机浏览器或微信内访问你的网页。
    2. 你的后端服务器判断用户环境(是否在微信内)。
    3. 如果在微信内,生成一个支付 URL。
    4. 你的网页前端(使用 JavaScript)将这个 URL 重定向到微信客户端,微信客户端会自动拉起支付。
    5. 如果不在微信内,则跳转到普通扫码支付页面。

下面我将为您提供一个 “最接近 JSP 开发者习惯”的解决方案,它本质上是一个 模拟的、用于演示的 JSP Demo,在实际生产环境中,请务必遵循微信官方的 H5 支付流程。


第一步:准备工作(在微信商户平台配置)

在写任何代码之前,你必须在微信支付商户平台完成以下配置:

  1. 申请微信支付商户号:拥有一个审核通过的商户号。
  2. 获取关键参数
    • APIv3 密钥 (APIv3 Key):在【账户中心】->【API安全】->【APIv3密钥】中设置和获取,这是签名和加密解密的核心。
    • API 证书:下载商户平台提供的 API 证书(包括证书和私钥),在后续代码中,私钥用于签名,证书用于验证微信的响应。
    • AppID:你的公众号或小程序的唯一标识。
    • 商户号 (mch_id):你的商户号。
  3. 产品开通:确保在【产品中心】已经开通了【JSAPI 支付】或【H5 支付】产品。
  4. 域名配置:在【产品中心】->【开发配置】->【支付授权目录 中,配置你的网站域名,如果你的支付页面是 http://www.yourdomain.com/pay.jsp,那么你需要添加 http://www.yourdomain.com/ 到白名单中。(注意:对于 H5 支付,配置的是支付授权域名)**

第二步:创建项目环境

我们使用一个简单的 Java Web 项目来演示。

微信 jsp网页支付demo
(图片来源网络,侵删)
  1. 创建 Web 项目:在 Eclipse 或 IntelliJ IDEA 中创建一个 Dynamic Web Project。

  2. 添加依赖库:你需要引入微信支付官方提供的 SDK 和 JSON 处理库。

    • 微信支付 SDK:从 微信支付官方 GitHub 下载最新版本的 JAR 包,或者使用 Maven。
    • JSON 库:推荐使用 fastjsongson,这里以 fastjson 为例。

    Maven 依赖 (pom.xml):

    <dependencies>
        <!-- 微信支付 V3 SDK -->
        <dependency>
            <groupId>com.wechatpay.v3</groupId>
            <artifactId>wechatpay-java</artifactId>
            <version>0.2.11</version>
        </dependency>
        <!-- 阿里巴巴 fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
        <!-- Servlet API -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

第三步:后端 Java 代码实现

后端主要负责两个核心功能:

微信 jsp网页支付demo
(图片来源网络,侵删)
  1. 创建支付订单:调用微信统一下单接口,生成预支付交易会话标识 (prepay_id)。
  2. 生成前端调起支付所需参数:根据 prepay_id 和你的 AppID 等信息,生成签名后的参数,供前端 JavaScript 使用。

配置信息类 (WeChatConfig.java)

创建一个类来存储你的微信支付配置信息,方便管理。

public class WeChatConfig {
    public static final String APP_ID = "你的AppID"; // 替换为你的AppID
    public static final String MCH_ID = "你的商户号"; // 替换为你的商户号
    public static final String API_V3_KEY = "你的APIv3密钥"; // 替换为你的APIv3密钥
    public static final String NOTIFY_URL = "http://www.yourdomain.com/wechatpay/notify.jsp"; // 支付结果通知URL
    public static final String API_KEY_PATH = "D:/cert/apiclient_key.pem"; // 替换为你的私钥文件路径
}

支付服务工具类 (WeChatPayService.java)

这是核心逻辑,负责与微信服务器通信。

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.wechatpay.v3.WechatPayHttpClientBuilder;
import com.wechatpay.v3.auth.CertificatesVerifier;
import com.wechatpay.v3.auth.PrivateKeySigner;
import com.wechatpay.v3.auth.WechatPay2Credentials;
import com.wechatpay.v3.core.Config;
import com.wechatpay.v3.core.RSASSASigner;
import com.wechatpay.v3.core.exception.MalformedMessageException;
import com.wechatpay.v3.core.exception.ServiceException;
import com.wechatpay.v3.core.util.PemUtil;
import com.wechatpay.v3.jsapi.model.Amount;
import com.wechatpay.v3.jsapi.model.Payer;
import com.wechatpay.v3.jsapi.model.PrepayRequest;
import com.wechatpay.v3.jsapi.model.PrepayResponse;
import com.wechatpay.v3.jsapi.service.JsapiService;
import com.wechatpay.v3.util.AesUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class WeChatPayService {
    private final JsapiService service;
    public WeChatPayService() {
        // 1. 加载私钥
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(
                Files.readAllBytes(Paths.get(WeChatConfig.API_KEY_PATH))));
        // 2. 初始化签名器
        Config config = new Config.Builder()
                .merchantId(WeChatConfig.MCH_ID)
                .privateKey(merchantPrivateKey)
                .wxpayCertificate(new CertificatesVerifier(new WechatPay2Credentials(WeChatConfig.MCH_ID, new PrivateKeySigner(WeChatConfig.MCH_ID, merchantPrivateKey))))
                .build();
        RSASSASigner signer = new RSASSASigner(config);
        // 3. 初始化服务
        this.service = new JsapiService.Builder()
                .credentials(new WechatPay2Credentials(WeChatConfig.MCH_ID, new PrivateKeySigner(WeChatConfig.MCH_ID, merchantPrivateKey)))
                .signer(signer)
                .build();
    }
    /**
     * 创建预支付订单
     * @param openid 用户的openid (必须通过微信授权获取)
     * @param outTradeNo 商户订单号
     * @param totalFee 订单金额,单位:分
     * @return 返回前端调起支付所需的参数
     */
    public Map<String, String> createPrepayId(String openid, String outTradeNo, Integer totalFee) throws Exception {
        // 1. 构造请求body
        PrepayRequest prepayRequest = new PrepayRequest();
        prepayRequest.setAppid(WeChatConfig.APP_ID);
        prepayRequest.setMchid(WeChatConfig.MCH_ID);
        prepayRequest.setDescription("商品描述"); // 商品描述
        prepayRequest.setNotifyUrl(WeChatConfig.NOTIFY_URL);
        prepayRequest.setOutTradeNo(outTradeNo);
        Amount amount = new Amount();
        amount.setTotal(totalFee); // 金额,单位:分
        prepayRequest.setAmount(amount);
        Payer payer = new Payer();
        payer.setOpenid(openid); // 必须是用户的openid
        prepayRequest.setPayer(payer);
        // 2. 调用统一下单API
        PrepayResponse prepayResponse = service.prepay(prepayRequest);
        // 3. 获取prepay_id
        String prepayId = prepayResponse.getPrepayId();
        // 4. 生成前端调起支付所需的参数
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        Map<String, String> params = new HashMap<>();
        params.put("appId", WeChatConfig.APP_ID);
        params.put("timeStamp", timeStamp);
        params.put("nonceStr", nonceStr);
        params.put("package", "prepay_id=" + prepayId);
        params.put("signType", "RSA");
        // 5. 生成签名 (这里SDK内部处理了,我们按格式返回即可)
        // 注意:JSAPI调起支付的签名方式是固定的,不是对整个params签名,而是对appId, timeStamp, nonceStr, package进行签名
        // 微信JS-SDK会自己处理签名,我们只需要把上述参数按顺序传给它即可。
        // 但为了安全,最好按照微信文档再次生成一个签名并返回给前端,让前端也带上。
        // 在JSP Demo的简化场景下,我们通常只返回这些参数,相信微信JS-SDK。
        // 如果要自己生成签名,可以这样做:
        // String paySign = Signer.sign(params, merchantPrivateKey); // 需要一个签名工具类
        // params.put("paySign", paySign);
        return params;
    }
}

Servlet 处理支付请求 (PayServlet.java)

创建一个 Servlet 来接收前端的支付请求,并调用上面的服务。

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
@WebServlet("/pay")
public class PayServlet extends HttpServlet {
    private WeChatPayService weChatPayService;
    @Override
    public void init() throws ServletException {
        weChatPayService = new WeChatPayService();
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 获取前端传来的参数
        String openid = request.getParameter("openid"); // **重要**:在实际应用中,openid需要通过微信OAuth2.0授权获取
        String outTradeNo = "ORDER_" + System.currentTimeMillis(); // 生成商户订单号
        Integer totalFee = 1; // 订单金额,单位:分,这里是1分钱
        try {
            // 2. 调用服务创建预支付订单
            Map<String, String> payParams = weChatPayService.createPrepayId(openid, outTradeNo, totalFee);
            // 3. 将参数以JSON格式返回给前端
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(JSON.toJSONString(payParams));
        } catch (Exception e) {
            e.printStackTrace();
            // 发生错误时返回错误信息
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.getWriter().write("{\"code\": 500, \"message\": \"支付请求失败: " + e.getMessage() + "\"}");
        }
    }
}

第四步:前端 JSP 页面实现

现在我们创建一个 JSP 页面,它有两个作用:

  1. 显示一个“支付”按钮。
  2. 当用户点击按钮时,向后端 Servlet 发送请求获取支付参数,并尝试调起微信支付。

pay.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>微信支付 JSP Demo</title>
    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
    <script>
        // **注意**:这个demo只能在微信内置浏览器中运行!
        // 因为wx.chooseWXPay是微信JS-SDK的方法,只在微信环境中存在。
        function onBridgeReady() {
            // 1. 发起AJAX请求到后端Servlet,获取支付参数
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "${pageContext.request.contextPath}/pay", true);
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            // **重要**:这里的openid是硬编码的,实际开发中需要通过微信OAuth2.0流程获取
            xhr.send("openid=oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"); 
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    var response = JSON.parse(xhr.responseText);
                    console.log("从后端获取的支付参数:", response);
                    // 2. 调用微信JS-SDK的支付接口
                    wx.chooseWXPay({
                        appId: response.appId,     // 公众号ID,由商户传入
                        timeStamp: response.timeStamp, // 时间戳,自1970年以来的秒数
                        nonceStr: response.nonceStr,   // 随机串
                        package: response.package,     // 统一下单接口返回的 prepay_id 参数值
                        signType: response.signType,   // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
                        paySign: response.paySign,     // 支付签名
                        success: function (res) {
                            // 支付成功后的回调函数
                            alert("支付成功!");
                            console.log("支付成功回调:", res);
                        },
                        cancel: function (res) {
                            // 用户取消支付
                            alert("您已取消支付。");
                            console.log("支付取消回调:", res);
                        },
                        fail: function (res) {
                            // 支付失败
                            alert("支付失败,请重试。");
                            console.log("支付失败回调:", res);
                        }
                    });
                } else if (xhr.readyState === 4 && xhr.status !== 200) {
                    alert("服务器错误,无法获取支付参数。");
                    console.error("获取支付参数失败:", xhr.statusText);
                }
            };
        }
        // 3. 判断微信JS-SDK是否加载完成
        if (typeof wx === 'undefined') {
            alert("请在微信内打开此页面!");
        } else {
            // 由于JS-SDK加载是异步的,我们直接调用,内部会处理
            // 更严谨的做法是监听 `WeixinJSBridgeReady` 事件
            document.addEventListener('WeixinJSBridgeReady', function() {
                onBridgeReady();
            }, false);
            // 兼容旧版本
            if (typeof WeixinJSBridge !== "undefined") {
                onBridgeReady();
            }
        }
    </script>
</head>
<body>
    <h1>微信支付 JSP Demo</h1>
    <p>请确保在微信内置浏览器中打开此页面。</p>
    <p>页面加载后会自动尝试调起支付。</p>
</body>
</html>

第五步:支付结果通知(异步回调)

当用户支付成功或失败后,微信服务器会主动向你配置的 NOTIFY_URL 发送一个 POST 请求,你的服务器需要处理这个请求,并返回特定的应答。

notify.jsp

<%@ page import="com.wechatpay.v3.util.AesUtil" %>
<%@ page import="com.alibaba.fastjson.JSON" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.security.PrivateKey" %>
<%@ page import="java.nio.file.Files" %>
<%@ page import="java.nio.file.Paths" %>
<%@ page import="com.wechatpay.v3.util.PemUtil" %>
<%@ page import="com.wechatpay.v3.auth.CertificatesVerifier" %>
<%@ page import="com.wechatpay.v3.auth.WechatPay2Credentials" %>
<%@ page import="com.wechatpay.v3.auth.PrivateKeySigner" %>
<%@ page import="com.wechatpay.v3.core.RSASSASigner" %>
<%@ page import="com.wechatpay.v3.core.Config" %>
<%@ page import="com.wechatpay.v3.notification.Notification" %>
<%@ page import="com.wechatpay.v3.notification.NotificationParser" %>
<%@ page import="com.wechatpay.v3.notification.EventHandler" %>
<%@ page contentType="text/xml;charset=UTF-8" language="java" %>
<%
    // 1. 读取微信发送的XML报文
    BufferedReader reader = request.getReader();
    StringBuilder sb = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        sb.append(line);
    }
    String body = sb.toString();
    System.out.println("收到微信支付通知: " + body);
    try {
        // 2. 初始化SDK和验证器
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(
                Files.readAllBytes(Paths.get(WeChatConfig.API_KEY_PATH))));
        Config config = new Config.Builder()
                .merchantId(WeChatConfig.MCH_ID)
                .privateKey(merchantPrivateKey)
                .wxpayCertificate(new CertificatesVerifier(new WechatPay2Credentials(WeChatConfig.MCH_ID, new PrivateKeySigner(WeChatConfig.MCH_ID, merchantPrivateKey))))
                .build();
        RSASSASigner signer = new RSASSASigner(config);
        // 3. 解析通知
        NotificationParser parser = new NotificationParser(signer);
        Notification notification = parser.parse(body, WeChatConfig.API_V3_KEY.getBytes(StandardCharsets.UTF_8));
        // 4. 处理通知事件
        if ("TRANSACTION.SUCCESS".equals(notification.getEventType())) {
            // 业务逻辑:更新订单状态为“已支付”
            JSONObject resourceData = JSON.parseObject(new String(notification.getResource().getAssociatedData(), StandardCharsets.UTF_8));
            String outTradeNo = resourceData.getString("out_trade_no");
            String transactionId = resourceData.getString("transaction_id");
            System.out.println("订单支付成功!");
            System.out.println("商户订单号: " + outTradeNo);
            System.out.println("微信支付订单号: " + transactionId);
            // TODO: 在这里更新你的数据库订单状态
            // orderService.paySuccess(outTradeNo, transactionId);
            // 5. 返回成功应答给微信服务器
            out.println("<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");
        } else {
            // 其他事件类型,如TRANSACTION.FAILED
            System.out.println("收到非支付成功通知: " + notification.getEventType());
            out.println("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[Unknown Event]]></return_msg></xml>");
        }
    } catch (Exception e) {
        e.printStackTrace();
        // 验证失败或处理异常,返回失败应答
        out.println("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[FAIL]]></return_msg></xml>");
    }
%>

总结与部署

  1. 这个 JSP Demo 展示了一个完整的后端生成支付参数、前端调起支付的流程,但请务必记住,这只是一个模拟和教学示例,真正的生产环境应使用微信官方推荐的 H5 支付流程,即后端生成一个支付 URL,前端通过 window.location.hrefWeixinJSBridge.invoke 的方式跳转。
  2. 部署
    • 将项目打包成 WAR 文件。
    • 部署到 Tomcat 或其他 Java Web 服务器上。
    • 确保服务器的 IP 和域名已经在微信商户平台的白名单中。
    • 用微信扫描二维码,在手机微信中访问 http://你的服务器IP:端口/pay.jsp,测试支付流程。

这个 Demo 覆盖了从后端到前端的核心逻辑,并附带了必要的注释和注意事项,希望能帮助你理解微信网页支付的实现原理。