最近在做Java整和微信JSAPI支付,遇到一些问题,现在把相关的注意点记录下,供大家参考,如有不对,还请指正。
登录微信公众号 > 开发 > 基本配置 > 公众号开发信息
记得把服务器加入IP白名单
登录微信公众号 > 设置 > 公众号设置 > 功能设置
3. 商户号 mch_id:
登录微信公众号 > 微信支付 > 商户号管理 > 已关联商户号
4. 微信支付API密钥(appKey):
登录微信支付 > 账户中心 > API安全
如果没有API证书,则需要申请证书再设置API密钥
5. 支付授权目录配置:
登录微信支付 > 产品中心 > 开发配置
这里必须配置,不然支付的时候会弹出xxx页面未注册,我们配置JSAPI支付授权目录,新版可以支付域名根目录配置,例如你的支目录为 http://xxx.com/wx/pay,这里可... ://xxx.com/ 注意最后的一个斜线,代表根目录,不能省略。
至此,配置工作全部完成。
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'] })
https请求方式: GET https://api.weixin.qq.com/cgi...
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("×tamp=") .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