一、微信登录
微信登录主要包含两种场景:
- 小程序登录
- PC(公众号)扫码登录
1. 小程序登录
思路: 使用 weixin-java sdk实现小程序登录
1 2 3 4 5
| <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>4.6.0</version> </dependency>
|
使用此sdk需要先进行配置
yml文件:
1 2 3 4
| wx: miniapp: appId: secret:
|
配置类:
1 2 3 4 5 6 7 8 9 10 11
| import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @Data @ConfigurationProperties(prefix = "wx.miniapp") public class WxConfigProperties { private String appId; private String secret; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @Component public class WxConfigOperator { @Autowired private WxConfigProperties wxConfigProperties; @Bean public WxMaService wxMaService() { WxMaDefaultConfigImpl wxMaConfig = new WxMaDefaultConfigImpl(); wxMaConfig.setAppid(wxConfigProperties.getAppId()); wxMaConfig.setSecret(wxConfigProperties.getSecret()); WxMaService service = new WxMaServiceImpl(); service.setWxMaConfig(wxMaConfig); return service; } }
|
前端调用获取 code:
前端通过 wx.login 接口获取临时登录凭证 code,并传递给后台。
调用微信接口获取会话信息:
后台利用 weixin‐java‐mp SDK 的 WxMaService.getUserService().getSessionInfo(code) 方法,根据 code 调用微信接口获取 session_key 与 openid(以及可能的 unionid)。
getSessionInfo调用了BaseWxMaServiceImpl 中的 jsCode2SessionInfo(jsCode)方法
代码:
将上面配置的 appid 和 secret 设置到请求参数中,并将前端传来的 jsCode 设置到请求参数中,之后请求微信官方的接口获取 session_key 与 openid
1 2 3 4 5 6 7 8 9 10
| public WxMaJscode2SessionResult jsCode2SessionInfo(String jsCode) throws WxErrorException { WxMaConfig config = this.getWxMaConfig(); Map<String, String> params = new HashMap(8); params.put("appid", config.getAppid()); params.put("secret", config.getSecret()); params.put("js_code", jsCode); params.put("grant_type", "authorization_code"); String result = this.get("https://api.weixin.qq.com/sns/jscode2session", Joiner.on("&").withKeyValueSeparator("=").join(params)); return WxMaJscode2SessionResult.fromJson(result); }
|
业务处理:
根据 openid 查询数据库,判断用户是否已注册;
- 若不存在,则注册新用户;
- 若已存在,则更新用户最后登录时间或其他必要信息。
返回登录结果:
最后将用户信息和必要的认证数据返回给前端。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; import cn.binarywang.wx.miniapp.config.WxMaInMemoryConfig; import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; import lombok.AllArgsConstructor;
@AllArgsConstructor public class WxMiniProgramLoginService {
private WxMaService wxMaService;
public WxMaJscode2SessionResult login(String code) throws Exception { WxMaJscode2SessionResult session = wxMaService.getUserService().getSessionInfo(code);
return session; } }
|
想要token可以自定义一个VO返回用户信息和token,在业务逻辑后 根据 openid 或者 用户信息生成 token
关于获取用户信息:
需要前端调用 wx.getUserInfo 接口获取 encryptedData 和 iv ,并传递给后台。
之后后端调用 wxMaService.getUserService().getUserInfo(sessionKey, encryptedData, ivStr)接口,获取用户信息。
2.PC登录
思路:使用 weixin-java sdk实现PC扫码登录
1 2 3 4 5
| <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-mp</artifactId> <version>4.6.0</version> </dependency>
|
yml文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| wx: mp: useRedis: false redisConfig: host: 127.0.0.1 port: 6379 configs: - appId: 1111 secret: 1111 token: 111 aesKey: 111 - appId: 2222 secret: 1111 token: 111 aesKey: 111
|
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| import com.github.binarywang.demo.wx.mp.utils.JsonUtils; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
@Data @ConfigurationProperties(prefix = "wx.mp") public class WxMpProperties {
private boolean useRedis;
private RedisConfig redisConfig;
@Data public static class RedisConfig {
private String host;
private Integer port;
private String password;
private Integer timeout; }
private List<MpConfig> configs;
@Data public static class MpConfig {
private String appId;
private String secret;
private String token;
private String aesKey; }
@Override public String toString() { return JsonUtils.toJson(this); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| import com.github.binarywang.demo.wx.mp.handler.*; import lombok.AllArgsConstructor; import me.chanjar.weixin.common.redis.JedisWxRedisOps; import me.chanjar.weixin.mp.api.WxMpMessageRouter; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig;
import java.util.List; import java.util.stream.Collectors;
import static me.chanjar.weixin.common.api.WxConsts.EventType; import static me.chanjar.weixin.common.api.WxConsts.EventType.SUBSCRIBE; import static me.chanjar.weixin.common.api.WxConsts.EventType.UNSUBSCRIBE; import static me.chanjar.weixin.common.api.WxConsts.XmlMsgType; import static me.chanjar.weixin.common.api.WxConsts.XmlMsgType.EVENT; import static me.chanjar.weixin.mp.constant.WxMpEventConstants.CustomerService.*; import static me.chanjar.weixin.mp.constant.WxMpEventConstants.POI_CHECK_NOTIFY;
@AllArgsConstructor @Configuration @EnableConfigurationProperties(WxMpProperties.class) public class WxMpConfiguration { private final WxMpProperties properties;
@Bean public WxMpService wxMpService() { final List<WxMpProperties.MpConfig> configs = this.properties.getConfigs(); if (configs == null) { throw new RuntimeException("配置错误!"); }
WxMpService service = new WxMpServiceImpl(); service.setMultiConfigStorages(configs .stream().map(a -> { WxMpDefaultConfigImpl configStorage; if (this.properties.isUseRedis()) { final WxMpProperties.RedisConfig redisConfig = this.properties.getRedisConfig(); JedisPoolConfig poolConfig = new JedisPoolConfig(); JedisPool jedisPool = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getTimeout(), redisConfig.getPassword()); configStorage = new WxMpRedisConfigImpl(new JedisWxRedisOps(jedisPool), a.getAppId()); } else { configStorage = new WxMpDefaultConfigImpl(); }
configStorage.setAppId(a.getAppId()); configStorage.setSecret(a.getSecret()); configStorage.setToken(a.getToken()); configStorage.setAesKey(a.getAesKey()); return configStorage; }).collect(Collectors.toMap(WxMpDefaultConfigImpl::getAppId, a -> a, (o, n) -> o))); return service; }
}
|
登录二维码url生成
调用 wxMpService.buildQrConnectUrl(redirectUri, scope, state)方法,生成url。将url传递返回前端,前端生成二维码或者后台生成二维码返回前端
微信扫码与授权
用户使用微信扫描二维码后,会跳转到微信的授权页面,用户确认后微信会将 code 参数传回你在微信公众号后台配置的回调 URL。你需要在该回调接口中调用微信接口获取 openid 和 session_key,并完成用户信息的查询或注册操作。
在回调接口中调用:
1 2 3 4
| WxOAuth2AccessToken accessToken = wxService.getOAuth2Service().getAccessToken(code);
WxOAuth2UserInfo user = wxService.getOAuth2Service().getUserInfo(accessToken, null);
|
状态更新
后台在完成用户授权和业务处理后,更新数据库中用户信息或最近登录时间等
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @GetMapping("/generateQrUrl") public String generateQrUrl() { String redirectUri = "http://yourdomain.com/oauth_callback"; String scope = "snsapi_login"; String state = "STATE"; String qrConnectUrl = wxMpService.buildQrConnectUrl(redirectUri, scope, state); return qrConnectUrl; }
@RequestMapping("/greet") public String greetUser(@RequestParam String code) { WxOAuth2AccessToken accessToken = wxService.getOAuth2Service().getAccessToken(code); WxOAuth2UserInfo user = wxService.getOAuth2Service().getUserInfo(accessToken, null);
return "greet_user"; }
|
二、微信支付
1. 下单
思路:使用 weixin-java sdk实现 统一下单接口
1 2 3 4 5
| <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> <version>4.6.0</version> </dependency>
|
使用此sdk需要先进行配置
yml文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| wx: pay: appId: mchId: mchKey: subAppId: subMchId: keyPath: apiV3Key: certSerialNo: privateCertPath: privateKeyPath: publicKeyPath: publicKeyId:
|
配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| package com.github.binarywang.demo.wx.pay.config;
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties;
@Data @ConfigurationProperties(prefix = "wx.pay") public class WxPayProperties {
private String appId;
private String mchId;
private String mchKey;
private String subAppId;
private String subMchId;
private String keyPath;
private String apiV3Key;
private String certSerialNo;
private String privateCertPath;
private String privateKeyPath;
private String publicKeyPath;
private String publicKeyId;
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| package com.github.binarywang.demo.wx.pay.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;
@Configuration @ConditionalOnClass(WxPayService.class) @EnableConfigurationProperties(WxPayProperties.class) @AllArgsConstructor public class WxPayConfiguration { private WxPayProperties properties;
@Bean @ConditionalOnMissingBean public WxPayService wxService() { WxPayConfig payConfig = new WxPayConfig(); payConfig.setAppId(StringUtils.trimToNull(this.properties.getAppId())); payConfig.setMchId(StringUtils.trimToNull(this.properties.getMchId())); payConfig.setMchKey(StringUtils.trimToNull(this.properties.getMchKey())); payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId())); payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId())); payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath())); payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiV3Key())); payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo())); payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath())); payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath())); payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath())); payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId()));
payConfig.setUseSandboxEnv(false);
WxPayService wxPayService = new WxPayServiceImpl(); wxPayService.setConfig(payConfig); return wxPayService; }
}
|
前端提交订单信息
前端根据用户选购的商品生成订单(包含订单号、金额、商品描述、支付类型、用户openid(JSAPI支付时必传)、客户端IP等信息),将这些订单数据传递给后台下单接口。
接口请求参数参考:https://pay.weixin.qq.com/doc/v2/merchant/4011935214
一些参数如appid、mchid等不用设置,方法内会自动从配置对象中获取到(前提是对应配置已经设置)
调用统一下单接口,组装支付参数
后台利用 weixin‐java‐pay SDK 的 wxPayService.createOrder(request)方法,调用统一下单接口并组装支付参数。
wxPayService.createOrder(request)做了以下操作:
统一下单
接收到订单数据后,构造一个 WxPayUnifiedOrderRequest 对象。
接着调用 wxPayService.unifiedOrder(request) 方法,该方法会:
- 对请求参数进行校验和签名;
- 将请求参数转换为XML格式后通过HTTP POST请求调用微信支付统一下单接口;
- 解析返回的XML数据为
WxPayUnifiedOrderResult 对象,并检查返回结果。如果返回中包含预支付交易会话标识(prepay_id),则下单成功,否则抛出异常。
组装支付参数
再根据订单请求中的 tradeType 不同(如JSAPI、APP、NATIVE、MWEB),调用 createOrder(WxPayUnifiedOrderRequest request) 方法会进一步根据返回的prepay_id组装出支付参数包。例如:
- 对于 JSAPI 支付,返回
WxPayMpOrderResult,其中包括 appId、timeStamp、nonceStr、package(格式为 “prepay_id=” + prepay_id)、signType,以及根据这些参数生成的 paySign;
- 对于 APP 支付,返回
WxPayAppOrderResult,其中封装了所有客户端调起微信支付所需的参数。
- 对于 NATIVE 支付(扫码支付),返回
WxPayNativeOrderResult,其中包含二维码生成所需的 CodeURL;
- 对于 MWEB 支付(H5支付),返回
WxPayMwebOrderResult,其中包含支付链接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| public <T> T createOrder(WxPayUnifiedOrderRequest request) throws WxPayException { WxPayUnifiedOrderResult unifiedOrderResult = this.unifiedOrder(request); String prepayId = unifiedOrderResult.getPrepayId(); if (StringUtils.isBlank(prepayId)) { throw new WxPayException(String.format("无法获取prepay id,错误代码: '%s',信息:%s。", unifiedOrderResult.getErrCode(), unifiedOrderResult.getErrCodeDes())); } else { String timestamp = String.valueOf(System.currentTimeMillis() / 1000L); String nonceStr = unifiedOrderResult.getNonceStr(); String signType; switch (request.getTradeType()) { case "MWEB": return new WxPayMwebOrderResult(unifiedOrderResult.getMwebUrl()); case "NATIVE": return new WxPayNativeOrderResult(unifiedOrderResult.getCodeURL()); case "APP": signType = unifiedOrderResult.getAppid(); if (StringUtils.isNotEmpty(unifiedOrderResult.getSubAppId())) { signType = unifiedOrderResult.getSubAppId(); }
Map<String, String> configMap = new HashMap(8); String partnerId = unifiedOrderResult.getMchId(); if (StringUtils.isNotEmpty(unifiedOrderResult.getSubMchId())) { partnerId = unifiedOrderResult.getSubMchId(); }
configMap.put("prepayid", prepayId); configMap.put("partnerid", partnerId); String packageValue = "Sign=WXPay"; configMap.put("package", packageValue); configMap.put("timestamp", timestamp); configMap.put("noncestr", nonceStr); configMap.put("appid", signType); WxPayAppOrderResult result = WxPayAppOrderResult.builder().sign(SignUtils.createSign(configMap, request.getSignType(), this.getConfig().getMchKey(), (String[])null)).prepayId(prepayId).partnerId(partnerId).appId(signType).packageValue(packageValue).timeStamp(timestamp).nonceStr(nonceStr).build(); return result; case "JSAPI": signType = request.getSignType(); if (signType == null) { signType = "MD5"; }
String appid = unifiedOrderResult.getAppid(); if (StringUtils.isNotEmpty(unifiedOrderResult.getSubAppId())) { appid = unifiedOrderResult.getSubAppId(); }
WxPayMpOrderResult payResult = WxPayMpOrderResult.builder().appId(appid).timeStamp(timestamp).nonceStr(nonceStr).packageValue("prepay_id=" + prepayId).signType(signType).build(); payResult.setPaySign(SignUtils.createSign(payResult, signType, this.getConfig().getMchKey(), (String[])null)); return payResult; default: throw new WxPayException("该交易类型暂不支持"); } } }
|
返回支付参数给前端
后端将组装好的支付参数包返回给前端,前端据此调用微信支付SDK完成支付流程。如果需要,也可以自定义VO将用户信息和token(如果与支付相关)一并返回。
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import com.github.binarywang.wxpay.bean.coupon.*; import com.github.binarywang.wxpay.bean.request.*; import com.github.binarywang.wxpay.bean.result.*; import com.github.binarywang.wxpay.exception.WxPayException; import com.github.binarywang.wxpay.service.WxPayService; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.*;
@AllArgsConstructor public class WechatPayService { private WxPayService wxPayService;
public <T> T createOrder(WxPayUnifiedOrderRequest request) throws WxPayException { T t = wxPayService.createOrder(request); return t; } }
|
2. 支付回调通知处理
思路:使用 weixin-java sdk实现 支付回调通知
配置同上
- 接收XML数据
通过 @RequestBody 接收微信支付平台发送过来的 XML 数据。
- 解析与签名校验
调用 wxPayService.parseOrderNotifyResult(xmlData) 方法,将 XML 转换为 WxPayOrderNotifyResult 对象。该方法内部会根据配置的微信支付参数进行签名校验、数据校验,如果校验不通过则抛出 WxPayException。
- 业务处理
解析成功后,你可以根据 notifyResult 中的 outTradeNo、transaction_id、trade_type、total_fee 等字段进行订单状态更新、支付记录保存等业务操作。
- 返回响应
无论是成功还是失败,回调接口都必须返回微信规定的响应格式。这里我们使用 WxPayNotifyResponse.success("成功") 或 WxPayNotifyResponse.fail("处理失败") 来生成响应字符串。
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
@PostMapping("/order") public String processOrderNotify(@RequestBody String xmlData) { try { WxPayOrderNotifyResult notifyResult = wxPayService.parseOrderNotifyResult(xmlData); log.info("微信支付回调解析成功:{}", notifyResult);
return WxPayNotifyResponse.success("成功"); } catch (WxPayException e) { log.error("微信支付回调处理失败:{}", e.getMessage(), e); return WxPayNotifyResponse.fail("处理失败"); } }
|