乐趣区

Vue-Java-微信JS-API-支付

一. 背景

最近在做 Java 整和微信 JSAPI 支付,遇到一些问题,现在把相关的注意点记录下,供大家参考,如有不对,还请指正。

二. 开发前准备工作

  1. 采用 vue + springboot + weixin-js-sdk(1.6.0)
  2. 微信公众号(appId,appSecret)
  3. 微信公众号开通微信支付, 具有商户号(mch_id)
  4. 微信支付 API 密钥(appKey).

三. 相关信息配置获取方式:

1. appKey,appSecret:

登录微信公众号 > 开发 > 基本配置 > 公众号开发信息

记得把服务器加入 IP 白名单

2. 配置 JS 接口安全域名, 网页授权域名:

登录微信公众号 > 设置 > 公众号设置 > 功能设置

3. 商户号 mch_id:
登录微信公众号 > 微信支付 > 商户号管理 > 已关联商户号

4. 微信支付 API 密钥 (appKey):
登录微信支付 > 账户中心 > API 安全
如果没有 API 证书, 则需要申请证书再设置 API 密钥

5. 支付授权目录配置:
登录微信支付 > 产品中心 > 开发配置
这里必须配置, 不然支付的时候会弹出 xxx 页面未注册, 我们配置 JSAPI 支付授权目录, 新版可以支付域名根目录配置, 例如你的支目录为 http://xxx.com/wx/pay, 这里可 …://xxx.com/ 注意最后的一个斜线, 代表根目录, 不能省略。

至此, 配置工作全部完成。

四. 编码阶段

1. 引入 weixin-js-sdk
html 方式: (支持 https):http://res.wx.qq.com/open/js/…
npm 方式: npm install weixin-js-sdk
使用 import wx from 'weixin-js-sdk'
建议使用微信支付 sdk https://pay.weixin.qq.com/wik…
2. 通过 config 接口注入权限验证配置
所有需要使用 JS-SDK 的页面必须先注入配置信息,否则将无法调用(同一个 url 仅需调用一次,对于变化 url 的 SPA 的 web app 可在每次 url 变化时进行调用, 目前 Android 微信客户端不支持 pushState 的 H5 新特性,所以使用 pushState 来实现 web app 的页面会导致签名失败,此问题会在 Android6.2 中修复)

wx.config({
debug: true, // 开启调试模式, 调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。appId: '', // 必填,公众号的唯一标识 公众号已经获取到
timestamp: '', // 必填,生成签名的时间戳 WXPayUtil.getCurrentTimestamp()
nonceStr: '', // 必填,生成签名的随机串 WXPayUtil.generateNonceStr()
signature: '',// 必填,签名 需要获取 jsapi_ticket,access_token, 详情见官方文档 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62
jsApiList: [] // 必填,需要使用的 JS 接口列表 ['chooseWXPay']
})

第一步. 获取 access_token:

https 请求方式: GET https://api.weixin.qq.com/cgi…

第二步. 获取 jsapi_ticket:

https 请求方式: GET https://api.weixin.qq.com/cgi…
后台代码示例:

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.client.RestTemplate;

/**
 * @author : Stone
 * @date : 2020/6/23
 */
@Slf4j
public class HttpUtil {


    private int times = 5;

    private final RestTemplate restTemplate = new RestTemplate();

    public  String executeGet(final String url) {
        int i = 1;
        String result = "";
        while (this.times > 0) {
            try {log.warn("尝试请求第: {} 次", i);
                result = this.restTemplate.getForObject(url, String.class);
                break;
            } catch (final Exception e) {
                i++;
                this.times--;
                log.error("", e);
            }
        }
        log.warn("请求地址: {}", url);
        log.warn("请求结果: {}", result);
        return result;
    }
}
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;

/**
 * author : Stone
 * time : 2019/11/22 17:39
 */
@Slf4j
public final class JsApiSignUtil {
    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;


    private JsApiSignUtil() {}


    /**
     * @param ticket
     * @param url
     * @return
     */
    public static Map<String, String> signature(final String ticket, final String url) {
        // 注意这里参数名必须全部小写,且必须有序
        final Map<String, String> map = new HashMap<>(3);

        String signature = "";

        final String nonceStr = create_nonce_str();

        final String timestamp = create_timestamp();

        final StringBuilder sb = new StringBuilder();
        sb
                .append("jsapi_ticket=")
                .append(ticket)
                .append("&noncestr=")
                .append(nonceStr)
                .append("&timestamp=")
                .append(timestamp)
                .append("&url=")
                .append(url);
        try {final MessageDigest crypt = MessageDigest.getInstance("SHA-1");
            crypt.reset();
            crypt.update(sb.toString().getBytes(DEFAULT_CHARSET));
            signature = byteToHex(crypt.digest());
        } catch (final NoSuchAlgorithmException e) {e.printStackTrace();
        }
        map.put("nonceStr", nonceStr);
        map.put("timestamp", timestamp);
        map.put("signature", signature);
        return map;
    }

    private static String byteToHex(final byte[] hash) {final Formatter formatter = new Formatter();
        for (final byte b : hash) {formatter.format("%02x", b);
        }
        final String result = formatter.toString();
        formatter.close();
        return result;
    }

    private static String create_nonce_str() {return WXPayUtil.generateNonceStr();
    }

    private static String create_timestamp() {return Long.toString(WXPayUtil.getCurrentTimestamp());
    }
}
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.stone.springboot.wx.util.JsApiSignUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.util.Map;

/**
 * @author : Stone
 * @date : 2020/6/23
 */

@Slf4j
public class TestTicket {private final HttpUtil httpUtil = new HttpUtil();

    /**
     * appId 此为测试账号
     */
    public static final String APP_ID = "";

    /**
     * appSecret 此为测试账号
     */
    public static final String APP_SECRET = "";

    /**
     * 获取 access_token 地址
     */
    private static final String ENDPOINT_ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token";

    /**
     * 获取 jsapi_ticket 地址
     */
    private static final String ENDPOINT_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket";

    /**
     * 拼接 access_token url
     *
     * @param appId
     * @param appSecret
     * @return
     */
    private static String accessTokenUrl(final String appId, final String appSecret) {return new StringBuilder(ENDPOINT_ACCESS_TOKEN)
                .append("?appid=")
                .append(appId)
                .append("&secret=")
                .append(appSecret)
                .append("&grant_type=")
                .append("client_credential").toString();}

    /**
     * 获取 access_token
     *
     * @param appId
     * @param appSecret
     * @return
     */
    public String getAccessToken(final String appId, final String appSecret) {final String url = TestTicket.accessTokenUrl(appId, appSecret);
        return this.httpUtil.executeGet(url);
    }

    /**
     * 拼接 jsapi_ticket url
     *
     * @param accessToken
     * @return
     */
    public static String ticketUrl(final String accessToken) {return new StringBuilder(ENDPOINT_TICKET)
                .append("?access_token=")
                .append(accessToken)
                .append("&type=")
                .append("jsapi").toString();}

    /**
     * 获取 jsapi_ticket
     *
     * @param accessToken
     * @return
     */
    public String getTicket(final String accessToken) {final String url = TestTicket.ticketUrl(accessToken);
        return this.httpUtil.executeGet(url);
    }

    @Test
    public void testAccessToken() {final String accessTokenResult = this.getAccessToken(APP_ID, APP_SECRET);
        final JSONObject tokenResult = JSON.parseObject(accessTokenResult);
        final String accessToken = tokenResult.getString("access_token");
        log.info("获取到的 access_token 为: {}", accessToken);
        final String jsTicketResult = this.getTicket(accessToken);
        final JSONObject ticketResult = JSON.parseObject(jsTicketResult);
        final String ticket = ticketResult.getString("ticket");
        log.info("获取到的 ticket 为: {}", ticket);

        final String url = ""; // 此 Url 为当前页面的 url, 前端请使用 location.href.split('#')[0] 传入后台
        final Map<String, String> map = JsApiSignUtil.signature(ticket, url);
        log.info("map : {}", map);
    }
}

输出结果:

前端代码示例:

wx.config({
debug: true, // 开启调试模式, 调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。appId: '', // 必填,公众号的唯一标识
timestamp: '1592903270', // 必填,生成签名的时间戳
nonceStr: 'xqz094C8EiyTvzfyYErMnAIHCu450Ebw', // 必填,生成签名的随机串
signature: 'b056783cd10a65e92817b403d3c54801f929a5ba',// 必填,签名
jsApiList: ['chooseWXPay'] // 必填,需要使用的 JS 接口列表
})

至此, 通过 config 接口注入权限验证配置全部完成。
3. 商户 server 调用统一下单接口请求订单
这里需要用到到一个重要的参数 openId, 详情见官方文档 https://developers.weixin.qq….
第一步:用户同意授权,获取 code
引导用户打开 https://open.weixin.qq.com/co…

appid: 公众号的唯一标识 公众号已经获取到
redirect_uri: 授权回调的地址
response_type:  返回类型,请填写 code
scope: 应用授权作用域,snsapi_base(不弹出授权页面,直接跳转,只能获取用户 openid),snsapi_userinfo(弹出授权页面,可通过 openid 拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息
state: 重定向后会带上 state 参数,开发者可以填写 a -zA-Z0- 9 的参数值,最多 128 字节
#wechat_redirect: 无论直接打开还是做页面 302 重定向时候,必须带此参数

后台代码示例:

第二步:通过 code 换取网页授权 access_token
获取 code 后,请求以下链接获取 access_token:https://api.weixin.qq.com/sns…

   appid: 公众号的唯一标识 公众号已经获取到
   secret: 公众号 appSecret 公众号已经获取到
   code: 第一步获取到的 code
   grant_type: authorization_code
退出移动版