diff --git a/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java b/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java index b9ab00ea4..4cd648fcf 100644 --- a/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java +++ b/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java @@ -126,6 +126,17 @@ public interface SecurityConstants { String MINI_APP_AUTHORIZATION_CODE_URL = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"; + /** + * 微信小程序接口调用凭证 + */ + String MINI_APP_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token" + + "?grant_type=client_credential&appid=%s&secret=%s"; + + /** + * 微信小程序手机号获取接口 + */ + String MINI_APP_PHONE_NUMBER_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s"; + /** * 码云获取token */ diff --git a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/handler/MiniAppLoginHandler.java b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/handler/MiniAppLoginHandler.java index 76a3cef1d..ea3ff832b 100644 --- a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/handler/MiniAppLoginHandler.java +++ b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/handler/MiniAppLoginHandler.java @@ -18,6 +18,7 @@ package com.pig4cloud.pigx.admin.handler; import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; @@ -29,132 +30,231 @@ import com.pig4cloud.pigx.admin.api.entity.SysSocialDetails; import com.pig4cloud.pigx.admin.api.entity.SysUser; import com.pig4cloud.pigx.admin.mapper.SysSocialDetailsMapper; import com.pig4cloud.pigx.admin.service.SysUserService; +import com.pig4cloud.pigx.common.core.constant.CacheConstants; import com.pig4cloud.pigx.common.core.constant.SecurityConstants; import com.pig4cloud.pigx.common.core.constant.enums.LoginTypeEnum; import com.pig4cloud.pigx.common.core.exception.CheckedException; import com.pig4cloud.pigx.common.core.util.MsgUtils; import com.pig4cloud.pigx.common.core.util.R; +import com.pig4cloud.pigx.common.data.cache.RedisUtils; import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** - * @author lengleng - * @date 2019年11月02日 + * 微信小程序手机号登录处理器。 *
- * 微信小程序 + * 通过前端 wx.getPhoneNumber 返回的 code,换取用户手机号并完成登录绑定。 + * access_token 获取后会缓存至 Redis,减少对微信接口的重复调用。 + * + * @author lengleng + * @date 2026-05-26 */ @Slf4j @Component("MINI") @AllArgsConstructor public class MiniAppLoginHandler extends AbstractLoginHandler { + /** + * 微信 access_token 默认有效期,单位秒(官方文档值)。 + */ + private static final long ACCESS_TOKEN_DEFAULT_EXPIRES_IN = 7200L; + + /** + * 提前过期缓冲时间,单位秒,避免在恰好过期时使用即将失效的 token。 + */ + private static final long ACCESS_TOKEN_EXPIRES_BUFFER = 300L; + + /** + * Redis 缓存 access_token 的最小 TTL,单位秒,防止因 expires_in 异常导致缓存永久有效。 + */ + private static final long ACCESS_TOKEN_MIN_TTL = 60L; + private final SysUserService sysUserService; private final SysSocialDetailsMapper sysSocialDetailsMapper; /** - * 小程序登录传入code + * 小程序手机号登录传入 getPhoneNumber 返回的 code。 *
- * 通过code 调用qq 获取唯一标识
+ * 该 code 不是 wx.login 返回的登录 code,只能用于服务端换取手机号,有效期 5 分钟且一次性。
*
- * @param code
- * @return
+ * @param code 前端 wx.getPhoneNumber 回调中的动态令牌,不允许为空
+ * @return 用户手机号(优先返回不含国家码的纯手机号)
*/
@Override
public String identify(String code) {
- SysSocialDetails condition = new SysSocialDetails();
- condition.setType(LoginTypeEnum.MINI_APP.getType());
- SysSocialDetails socialDetails = sysSocialDetailsMapper.selectOne(new QueryWrapper<>(condition));
-
- String url = String.format(SecurityConstants.MINI_APP_AUTHORIZATION_CODE_URL, socialDetails.getAppId(),
- socialDetails.getAppSecret(), code);
- String result = HttpUtil.get(url);
- log.debug("微信小程序响应报文:{}", result);
- JSONObject resultJsonObj = JSONUtil.parseObj(result);
-
- if (resultJsonObj.containsKey("errcode")) {
- log.error("微信小程序登录失败:{}", result);
- throw new CheckedException(MsgUtils.getMessage(UpmsErrorCodes.SYS_MINIAPP_LOGIN_FAILED));
+ if (StrUtil.isBlank(code)) {
+ log.warn("小程序手机号登录 code 为空");
+ throw miniAppLoginFailed();
}
- return resultJsonObj.getStr("openid");
+ SysSocialDetails socialDetails = getMiniAppSocialDetails();
+ String accessToken = getMiniAppAccessToken(socialDetails);
+ String result = postJson(String.format(SecurityConstants.MINI_APP_PHONE_NUMBER_URL, accessToken),
+ JSONUtil.createObj().set("code", code).toString());
+ log.debug("微信小程序手机号响应报文:{}", result);
+ JSONObject resultJsonObj = JSONUtil.parseObj(result);
+
+ checkWechatResponse(resultJsonObj, result);
+ JSONObject phoneInfo = resultJsonObj.getJSONObject("phone_info");
+ if (phoneInfo == null) {
+ log.error("微信小程序手机号响应缺少 phone_info:{}", result);
+ throw miniAppLoginFailed();
+ }
+ String purePhoneNumber = phoneInfo.getStr("purePhoneNumber");
+ if (StrUtil.isNotBlank(purePhoneNumber)) {
+ return purePhoneNumber;
+ }
+ String phoneNumber = phoneInfo.getStr("phoneNumber");
+ if (StrUtil.isBlank(phoneNumber)) {
+ log.error("微信小程序手机号响应缺少手机号:{}", result);
+ throw miniAppLoginFailed();
+ }
+ return phoneNumber;
}
/**
- * 根据openId获取用户信息
+ * 根据手机号获取用户信息。
*
- * @param openId 用户openId
+ * @param phone 用户手机号
* @return 用户信息对象,未找到时返回null
*/
@Override
- public UserInfo info(String openId) {
- if (StrUtil.isBlank(openId)) {
- log.warn("小程序openId为空,无法获取用户信息");
+ public UserInfo info(String phone) {
+ if (StrUtil.isBlank(phone)) {
+ log.warn("小程序手机号为空,无法获取用户信息");
return null;
}
UserDTO userDTO = new UserDTO();
- userDTO.setMiniOpenid(openId);
+ userDTO.setPhone(phone);
R