首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 服務端實現微信 APP 支付完整流程與安全簽名詳解

     編輯:Java 相關知識

微信 APP 支付是移動應用商業化的核心環節。本文完整梳理從商戶進件、統一下單、簽名算法、回調處理到安全驗證的全流程實作,並針對關鍵安全點提供最佳實踐建議。

前置準備與商戶配置

  1. 在 微信商戶平台 完成商戶進件,獲取 appid、mch_id、api_key(商戶金鑰)
  2. 配置支付授權目錄與回調域名(需公開可訪問、HTTPS)
  3. 應用配置文件:
wechat:
  pay:
    appid: wx****************  # 開放平台應用 AppID
    mchId: 1**********         # 商戶號
    apiKey: ****************   # 32位商戶金鑰 (商戶平台-賬戶中心-API安全設置)
    notifyUrl: https://api.example.com/pay/wechat/notify  # 異步回調地址
    tradeType: APP

核心流程時序

App客戶端 → 你的後端 → 微信支付統一下單API → 返回prepay_id → 後端二次簽名 → App喚起微信支付 → 用戶支付 → 微信異步回調你的後端 → 後端驗證簽名/金額 → 業務處理 → 返回SUCCESS

統一下單與簽名實作

1. 統一下單請求構建

@PostMapping("/unifiedorder")
public ResponseEntity<Map<String, Object>> unifiedOrder(@RequestBody OrderRequest req) {
    // 1. 參數組裝
    Map<String, String> params = new TreeMap<>(); // TreeMap 自動按 ASCII 排序
    params.put("appid", config.getAppid());
    params.put("mch_id", config.getMchId());
    params.put("nonce_str", RandomUtil.randomString(32));
    params.put("body", req.getBody());
    params.put("detail", req.getDetail());
    params.put("attach", req.getAttach());
    params.put("out_trade_no", req.getOutTradeNo());  // 唯一訂單號
    params.put("total_fee", String.valueOf(req.getTotalFee())); // 單位:分
    params.put("spbill_create_ip", req.getClientIp());
    params.put("time_start", DateUtil.format(new Date(), "yyyyMMddHHmmss"));
    params.put("time_expire", DateUtil.format(DateUtil.offsetMinute(new Date(), 120), "yyyyMMddHHmmss"));
    params.put("notify_url", config.getNotifyUrl());
    params.put("trade_type", "APP");
    
    // 2. 生成簽名
    String sign = WXPayUtil.generateSignature(params, config.getApiKey());
    params.put("sign", sign);
    
    // 3. 發送 XML 請求
    String xml = WXPayUtil.mapToXml(params);
    String responseXml = HttpUtil.post(config.getUnifiedOrderUrl(), xml);
    
    // 4. 解析回應
    Map<String, String> result = WXPayUtil.xmlToMap(responseXml);
    if ("SUCCESS".equals(result.get("return_code")) && "SUCCESS".equals(result.get("result_code"))) {
        // 5. 二次簽名給 App
        Map<String, String> appParams = new TreeMap<>();
        appParams.put("appid", result.get("appid"));
        appParams.put("partnerid", result.get("mch_id"));
        appParams.put("prepayid", result.get("prepay_id"));
        appParams.put("package", "Sign=WXPay");
        appParams.put("noncestr", RandomUtil.randomString(32));
        appParams.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
        String paySign = WXPayUtil.generateSignature(appParams, config.getApiKey());
        appParams.put("sign", paySign);
        return ResponseEntity.ok(appParams);
    }
    throw new BusinessException("下單失敗: " + result.get("return_msg"));
}

2. 簽名算法核心實現 (MD5 + ASCII 排序)

public class WXPayUtil {
    
    /** 生成簽名
     * 1. 參數按 key ASCII 碼排序
     * 2. 拼接 key1=value1&key2=value2...&key=API_KEY
     * 3. MD5 加密並轉大寫
     */
    public static String generateSignature(Map<String, String> params, String apiKey) {
        StringBuilder sb = new StringBuilder();
        params.entrySet().stream()
            .filter(e -> e.getValue() != null && !e.getValue().isEmpty() && !"sign".equals(e.getKey()))
            .forEach(e -> sb.append(e.getKey()).append('=').append(e.getValue()).append('&'));
        sb.append("key=").append(apiKey);
        return DigestUtils.md5DigestAsHex(sb.toString().getBytes(StandardCharsets.UTF_8)).toUpperCase();
    }
    
    /** XML <-> Map 雙向轉換 (CDATA 處理) */
    public static String mapToXml(Map<String, String> map) { ... }
    public static Map<String, String> xmlToMap(String xml) { ... }
}

支付回調處理與安全驗證

@PostMapping("/notify")
public String notify(HttpServletRequest request) {
    try {
        // 1. 讀取原始 XML
        String xml = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8);
        Map<String, String> params = WXPayUtil.xmlToMap(xml);
        
        // 2. 通訊層驗證
        if (!"SUCCESS".equals(params.get("return_code"))) {
            log.warn("微信通訊失敗: {}", params.get("return_msg"));
            return WXPayUtil.mapToXml(Map.of("return_code", "SUCCESS", "return_msg", "OK"));
        }
        
        // 3. 業務層驗證
        if (!"SUCCESS".equals(params.get("result_code"))) {
            log.info("支付失敗: {}", params.get("err_code_des"));
            return WXPayUtil.mapToXml(Map.of("return_code", "SUCCESS", "return_msg", "OK"));
        }
        
        // 4. 簽名驗證 (防偽造回調)
        String receivedSign = params.remove("sign");
        String calculatedSign = WXPayUtil.generateSignature(params, config.getApiKey());
        if (!receivedSign.equals(calculatedSign)) {
            log.error("簽名驗證失敗! 收到: {}, 計算: {}", receivedSign, calculatedSign);
            throw new SecurityException("簽名驗證失敗");
        }
        
        // 5. 金額核對 (防篡改金額)
        String outTradeNo = params.get("out_trade_no");
        int totalFee = Integer.parseInt(params.get("total_fee"));
        Order order = orderService.getByOutTradeNo(outTradeNo);
        if (order == null || order.getTotalFee() != totalFee) {
            log.error("金額不匹配! 訂單: {}, 回調金額: {}", order, totalFee);
            throw new BusinessException("金額核對失敗");
        }
        
        // 6. 冪等性處理 (防重複回調)
        if (order.getStatus() == OrderStatus.PAID) {
            log.info("訂單已處理,忽略重複回調: {}", outTradeNo);
            return WXPayUtil.mapToXml(Map.of("return_code", "SUCCESS", "return_msg", "OK"));
        }
        
        // 7. 業務邏輯: 更新訂單、發送通知、發放權益等
        orderService.markPaid(outTradeNo, params.get("transaction_id"), new Date());
        
        return WXPayUtil.mapToXml(Map.of("return_code", "SUCCESS", "return_msg", "OK"));
        
    } catch (Exception e) {
        log.error("回調處理異常", e);
        return WXPayUtil.mapToXml(Map.of("return_code", "FAIL", "return_msg", "處理異常"));
    }
}

關鍵安全最佳實踐

風險點防護措施
API 金鑰洩露配置中心加密存儲、定期輪換、不提交代碼倉庫
回調偽造必須驗證簽名、驗證金額、驗證訂單狀態
重放攻擊回調處理冪等設計、資料庫唯一索引約束
金額篡改回調時核對 total_fee 與訂單金額一致
訂單號預測使用 UUID/雪花算法生成不可預測 out_trade_no

常見錯誤碼對照

  • SYSTEMERROR:系統錯誤,重試
  • OUT_TRADE_NO_USED:商戶訂單號重複,換單號重試
  • NOTENOUGH:餘額不足
  • ORDERPAID:訂單已支付,勿重複下單
  • SIGNERROR:簽名錯誤,檢查參數排序、API Key、編碼

總結

微信 APP 支付核心在於「雙向簽名驗證」與「金額核對」。服務端需嚴格實現:統一下單→二次簽名→回調驗簽→金額核對→冪等處理→業務邏輯。建議封裝通用 WXPayClient 組件,降低業務代碼耦合,提升可維護性與安全性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved