微信小程序登录修改

This commit is contained in:
2026-01-09 12:17:21 +08:00
parent f948362d1b
commit 8073a95f74
23 changed files with 1417 additions and 58 deletions

View File

@@ -94,4 +94,30 @@ public class LoginParam implements Serializable {
*/
private String token;
// ========== 微信小程序登录相关字段 ==========
/**
* 微信登录codewx.login返回用于换取openid和session_key
* @since 2026-01-09
*/
private String code;
/**
* 手机号授权codegetPhoneNumber返回新版API推荐使用
* @since 2026-01-09
*/
private String phoneCode;
/**
* 加密数据getPhoneNumber返回旧版API用于解密手机号
* @since 2026-01-09
*/
private String encryptedData;
/**
* 解密向量getPhoneNumber返回旧版API用于解密手机号
* @since 2026-01-09
*/
private String iv;
}

View File

@@ -0,0 +1,25 @@
package org.xyzh.common.wechat.config;
import lombok.Data;
/**
* 微信小程序配置
* @author cascade
* @since 2026-01-09
*/
@Data
public class WeChatMiniProgramConfig {
/** 小程序AppID */
private String appId;
/** 小程序AppSecret */
private String appSecret;
/** 当前access_token */
private String accessToken;
/** access_token过期时间毫秒时间戳 */
private Long accessTokenExpireTime;
}

View File

@@ -0,0 +1,52 @@
package org.xyzh.common.wechat.pojo;
import lombok.Data;
/**
* 微信小程序获取手机号接口返回结果
* @author cascade
* @since 2026-01-09
*/
@Data
public class WeChatPhoneResult {
/** 错误码 */
private Integer errcode;
/** 错误信息 */
private String errmsg;
/** 手机号信息 */
private PhoneInfo phoneInfo;
/**
* 手机号信息
*/
@Data
public static class PhoneInfo {
/** 用户绑定的手机号(国外手机号会有区号) */
private String phoneNumber;
/** 没有区号的手机号 */
private String purePhoneNumber;
/** 区号 */
private String countryCode;
/** 数据水印 */
private Watermark watermark;
}
/**
* 数据水印
*/
@Data
public static class Watermark {
/** 小程序appid */
private String appid;
/** 时间戳 */
private Long timestamp;
}
}

View File

@@ -0,0 +1,28 @@
package org.xyzh.common.wechat.pojo;
import lombok.Data;
/**
* 微信小程序 code2Session 接口返回结果
* @author cascade
* @since 2026-01-09
*/
@Data
public class WeChatSessionResult {
/** 用户唯一标识 */
private String openid;
/** 会话密钥 */
private String sessionKey;
/** 用户在开放平台的唯一标识符(需要绑定开放平台) */
private String unionid;
/** 错误码 */
private Integer errcode;
/** 错误信息 */
private String errmsg;
}

View File

@@ -0,0 +1,300 @@
package org.xyzh.common.wechat.service;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.dubbo.config.annotation.DubboReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.xyzh.api.system.service.SysConfigService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.wechat.config.WeChatMiniProgramConfig;
import org.xyzh.common.wechat.pojo.WeChatPhoneResult;
import org.xyzh.common.wechat.pojo.WeChatSessionResult;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
/**
* 微信小程序服务
* @author cascade
* @since 2026-01-09
*/
@Service
public class WeChatMiniProgramService {
private static final Logger logger = LoggerFactory.getLogger(WeChatMiniProgramService.class);
/** 微信小程序配置缓存 */
private WeChatMiniProgramConfig config;
/** code2Session接口地址 */
private static final String CODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session";
/** 获取access_token接口地址 */
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
/** 获取手机号接口地址新版API */
private static final String GET_PHONE_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber";
@DubboReference(version = "1.0.0", group = "system", timeout = 5000, check = false, retries = 0)
private SysConfigService sysConfigService;
/**
* 获取微信小程序配置
*/
public WeChatMiniProgramConfig getConfig() {
if (config == null) {
loadConfig();
}
return config;
}
/**
* 从系统配置加载微信小程序配置
*/
private synchronized void loadConfig() {
if (config != null) {
return;
}
config = new WeChatMiniProgramConfig();
try {
// 从系统配置获取小程序AppID
ResultDomain<String> appIdResult = sysConfigService.getConfigValue("wechat.miniprogram.appid");
if (appIdResult.getSuccess() && appIdResult.getData() != null) {
config.setAppId(appIdResult.getData());
}
// 从系统配置获取小程序AppSecret
ResultDomain<String> appSecretResult = sysConfigService.getConfigValue("wechat.miniprogram.appsecret");
if (appSecretResult.getSuccess() && appSecretResult.getData() != null) {
config.setAppSecret(appSecretResult.getData());
}
logger.info("微信小程序配置加载成功, appId: {}", config.getAppId());
} catch (Exception e) {
logger.error("加载微信小程序配置失败", e);
}
}
/**
* 刷新配置
*/
public void refreshConfig() {
config = null;
loadConfig();
}
/**
* 通过code获取session信息openid和session_key
* @param code wx.login返回的code
* @return session信息
*/
public ResultDomain<WeChatSessionResult> code2Session(String code) {
WeChatMiniProgramConfig cfg = getConfig();
if (cfg == null || cfg.getAppId() == null || cfg.getAppSecret() == null) {
return ResultDomain.failure("微信小程序配置未初始化");
}
try {
String url = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
CODE2SESSION_URL, cfg.getAppId(), cfg.getAppSecret(), code);
String response = httpGet(url);
logger.debug("code2Session响应: {}", response);
WeChatSessionResult result = JSON.parseObject(response, WeChatSessionResult.class);
if (result.getErrcode() != null && result.getErrcode() != 0) {
logger.error("code2Session失败: {} - {}", result.getErrcode(), result.getErrmsg());
return ResultDomain.failure("获取session失败: " + result.getErrmsg());
}
return ResultDomain.success("获取session成功", result);
} catch (Exception e) {
logger.error("code2Session异常", e);
return ResultDomain.failure("获取session异常: " + e.getMessage());
}
}
/**
* 获取access_token
* @return access_token
*/
public String getAccessToken() {
WeChatMiniProgramConfig cfg = getConfig();
if (cfg == null) {
return null;
}
// 检查缓存的token是否有效
if (cfg.getAccessToken() != null && cfg.getAccessTokenExpireTime() != null
&& System.currentTimeMillis() < cfg.getAccessTokenExpireTime()) {
return cfg.getAccessToken();
}
// 重新获取token
try {
String url = String.format("%s?grant_type=client_credential&appid=%s&secret=%s",
ACCESS_TOKEN_URL, cfg.getAppId(), cfg.getAppSecret());
String response = httpGet(url);
JSONObject json = JSON.parseObject(response);
if (json.containsKey("access_token")) {
String accessToken = json.getString("access_token");
int expiresIn = json.getIntValue("expires_in");
cfg.setAccessToken(accessToken);
// 提前5分钟过期
cfg.setAccessTokenExpireTime(System.currentTimeMillis() + (expiresIn - 300) * 1000L);
logger.info("获取access_token成功, 有效期: {}秒", expiresIn);
return accessToken;
} else {
logger.error("获取access_token失败: {}", response);
return null;
}
} catch (Exception e) {
logger.error("获取access_token异常", e);
return null;
}
}
/**
* 通过phoneCode获取手机号新版API推荐使用
* @param phoneCode getPhoneNumber返回的code
* @return 手机号信息
*/
public ResultDomain<WeChatPhoneResult> getPhoneNumber(String phoneCode) {
String accessToken = getAccessToken();
if (accessToken == null) {
return ResultDomain.failure("获取access_token失败");
}
try {
String url = GET_PHONE_URL + "?access_token=" + accessToken;
JSONObject requestBody = new JSONObject();
requestBody.put("code", phoneCode);
String response = httpPost(url, requestBody.toJSONString());
logger.debug("getPhoneNumber响应: {}", response);
WeChatPhoneResult result = JSON.parseObject(response, WeChatPhoneResult.class);
if (result.getErrcode() != null && result.getErrcode() != 0) {
logger.error("getPhoneNumber失败: {} - {}", result.getErrcode(), result.getErrmsg());
return ResultDomain.failure("获取手机号失败: " + result.getErrmsg());
}
return ResultDomain.success("获取手机号成功", result);
} catch (Exception e) {
logger.error("getPhoneNumber异常", e);
return ResultDomain.failure("获取手机号异常: " + e.getMessage());
}
}
/**
* 通过encryptedData和iv解密手机号旧版API
* @param sessionKey 会话密钥
* @param encryptedData 加密数据
* @param iv 解密向量
* @return 解密后的手机号
*/
public ResultDomain<String> decryptPhoneNumber(String sessionKey, String encryptedData, String iv) {
try {
byte[] sessionKeyBytes = Base64.getDecoder().decode(sessionKey);
byte[] encryptedDataBytes = Base64.getDecoder().decode(encryptedData);
byte[] ivBytes = Base64.getDecoder().decode(iv);
SecretKeySpec keySpec = new SecretKeySpec(sessionKeyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] decryptedBytes = cipher.doFinal(encryptedDataBytes);
String decryptedData = new String(decryptedBytes, StandardCharsets.UTF_8);
logger.debug("解密数据: {}", decryptedData);
JSONObject json = JSON.parseObject(decryptedData);
String phoneNumber = json.getString("phoneNumber");
if (phoneNumber == null || phoneNumber.isEmpty()) {
phoneNumber = json.getString("purePhoneNumber");
}
return ResultDomain.success("解密成功", phoneNumber);
} catch (Exception e) {
logger.error("解密手机号失败", e);
return ResultDomain.failure("解密手机号失败: " + e.getMessage());
}
}
/**
* HTTP GET请求
*/
private String httpGet(String urlStr) throws Exception {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
} finally {
conn.disconnect();
}
}
/**
* HTTP POST请求
*/
private String httpPost(String urlStr, String body) throws Exception {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json");
try (OutputStream os = conn.getOutputStream()) {
os.write(body.getBytes(StandardCharsets.UTF_8));
}
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
} finally {
conn.disconnect();
}
}
}