Activación del Servicio de Puntuación de WeChat Pay desde H5 con JS-SDK

La integración del servicio de Puntuación de WeChat Pay en aplicaciones H5 permite a los usuarios activar y utilizar funcionalidades de pago avanzadas sin fricciones. Para lograr esto, es fundamental configurar correctamente el JS-SDK de WeChat y gestionar las firmas criptográficas necesarias. Este artículo detalla el proceso para inicializar el JS-SDK y luego invocar la vista de activación del servicio de Puntuación de WeChat Pay.

Configuración del JS-SDK de WeChat para H5 Antes de interactuar con cualquier API de WeChat desde una página H5, es imprescindible autenticar la página a través del método wx.config. Este paso requiere una firma generada en el lado del servidor para verificar la identidad de la aplicación. Los componentes clave para esta firma son el access_token y el jsapi_ticket.

Obtención de Credenciales: access_token y jsapi_ticket El access_token es una credencial de acceso global para la cuenta pública de WeChat, con una validez de 7200 segundos. Se utiliza para obtener el jsapi_ticket. Debido a las limitaciones de llamadas a la API y la vida útil del ticket, es crucial almacenar en caché estas credenciales globalmente en su servicio backend.

  1. Obtener access_token: ``` GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
2. **Obtener `jsapi_ticket`:** ```
GET https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi

Una respuesta exitosa contendrá un JSON similar a:

{
    "errcode": 0,
    "errmsg": "ok",
    "ticket": "bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
    "expires_in": 7200
}

Generación de la Firma para wx.config (SHA1) La firma para wx.config se genera mediente un algoritmo SHA1 utilizando los siguientes parámetros, ordenados alfabéticamente:

  • jsapi_ticket
  • noncestr (cadena aleatoria)
  • timestamp (marca de tiempo en segundos)
  • url (la URL actual de la página H5, sin el hash #) La cadena a firmar se forma como jsapi_ticket=TICKET&noncestr=NONCESTR&timestamp=TIMESTAMP&url=URL. El hash SHA1 de esta cadena es la signature requerida por wx.config.

Activación de la Puntuación de WeChat Pay con openBusinessViewUna vez que el JS-SDK está configurado, la activación del servicio de Puntuación de WeChat Pay se realiza a través de la API wx.invoke('openBusinessView'). Esta API requiere un businessType específico y un queryString que contiene todos los parámetros necesarios para la operación, incluida una segunda firma (HMAC-SHA256).

Parámetros para openBusinessView

  • businessType: Para la activación de la Puntuación de WeChat Pay, el valor es wxpayScoreEnable.
  • queryString: Una cadena de consulta que incluye los detalles del comerciante, el servicio y una firma (HMAC-SHA256).

Generación de la Firma para openBusinessView (HMAC-SHA256) Los parámetros dentro del queryString también deben ser firmados, pero esta vez usando el algoritmo HMAC-SHA256. Los parámetros típicos incluyen:

  • mch_id: ID de comerciante de WeChat Pay.
  • service_id: ID del servicio de Puntuación de WeChat Pay.
  • out_request_no: Número de solicitud externo único.
  • timestamp: Marca de tiempo en segundos.
  • nonce_str: Cadena aleatoria.
  • sign_type: Tipo de firma, que debe ser HMAC-SHA256. Estos parámetros, junto con la clave de API del comerciante, se utilizan para generar la sign del queryString siguiendo las reglas de firma de WeChat Pay (orden alfabético de parámetros, concatenación, y hashing con HMAC-SHA256).

Implementación Práctica

Ejemplo Frontend (HTML/JavaScript) El siguiente código JavaScript, utilizando jQuery, ilustra cómo inicializar el JS-SDK y luego activar la Puntuación de WeChat Pay. Asume que hay dos endpoints de backend disponibles para obtener la configuración de JS-SDK y la firma de Pay Score.


<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Activación de Puntuación WeChat Pay</title>
    <!-- Incluir la librería JS-SDK de WeChat -->
    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
    <!-- Incluir jQuery -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <!-- Opcional: vConsole para depuración en móviles -->
    <!-- <script src="https://cdn.jsdelivr.net/npm/vconsole@latest/dist/vconsole.min.js"></script> -->
</head>
<body>
    <div>
        <button id="btnActivatePayScore">Activar Puntuación WeChat Pay</button>
    </div>

    <script>
        // Opcional: Inicializar vConsole
        // if (typeof VConsole !== 'undefined') {
        //     new VConsole();
        // }

        let jsSdkConfigData = {}; // Objeto para almacenar los parámetros de wx.config

        /**
         * Inicializa el JS-SDK de WeChat obteniendo la configuración del backend.
         */
        function configureWechatJsSdk() {
            const currentPageUrl = window.location.href.split('#')[0];
            console.log("URL de la página para la firma JS-SDK:", currentPageUrl);

            $.ajax({
                url: "/api/wechat/jssdk-config", // Endpoint para obtener la configuración de wx.config
                method: "GET",
                data: { pageUrl: currentPageUrl },
                success: function (response) {
                    console.log("Configuración JS-SDK recibida:", response);
                    jsSdkConfigData = response; // Almacenar para uso posterior

                    wx.config({
                        debug: true, // Habilitar modo de depuración
                        appId: response.appId,
                        timestamp: response.timestamp,
                        nonceStr: response.nonceStr,
                        signature: response.signature,
                        jsApiList: ['openBusinessView'] // API específica para Puntuación WeChat Pay
                    });

                    wx.ready(function() {
                        console.log("WeChat JS-SDK listo para usar.");
                        // Verificar disponibilidad de la API (buena práctica)
                        wx.checkJsApi({
                            jsApiList: ['openBusinessView'],
                            success: function(checkRes) {
                                if (!checkRes.checkResult.openBusinessView) {
                                    console.error("La API openBusinessView no está disponible en este entorno.");
                                    alert("El servicio de Puntuación WeChat Pay no está disponible.");
                                }
                            }
                        });
                    });

                    wx.error(function(errorRes) {
                        console.error("Error al inicializar WeChat JS-SDK:", errorRes.errMsg);
                        alert("Error de JS-SDK: " + errorRes.errMsg);
                    });
                },
                error: function(jqXHR, textStatus, errorThrown) {
                    console.error("Error al obtener la configuración de JS-SDK:", textStatus, errorThrown);
                    alert("No se pudo cargar la configuración de WeChat JS-SDK.");
                }
            });
        }

        /**
         * Invoca la vista de activación del servicio de Puntuación de WeChat Pay.
         */
        function requestPayScoreActivation() {
            if (!jsSdkConfigData.timestamp || !jsSdkConfigData.nonceStr) {
                alert("La configuración de JS-SDK no está lista. Por favor, espere.");
                return;
            }

            // Parámetros fijos para la demostración, en producción deberían ser dinámicos
            const merchantId = "1518750531";
            const serviceId = "00004000000000704283351234894845";
            const uniqueRequestNumber = "ORDER" + Date.now(); // Generar un número de solicitud único

            // Usar el mismo timestamp y nonceStr de wx.config para consistencia, o generar nuevos
            const activationTimestamp = jsSdkConfigData.timestamp;
            const activationNonceStr = jsSdkConfigData.nonceStr;

            const payScoreApiParams = {
                mch_id: merchantId,
                service_id: serviceId,
                out_request_no: uniqueRequestNumber,
                timestamp: activationTimestamp,
                nonce_str: activationNonceStr
            };

            $.ajax({
                url: "/api/wechat/payscore-signature", // Endpoint para obtener la firma HMAC-SHA256
                method: "POST",
                data: payScoreApiParams,
                success: function (signatureResponse) {
                    const payScoreSignature = signatureResponse.signature;
                    console.log("Firma de Puntuación WeChat Pay recibida:", payScoreSignature);

                    wx.invoke(
                        'openBusinessView',
                        {
                            businessType: 'wxpayScoreEnable',
                            queryString: `mch_id=${merchantId}&service_id=${serviceId}&out_request_no=${uniqueRequestNumber}&timestamp=${activationTimestamp}&nonce_str=${activationNonceStr}&sign_type=HMAC-SHA256&sign=${payScoreSignature}`
                        },
                        function (res) {
                            if (parseInt(res.err_code) === 0) {
                                console.log("Activación de Puntuación WeChat Pay exitosa:", res);
                                alert("Activación de Puntuación WeChat Pay exitosa.");
                            } else {
                                console.error("Fallo en la activación de Puntuación WeChat Pay:", res);
                                alert("Fallo en la activación de Puntuación WeChat Pay: " + res.err_msg);
                            }
                        }
                    );
                },
                error: function(jqXHR, textStatus, errorThrown) {
                    console.error("Error al obtener la firma de Puntuación WeChat Pay:", textStatus, errorThrown);
                    alert("No se pudo generar la firma para Puntuación WeChat Pay.");
                }
            });
        }

        // Ejecutar al cargar la página
        $(document).ready(function() {
            configureWechatJsSdk(); // Primero configurar JS-SDK
            $('#btnActivatePayScore').on('click', requestPayScoreActivation); // Luego vincular el botón
        });
    </script>
</body>
</html>

Ejemplo Backend (Java/Spring Boot) El backend se encargará de proporcionar los parámetros para wx.config y generar la firma HMAC-SHA256 para openBusinessView. Se utiliza una clase de utilidad para la generación de firmas.

// src/main/java/com/example/wechat/util/SignatureUtil.java
package com.example.wechat.util;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * Utilidad para la generación de firmas de WeChat.
 */
public class SignatureUtil {

    private static final String HMACSHA256_ALGORITHM = "HmacSHA256";
    private static final String MD5_ALGORITHM = "MD5";
    private static final String FIELD_SIGN = "sign";

    public enum SignMethod {
        MD5, HMAC_SHA256, SHA1
    }

    /**
     * Genera una firma basada en los datos proporcionados y una clave secreta.
     * Los parámetros se ordenan alfabéticamente, se concatenan, y se aplica el método de hash especificado.
     * @param data Mapa de parámetros a firmar.
     * @param secretKey Clave secreta para la firma.
     * @param method Método de firma (MD5, HMAC_SHA256).
     * @return La firma generada en mayúsculas.
     * @throws Exception Si ocurre un error en la generación de la firma.
     */
    public static String generateSignature(Map<String, String> data, String secretKey, SignMethod method) throws Exception {
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[0]);
        Arrays.sort(keyArray);

        StringBuilder stringBuilder = new StringBuilder();
        for (String key : keyArray) {
            // Excluir el campo "sign" y parámetros vacíos
            if (FIELD_SIGN.equals(key) || data.get(key) == null || data.get(key).trim().isEmpty()) {
                continue;
            }
            stringBuilder.append(key).append("=").append(data.get(key).trim()).append("&");
        }
        stringBuilder.append("key=").append(secretKey); // Añadir la clave al final

        String stringToHash = stringBuilder.toString();

        if (SignMethod.MD5.equals(method)) {
            return calculateMD5(stringToHash).toUpperCase();
        } else if (SignMethod.HMAC_SHA256.equals(method)) {
            return calculateHmacSha256(stringToHash, secretKey).toUpperCase();
        } else {
            throw new IllegalArgumentException("Método de firma no válido: " + method);
        }
    }

    /**
     * Calcula el hash MD5 de una cadena.
     */
    private static String calculateMD5(String data) throws Exception {
        MessageDigest md = MessageDigest.getInstance(MD5_ALGORITHM);
        byte[] array = md.digest(data.getBytes(StandardCharsets.UTF_8));
        return toHexString(array);
    }

    /**
     * Calcula el hash HMAC-SHA256 de una cadena con una clave.
     */
    private static String calculateHmacSha256(String data, String key) throws Exception {
        Mac hmacSha256 = Mac.getInstance(HMACSHA256_ALGORITHM);
        SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), HMACSHA256_ALGORITHM);
        hmacSha256.init(secretKeySpec);
        byte[] array = hmacSha256.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return toHexString(array);
    }

    /**
     * Calcula el hash SHA1 de una cadena.
     * Este es el método usado específicamente para la firma de wx.config.
     */
    public static String calculateSha1(String data) throws Exception {
        MessageDigest crypt = MessageDigest.getInstance("SHA-1");
        crypt.reset();
        crypt.update(data.getBytes(StandardCharsets.UTF_8));
        return toHexString(crypt.digest());
    }

    /**
     * Convierte un array de bytes a una cadena hexadecimal.
     */
    private static String toHexString(byte[] bytes) {
        StringBuilder hexString = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        return hexString.toString();
    }

    /**
     * Genera una cadena aleatoria (nonce_str).
     */
    public static String generateNonceStr() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    /**
     * Genera una marca de tiempo en segundos.
     */
    public static long generateTimestampSeconds() {
        return System.currentTimeMillis() / 1000;
    }
}

// src/main/java/com/example/wechat/controller/WeChatPayController.java
package com.example.wechat.controller;

import com.example.wechat.util.SignatureUtil;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * Controlador REST para interactuar con las APIs de WeChat Pay y JS-SDK.
 */
@RestController
@RequestMapping("/api/wechat")
public class WeChatPayController {

    // Configuración de la aplicación WeChat. En un entorno real, estos serían inyectados
    // a través de configuración (ej. application.properties o Vault).
    private final String APP_ID = "wx954f1899ef15d2ea"; // ID de la aplicación WeChat
    // La clave API para WeChat Pay. Es diferente del App Secret de la cuenta pública.
    private final String MCH_API_KEY = "acb8ad2a46765766c7881ea37237c1c7";

    // jsapi_ticket debe ser obtenido dinámicamente de la API de WeChat y CACHEADO.
    // Este es solo un valor de ejemplo.
    private String cachedJsapiTicket = "HoagFKDcsGMVCIY2vOjf9kjcHCRqLoF8k77O2dyyZiMTaOwxLTQ9nHU3fa4jfiEj2XW4NsvQG53gZZVmkQdJA";

    /**
     * Proporciona los parámetros de configuración necesarios para el método wx.config del JS-SDK de WeChat.
     * Genera la firma SHA1 requerida para la autenticación de la página H5.
     *
     * @param pageUrl La URL completa de la página H5 actual (sin el hash #).
     * @return Un mapa que contiene appId, timestamp, nonceStr y signature para wx.config.
     * @throws Exception Si hay un error al generar la firma.
     */
    @GetMapping("/jssdk-config")
    public Map<String, String> getJsSdkConfiguration(@RequestParam String pageUrl) throws Exception {
        long timestamp = SignatureUtil.generateTimestampSeconds();
        String nonceStr = SignatureUtil.generateNonceStr();

        // Construir la cadena para firmar según las reglas de JS-SDK (parámetros ordenados alfabéticamente)
        String stringToSign = String.format("jsapi_ticket=%s&noncestr=%s&timestamp=%s&url=%s",
                cachedJsapiTicket, nonceStr, timestamp, pageUrl);

        // La firma para wx.config es SHA1
        String signature = SignatureUtil.calculateSha1(stringToSign);

        Map<String, String> configData = new HashMap<>();
        configData.put("appId", APP_ID);
        configData.put("timestamp", String.valueOf(timestamp));
        configData.put("nonceStr", nonceStr);
        configData.put("signature", signature);
        return configData;
    }

    /**
     * Genera la firma HMAC-SHA256 para la llamada a la API openBusinessView
     * (específicamente para la activación de Puntuación de WeChat Pay).
     *
     * @param mch_id ID del comerciante.
     * @param service_id ID del servicio de Puntuación de WeChat Pay.
     * @param out_request_no Número de solicitud externo único.
     * @param timestamp Marca de tiempo en segundos.
     * @param nonce_str Cadena aleatoria.
     * @return Un mapa que contiene la firma generada.
     * @throws Exception Si hay un error al generar la firma.
     */
    @PostMapping("/payscore-signature")
    public Map<String, String> generatePayScoreSignature(
            @RequestParam String mch_id,
            @RequestParam String service_id,
            @RequestParam String out_request_no,
            @RequestParam String timestamp,
            @RequestParam String nonce_str) throws Exception {

        Map<String, String> data = new HashMap<>();
        data.put("mch_id", mch_id);
        data.put("service_id", service_id);
        data.put("out_request_no", out_request_no);
        data.put("timestamp", timestamp);
        data.put("nonce_str", nonce_str);
        data.put("sign_type", "HMAC-SHA256"); // Tipo de firma fijo para esta API

        // La firma para openBusinessView es HMAC-SHA256
        String signature = SignatureUtil.generateSignature(data, MCH_API_KEY, SignatureUtil.SignMethod.HMAC_SHA256);

        Map<String, String> response = new HashMap<>();
        response.put("signature", signature);
        return response;
    }
}

Etiquetas: WeChatPay H5 JS-SDK PuntuacionWeChatPay IntegracionPagos

Publicado el 7-30 16:26