准备流程
使用“普通商户号”(登陆商户平台后,上方有:首页、交易中心、账户中心、营销中心、产品中心、数据中心),在产品中心中开通JSAPI支付,记录商户号。
- 进入:账户中心 - API安全 - 申请API证书 - 管理证书,按提示添加证书,添加完毕后记录证书序列号
- 进入:账户中心 - API安全 - 设置APIv3密钥,修改,按提示操作,密钥自行生成,为长度上限32位的字符串;记录该密钥。
- 进入:账户中心 - API安全 - 设置API密钥,修改,同上(本步骤可能用不到)
- 进入:产品中心 - AppID账号管理 - 关联AppID,填写需要关联的AppID(小程序、公众号等)。登陆对应的AppID平台,通过关联申请;小程序平台的操作路径为:功能 - 微信支付
除此以外还需要准备
- 小程序的appId
- 小程序的appSecret
提前阅读:
- 证书、密钥的相关规则
- 签名的 生成 和 验证
- 回调报文解密
- 也可以直接使用官方SDK
- JAVA(有示例代码,建议使用”定时更新平台证书功能“管理平台证书)。只要将商户号、证书序列号,密钥提供给SDK,即可自动完成签名、验签操作。
正式流程
登陆
- 前端:调用该接口获取授权:
- 前端:成功后调用该接口: 向后端提供的微信登陆接口传递参数:code,也可以同时额外提供前一接口获取到的其他信息
- 后端:在微信登陆接收到 code 后,调用该接口,使用小程序的 appId 和 appSecret + 获取到的 code 请求获得用户信息(openid unionid session_key)并保存,获取成功后检查openid是否已进行过注册,如果没有进行注册,再执行免密登陆。
支付
- 前端:向后端接口发送下单请求,提交订单UUID
- 后端:
- 前端:
- 调用该接口 ,调用wx.requestPayment(OBJECT)发起微信支付,其中请求参数由后端生成,应在前一步请求时返回给前端
- 用户根据提示进行支付(非本系统流程)
- 后端:微信服务器会将支付结果发送到步骤2中的 notify_url 字段提供的接口,该接口 需要完成的步骤为
部分Java代码
AppId等配置信息(在运行配置文件中设置)
@Repository
@ConfigurationProperties(prefix = "wx")
@Data
public class WxConfig {
String appId;
String appSecret;
/**
* 商户号Id
*/
String mchId;
/**
* 证书序列号
*/
String serialNo;
/**
* apiV3 私钥
*/
String apiV3Key;
/**
* api私钥
*/
String apiKey;
/**
* 认证类型
*/
String authType;
/**
* 商户私钥文件路径
*/
String privateKeyPath;
}
签名、验签工具类
public class SignUtils {
public static String buildMessage(String... message) {
return String.join("\n", message) + "\n";
}
public static String sign(PrivateKey privateKey, String message)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
return sign(privateKey, message.getBytes(StandardCharsets.UTF_8));
}
public static String sign(PrivateKey privateKey, byte[] message)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
public static String sign(PrivateKey privateKey, String... message)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
return sign(privateKey, buildMessage(message));
}
public static boolean check(X509Certificate certificate, String timestamp, String nonce, Object requestBody, String signature)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
//构造验签名串
String signatureStr = buildMessage(timestamp, nonce, JSONObject.toJSONString(requestBody));
System.out.println("signatureStr = " + signatureStr);
// 加载SHA256withRSA签名器
Signature signer = Signature.getInstance("SHA256withRSA");
// 用微信平台公钥对签名器进行初始化(调上一节中的获取平台证书方法)
signer.initVerify(certificate);
// 把我们构造的验签名串更新到签名器中
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return signer.verify(Base64Utils.decodeFromString(signature));
}
public static boolean check(X509Certificate certificate, HttpServletRequest httpServletRequest, Object requestBody)
throws NoSuchAlgorithmException, SignatureException, InvalidKeyException {
String signature = httpServletRequest.getHeader("Wechatpay-Signature");
String timestamp = httpServletRequest.getHeader("Wechatpay-Timestamp");
String nonce = httpServletRequest.getHeader("Wechatpay-Nonce");
return check(certificate, timestamp, nonce, requestBody, signature);
}
}
报文解密器
public class AesDecrypt {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] apiV3Key;
public AesDecrypt(byte[] apiV3Key) {
if (apiV3Key.length != KEY_LENGTH_BYTE) {
throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
}
this.apiV3Key = apiV3Key;
}
public AesDecrypt(String apiV3Key) {
this(apiV3Key.getBytes());
}
public String decryptToString(String associatedData, String nonce, String ciphertext)
throws GeneralSecurityException {
return decryptToString(
associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext
);
}
public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
throws GeneralSecurityException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(apiV3Key, "AES");
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
}
一些工具Bean
@Configuration
@Slf4j
public class WxConfiguration {
@Bean
public PrivateKey merchantPrivateKey(WxConfig wxConfig) throws FileNotFoundException {
log.info("加载私钥");
File file = new File(wxConfig.getPrivateKeyPath());
System.out.println("file.exists() = " + file.exists());
return PemUtil.loadPrivateKey(new FileInputStream(wxConfig.getPrivateKeyPath()));
}
@Bean
public ScheduledUpdateCertificatesVerifier verifier(WxConfig wxConfig, PrivateKey merchantPrivateKey) {
log.info("加载验证器");
return new ScheduledUpdateCertificatesVerifier(
new WechatPay2Credentials(wxConfig.getMchId(), new PrivateKeySigner(wxConfig.getSerialNo(), merchantPrivateKey)),
wxConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
}
@Bean
public AesDecrypt aesDecrypt(WxConfig wxConfig) {
log.info("加载解密器");
return new AesDecrypt(wxConfig.getApiV3Key());
}
}