WxJava develops WeChat login and WeChat payment


Preface

WxJava is a WeChat Java development toolkit that supports the back-end development of WeChat functional modules including WeChat payment, open platform, public account, enterprise WeChat/enterprise account, mini program and other WeChat function modules.

name website
Gitee official website https://gitee.com/binary/weixin-java-tools
WxJava online documentation http://wxjava.fly2you.cn/zh-CN/start/pay/explain.html
Development Documentation Wiki https://github.com/wechat-group/WxJava/wiki
Javadoc weixin-java-miniapp , weixin-java-pay , weixin-java-mp, weixin-java-common , weixin-java-cp , weixin-java-open
WeChat payment document https://pay.weixin.qq.com/docs/merchant/apis/mini-program-payment/mini-prepay.html
WeChat login document https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html

Preparation work
WeChat payment needs to open WeChat payment merchants. Please check the official website: https://pay.weixin.qq.com/docs/merchant/products/jsapi-payment/preparation.html

1. Introduce dependencies

The version I am using here is 4.5.0

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-pay</artifactId>
    <version>${weixin-java.version}</version>
</dependency>
<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-miniapp</artifactId>
    <version>${weixin-java.version}</version>
</dependency>

2. Modify the configuration file

The configuration information here has been changed and cannot be used directly.
WeChat payment uses the apiV3 version.
The certificate path can be an absolute path or a classpath. I put it under /resource/cert of the project.

wx:
  miniapp:
    configs:
      - appid: wx2xxxxxxxx
        secret: f7bd3ed88cxxxxxxx222e1a7e4f722ad9
        msgDataFormat: JSON
  pay:
    appId: wx2d0f68xxx7f #微信公众号或者小程序等的appid
    mchId: 1650000080 #微信支付商户号
    apiV3Key: TWBQNkNwwjxxxxx2hN5oQ
    certSerialNo: 2078791B21788DC90E44xxxxxx7291FFD
    privateKeyPath: classpath:cert/apiclient_key.pem #apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径
    privateCertPath: classpath:cert/apiclient_cert.pem #apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径
    notifyUrl: https://35fxxxxxpbf.guyubao.com/anonymous/wx/notify/order

3. Mini program WeChat login

1.Login process timing

Insert image description here

2. Understand openid, unionid and code

1.openid

openid is a string used to uniquely identify a user. In the WeChat applet, each user's openid is unique. Through openid, the mini program can obtain the user's basic information, such as avatar, nickname, etc.

Note: The same user has different openids in different mini programs. Therefore, when developing small programs, openid cannot be used to determine the uniqueness of users.

2. unions

Unionid is a string used to uniquely identify the user when the user binds multiple applications under the same WeChat open platform account. If a user uses the same WeChat ID for login authorization in multiple mini-programs, the unionid in these mini-programs will be the same.

Note: The user's unionid will only be generated when the user binds multiple applications to the same WeChat open platform account. Therefore, if the user is not bound to multiple applications, the applet will not be able to obtain the user's unionid.

3. code

The code is the user login credential, which is issued to the mini program by the WeChat server. After the user authorizes the login, the applet can obtain the user's code by calling the WeChat login interface. Then, request the user's openid, session_key and other information from the WeChat server through code.

Note: Each code can only be used once and is valid for 5 minutes. Therefore, when using the code to log in, it needs to be converted into the user's openid and session_key and other information in time to avoid the code expiration.

3. Code implementation

1. Add the file WxMaProperties in the config directory

package com.ruoyi.xyhj.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.List;

/**
 * @author <a href="https://github.com/binarywang">Binary Wang</a>
 */
@Data
@ConfigurationProperties(prefix = "wx.miniapp")
public class WxMaProperties {
    
    

    private List<Config> configs;

    @Data
    public static class Config {
    
    
        /**
         * 设置微信小程序的appid
         */
        private String appid;

        /**
         * 设置微信小程序的Secret
         */
        private String secret;

        /**
         * 设置微信小程序消息服务器配置的token
         */
        private String token;

        /**
         * 设置微信小程序消息服务器配置的EncodingAESKey
         */
        private String aesKey;

        /**
         * 消息格式,XML或者JSON
         */
        private String msgDataFormat;
    }

}

2. Inject wxMaService

package com.ruoyi.xyhj.config;

import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage;
import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
import cn.binarywang.wx.miniapp.message.WxMaMessageHandler;
import cn.binarywang.wx.miniapp.message.WxMaMessageRouter;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.File;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author <a href="https://github.com/binarywang">Binary Wang</a>
 */
@Slf4j
@Configuration
@EnableConfigurationProperties(WxMaProperties.class)
public class WxMaConfiguration {
    
    
    private final WxMaProperties properties;

    @Autowired
    public WxMaConfiguration(WxMaProperties properties) {
    
    
        this.properties = properties;
    }

    @Bean("wxMaService")
    public WxMaService wxMaService() {
    
    
        List<WxMaProperties.Config> configs = this.properties.getConfigs();
        if (configs == null) {
    
    
            throw new WxRuntimeException("大哥,拜托先看下项目首页的说明(readme文件),添加下相关配置,注意别配错了!");
        }
        WxMaService maService = new WxMaServiceImpl();
        maService.setMultiConfigs(
            configs.stream()
                .map(a -> {
    
    
                    WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
//                WxMaDefaultConfigImpl config = new WxMaRedisConfigImpl(new JedisPool());
                    // 使用上面的配置时,需要同时引入jedis-lock的依赖,否则会报类无法找到的异常
                    config.setAppid(a.getAppid());
                    config.setSecret(a.getSecret());
                    config.setToken(a.getToken());
                    config.setAesKey(a.getAesKey());
                    config.setMsgDataFormat(a.getMsgDataFormat());
                    return config;
                }).collect(Collectors.toMap(WxMaDefaultConfigImpl::getAppid, a -> a, (o, n) -> o)));
        return maService;
    }

    @Bean
    public WxMaMessageRouter wxMaMessageRouter(WxMaService wxMaService) {
    
    
        final WxMaMessageRouter router = new WxMaMessageRouter(wxMaService);
        router
            .rule().handler(logHandler).next()
            .rule().async(false).content("订阅消息").handler(subscribeMsgHandler).end()
            .rule().async(false).content("文本").handler(textHandler).end()
            .rule().async(false).content("图片").handler(picHandler).end()
            .rule().async(false).content("二维码").handler(qrcodeHandler).end();
        return router;
    }

    private final WxMaMessageHandler subscribeMsgHandler = (wxMessage, context, service, sessionManager) -> {
    
    
        service.getMsgService().sendSubscribeMsg(WxMaSubscribeMessage.builder()
            .templateId("此处更换为自己的模板id")
            .data(Lists.newArrayList(
                new WxMaSubscribeMessage.MsgData("keyword1", "339208499")))
            .toUser(wxMessage.getFromUser())
            .build());
        return null;
    };

    private final WxMaMessageHandler logHandler = (wxMessage, context, service, sessionManager) -> {
    
    
        log.info("收到消息:" + wxMessage.toString());
        service.getMsgService().sendKefuMsg(WxMaKefuMessage.newTextBuilder().content("收到信息为:" + wxMessage.toJson())
            .toUser(wxMessage.getFromUser()).build());
        return null;
    };

    private final WxMaMessageHandler textHandler = (wxMessage, context, service, sessionManager) -> {
    
    
        service.getMsgService().sendKefuMsg(WxMaKefuMessage.newTextBuilder().content("回复文本消息")
            .toUser(wxMessage.getFromUser()).build());
        return null;
    };

    private final WxMaMessageHandler picHandler = (wxMessage, context, service, sessionManager) -> {
    
    
        try {
    
    
            WxMediaUploadResult uploadResult = service.getMediaService()
                .uploadMedia("image", "png",
                    ClassLoader.getSystemResourceAsStream("tmp.png"));
            service.getMsgService().sendKefuMsg(
                WxMaKefuMessage
                    .newImageBuilder()
                    .mediaId(uploadResult.getMediaId())
                    .toUser(wxMessage.getFromUser())
                    .build());
        } catch (WxErrorException e) {
    
    
            e.printStackTrace();
        }

        return null;
    };

    private final WxMaMessageHandler qrcodeHandler = (wxMessage, context, service, sessionManager) -> {
    
    
        try {
    
    
            final File file = service.getQrcodeService().createQrcode("123", 430);
            WxMediaUploadResult uploadResult = service.getMediaService().uploadMedia("image", file);
            service.getMsgService().sendKefuMsg(
                WxMaKefuMessage
                    .newImageBuilder()
                    .mediaId(uploadResult.getMediaId())
                    .toUser(wxMessage.getFromUser())
                    .build());
        } catch (WxErrorException e) {
    
    
            e.printStackTrace();
        }

        return null;
    };

}

3. Call

	@Resource
    private WxMaService wxMaService;

    public String wxLoginOrRegister(String code) {
    
    
        if (StringUtils.isBlank(code)) {
    
    
            return "empty jscode";
        }
        try {
    
    
            //根据code获取openid
            WxMaJscode2SessionResult session = wxMaService.getUserService().getSessionInfo(code);
        } catch (WxErrorException e) {
    
    
          throw new WxLoginException(e.getMessage());
        }finally {
    
    
            WxMaConfigHolder.remove();//清理ThreadLocal
        }
    }

For more information, please refer to the official examples: https://github.com/binarywang/weixin-java-miniapp-demo or javadoc

4. Mini Program WeChat Payment

1.Business flow chart

Insert image description here

2. Signature, private key, certificate, and sensitive information encryption and decryption instructions

1. Signature format
We hope that the merchant's technical developers will construct the signature string according to the rules agreed in the current document. WeChat Pay will use the same method to construct the signature string. If the merchant constructs the signature string in the wrong way, the signature verification will fail. The specific format of the signature string will be explained below.

There are five lines in the signature string, each line has a parameter. Ends with \n (newline character, ASCII encoding value 0x0A), including the last line. If the parameter itself ends with \n, a \n also needs to be appended.

HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n

Document address:
https://pay.weixin.qq.com/docs/merchant/development/interface-rules/signature-generation.html

2. Private key
When a merchant applies for a merchant API certificate, the certificate tool will generate the merchant's private key and save it in the file apiclient_key.pem in the local certificate folder. The private key can also be exported from the merchant's p12 certificate through tools. Please keep the merchant private key file properly.
Document address:
https://pay.weixin.qq.com/docs/merchant/development/interface-rules/privatekey-and-certificate.html

3. Certificate
Merchant API certificate refers to the certificate applied by the merchant and contains the merchant's merchant number, company name, and public key information.

WeChat Payment APIv3 uses a certificate issued by a Certificate Authority (CA). Merchants can generate certificate request strings by themselves, or download the WeChat payment certificate tool to generate certificate request strings. After submitting the certificate request string to the merchant platform, you can obtain the merchant API certificate file. Please pay attention to save the private key file safely.

Each WeChat payment platform certificate is valid for 5 years. Before the certificate expires, WeChat Pay will gradually use the new platform certificate to generate signatures. In order to avoid signature verification failure due to lack of corresponding certificates, the merchant system needs to support multiple WeChat payment platform certificates and regularly download new certificates through the interface and deploy them to the server. Please refer to our certificate renewal guidelines to avoid relying on manual certificate renewal and ensure the continuous operation of your business.

Document address:
https://pay.weixin.qq.com/docs/merchant/development/interface-rules/privatekey-and-certificate.html

4. Encryption and decryption
In order to ensure the confidentiality of sensitive information fields (such as the user's address, bank card number, mobile phone number, etc.) during the communication process, WeChat Payment API v3 requires merchants to encrypt the submitted sensitive information fields. Correspondingly, WeChat Pay will encrypt the downstream sensitive information fields, and merchants need to decrypt them before they can obtain the original text. The following describes the encryption and decryption methods in detail, and how to perform the corresponding calculations.

Document address:
https://pay.weixin.qq.com/docs/merchant/development/interface-rules/sensitive-data-encryption.html

3. Code implementation

1. Add the file WxPayProperties in the config directory

package com.ruoyi.xyhj.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * wxpay pay properties.
 *
 * @author Binary Wang
 */
@Data
@ConfigurationProperties(prefix = "wx.pay")
public class WxPayProperties {
    
    
  /**
   * 设置微信公众号或者小程序等的appid
   */
  private String appId;

  /**
   * 微信支付商户号
   */
  private String mchId;

  /**
   * 微信支付商户V3密钥
   */
  private String apiV3Key;

  /**
   * 证书号
   */
  private String certSerialNo;

  /**
   * apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径
   */
  private String privateKeyPath;

  /**
   * apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径
   */
  private String privateCertPath;

  /**
   * 回调地址
   */
  private String notifyUrl;

}

2. Inject WxPayService

package com.ruoyi.xyhj.config;

import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Binary Wang
 */
@Configuration
@ConditionalOnClass(WxPayService.class)
@EnableConfigurationProperties(WxPayProperties.class)
@AllArgsConstructor
public class WxPayConfiguration {
    
    
  private WxPayProperties properties;

  @Bean("wxPayService")
  @ConditionalOnMissingBean
  public WxPayService wxService() {
    
    
    WxPayConfig payConfig = new WxPayConfig();
    payConfig.setAppId(StringUtils.trimToNull(this.properties.getAppId()));
    payConfig.setMchId(StringUtils.trimToNull(this.properties.getMchId()));
    payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiV3Key()));
    payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo()));
    payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath()));
    payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath()));
    payConfig.setNotifyUrl(StringUtils.trimToNull(this.properties.getNotifyUrl()));
    payConfig.setTradeType("JSAPI");
    payConfig.setSignType("MD5");
    WxPayService wxPayService = new WxPayServiceImpl();
    wxPayService.setConfig(payConfig);
    return wxPayService;
  }

}

3. Create a prepayment order

Note that we are using the v3 version, so when creating an object or calling wxpayService, you need to call the v3 version, otherwise the parameters may not match.

/**
* 创建预支付订单
*/
    @Override
    @Transactional
    public WxUnifiedOrderVo createOrder(PatriarchCreateOrderBo bo) throws WxPayException, IOException, SignatureException, NoSuchAlgorithmException, InvalidKeyException {
    
    
  
        //构建预支付订单对象
        WxPayUnifiedOrderV3Request orderV3Request = buildWxPayUnifiedOrderRequest(order, parentUser.getOpenid(), product.getName());
        WxPayUnifiedOrderV3Result wxPayUnifiedOrderV3Result = wxPayService.unifiedOrderV3(TradeTypeEnum.JSAPI, orderV3Request);
        //构建返回参数
        WxUnifiedOrderVo tokenJSAPI = WechatSignUtil.getTokenJSAPI(wxPayProperties.getAppId(), wxPayUnifiedOrderV3Result.getPrepayId(), wxPayProperties.getPrivateKeyPath());
        return tokenJSAPI;
    }
/**
     * 构建统一下单对象
     * @param order 订单对象
     * @param openId user openId
     * @param productName 产品名
     * @return
     */
    public WxPayUnifiedOrderV3Request buildWxPayUnifiedOrderRequest(TelOrder order, String openId,String productName){
    
    
        WxPayUnifiedOrderV3Request orderRequest = new WxPayUnifiedOrderV3Request();
        //设置订单号
        orderRequest.setOutTradeNo(order.getId().toString());
        //设置交易结束时间为24小时
        orderRequest.setTimeExpire(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format(new Date()));
        //设置订单金额
        orderRequest.setAmount(new WxPayUnifiedOrderV3Request.Amount()
                        .setTotal(BaseWxPayRequest.yuanToFen(order.getAmount().toString())));
        //设置支付者信息
        orderRequest.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(openId));
        //设置商品描述
        orderRequest.setDescription(productName);
        return orderRequest;
    }

4. Signature tool class

package com.ruoyi.xyhj.utils;

import com.ruoyi.xyhj.domain.vo.WxUnifiedOrderVo;

import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.UUID;

public class WechatSignUtil {
    
    

    /**
     * 参考网站 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml
     * 计算签名值
     *
     * @param appId
     * @param prepay_id
     * @return
     * @throws IOException
     * @throws SignatureException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     */
    public static WxUnifiedOrderVo getTokenJSAPI(String appId, String prepay_id, String privateKey) throws IOException, SignatureException, NoSuchAlgorithmException, InvalidKeyException {
    
    
        // 获取随机字符串
        String nonceStr = getNonceStr();
        // 获取微信小程序支付package
        String packagestr = "prepay_id=" + prepay_id;
        long timestamp = System.currentTimeMillis() / 1000;
        //签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值
        String message = buildMessageTwo(appId, timestamp, nonceStr, packagestr);
        //获取对应的签名
        String signature = sign(message.getBytes("utf-8"),privateKey);
        // 组装返回
        WxUnifiedOrderVo vo = new WxUnifiedOrderVo();
        vo.setAppId(appId);
        vo.setTimeStamp(String.valueOf(timestamp));
        vo.setNonceStr(nonceStr);
        vo.setPackageStr(packagestr);
        vo.setSignType("RSA");
        vo.setPaySign(signature);
        return vo;
    }

    /**
     * 生成随机数
     * @return
     */
    public static String getNonceStr(){
    
    
        return UUID.randomUUID().toString()
                .replaceAll("-", "")
                .substring(0, 32);
    }
    /**
     * 拼接参数
     *
     * @return
     */

    public static String buildMessageTwo(String appId, long timestamp, String nonceStr, String packag) {
    
    
        return appId + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + packag + "\n";
    }
    /**
     * 生成签名
     *
     * @return
     */
    public static String sign(byte[] message,String privateKey) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
    
    
        Signature sign = Signature.getInstance("SHA256withRSA"); //SHA256withRSA
        sign.initSign(getPrivateKey(privateKey));
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }
    /**
     * 获取私钥
     * @param filename 私钥文件路径  (required)
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String filename) throws IOException {
    
    
        System.out.println("filename:" + filename);
        filename = filename.replace("classpath:", "");
        WechatSignUtil wechatSignUtil = new WechatSignUtil();
        InputStream resourceAsStream = wechatSignUtil.getClass().getClassLoader().getResourceAsStream(filename);
        byte[] bytes = new byte[0];
        bytes = new byte[resourceAsStream.available()];
        resourceAsStream.read(bytes);
        String content = new String(bytes);
//        String content = new String(Files.readAllBytes(Paths.get(resource.getPath())), "utf-8");
        try {
    
    
            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");

            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
    
    
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
    
    
            throw new RuntimeException("无效的密钥格式");
        }
    }
}

5. Callback interface

 @ApiOperation(value = "支付回调通知处理")
    @PostMapping("/wx/notify/order")
    public void parseOrderNotifyResult(@RequestBody String resultData) throws WxPayException {
    
    
        WxPayOrderNotifyV3Result notifyV3Result = wxPayService.parseOrderNotifyV3Result(resultData,null);
        log.info("回调:{}",notifyV3Result.getResult());
        orderService.wxNotify(notifyV3Result.getResult());
    }

Guess you like

Origin blog.csdn.net/qq_43548590/article/details/132837104