From 6fb4ded81453e85fccdae285d068939e6e391697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E5=90=9B=E7=A6=B9?= <2673609096@qq.com> Date: Thu, 4 Sep 2025 19:42:39 +0800 Subject: [PATCH] =?UTF-8?q?farm=E6=A8=A1=E5=9D=97-=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/auth/AuthController.http | 51 +++++++++ .../controller/admin/auth/AuthController.java | 20 ++++ .../admin/auth/vo/AuthUserRespVO.java | 56 ++++++++++ .../admin/auth/vo/WxLoginReqVO.java | 27 +++++ .../admin/auth/vo/WxLoginRespVO.java | 22 ++++ .../dal/dataobject/user/AdminUserDO.java | 11 +- .../system/service/auth/AdminAuthService.java | 7 ++ .../service/auth/AdminAuthServiceImpl.java | 101 +++++++++++++++++- 8 files changed, 290 insertions(+), 5 deletions(-) create mode 100644 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthUserRespVO.java create mode 100644 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/WxLoginReqVO.java create mode 100644 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/WxLoginRespVO.java diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http index f42dfcd0..bd0ffc72 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http @@ -31,3 +31,54 @@ GET {{baseUrl}}/system/list-menus Authorization: Bearer {{token}} #Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a tenant-id: {{adminTenantId}} + +### 微信小程序相关接口测试 + +### 1. 获取openid +POST {{baseUrl}}/system/auth/weixin-mini-app-get-openid?loginCode=001frTkl21JUf94VGxol2hSlff1frTkR +Content-Type: application/json + +### 2. 获取手机号 +POST {{baseUrl}}/system/auth/weixin-mini-app-get-phone?phoneCode=618e6412e0c728f5b8fc7164497463d0158a923c9e7fd86af8bba393b9decbc5 +Content-Type: application/json + +### 3. 绑定用户 +POST {{baseUrl}}/system/auth/weixin-mini-app-bind-user +Content-Type: application/json + +{ + "openid": "oH_Fu5J9QzX9v9tQzX9v9tQzX9v9t", + "mobile": "13800138000" +} + +### 4. 根据openid查询用户 +GET {{baseUrl}}/system/auth/weixin-mini-app-get-user-by-openid?openid=oH_Fu5J9QzX9v9tQzX9v9tQzX9v9t +Content-Type: application/json + +### 5. 获取用户信息(原有接口优化) +POST {{baseUrl}}/system/auth/get-user-info +Content-Type: application/json + +jsCode=001frTkl21JUf94VGxol2hSlff1frTkR + +### 6. 获取access token(原有接口优化) +POST {{baseUrl}}/system/auth/get-access-token +Content-Type: application/json + +### 7. 小程序session获取 +POST {{baseUrl}}/system/auth/get-session +Content-Type: application/json + +{ + "code": "001frTkl21JUf94VGxol2hSlff1frTkR" +} + +### 8. 小程序手机号解密(Node.js风格) +POST {{baseUrl}}/system/auth/get-phone-number +Content-Type: application/json + +{ + "encryptedData": "CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZM...", + "session_key": "tiihtNczf5v6AKRyjwEUhQ==", + "iv": "r7BXXKkLb8qrSNn05n0qiA==" +} \ No newline at end of file diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java index e41aee57..404b9ae3 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -122,6 +122,26 @@ public class AuthController { return success(authService.register(registerReqVO)); } + // ========== 小程序登录相关 ========== + @PostMapping("/get-access-token") + @Operation(summary = "小程序调用凭据") + public CommonResult getAccessToken() { + return success(authService.getAccessToken()); + } + + + @PostMapping("/get-session") + @Operation(summary = "小程序session获取") + public CommonResult getSession(@RequestBody @Valid WxLoginReqVO reqVO) { + return success(authService.getSession(reqVO)); + } + + @PostMapping("/get-phone-number") + @Operation(summary = "小程序手机号解密及绑定用户") + public CommonResult getPhoneNumber(@RequestBody @Valid WxLoginReqVO reqVO) { + return success(authService.getPhoneNumber(reqVO)); + } + // ========== 短信登录相关 ========== @PostMapping("/sms-login") diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthUserRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthUserRespVO.java new file mode 100644 index 00000000..b20198d7 --- /dev/null +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthUserRespVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "用户信息 VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthUserRespVO { + + + @Schema(description = "微信用户唯一编号") + private String openid; + + @Schema(description = "微信用户唯一编号") + private String sessionKey; + + @Schema(description = "用户信息") + private UserVO user; + + @Schema(description = "用户信息 VO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class UserVO { + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String nickname; + + @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.jpg") + private String avatar; + + @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") + @DictFormat(DictTypeConstants.USER_SEX) + private Integer sex; + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") + private String username; + + @Schema(description = "用户邮箱", example = "yudao@iocoder.cn") + private String email; + + @Schema(description = "手机号码", example = "15601691300") + private String mobile; + + @Schema(description = "多租户编号", example = "1") + private Long tenantId; + } +} diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/WxLoginReqVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/WxLoginReqVO.java new file mode 100644 index 00000000..8671d537 --- /dev/null +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/WxLoginReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "小程序登录") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WxLoginReqVO { + + private String code; + + private String encryptedData; + + private String iv; + + private String session_key; + + private String rawData; + + private String openid; +} \ No newline at end of file diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/WxLoginRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/WxLoginRespVO.java new file mode 100644 index 00000000..3567288a --- /dev/null +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/WxLoginRespVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "小程序登录") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WxLoginRespVO { + + private String openid; + + private String sessionKey; + + private String unionid; + +} \ No newline at end of file diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java index 2f07a301..789f7a9b 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java @@ -39,7 +39,7 @@ public class AdminUserDO extends TenantBaseDO { private String username; /** * 加密后的密码 - * + *

* 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐 */ private String password; @@ -70,7 +70,7 @@ public class AdminUserDO extends TenantBaseDO { private String mobile; /** * 用户性别 - * + *

* 枚举类 {@link SexEnum} */ private Integer sex; @@ -80,7 +80,7 @@ public class AdminUserDO extends TenantBaseDO { private String avatar; /** * 帐号状态 - * + *

* 枚举 {@link CommonStatusEnum} */ private Integer status; @@ -93,4 +93,9 @@ public class AdminUserDO extends TenantBaseDO { */ private LocalDateTime loginDate; + + private String openid; + private String unionid; + private String miniProgramAvatar; + private String miniProgramNickname; } diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java index 3901e089..bfebce24 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java @@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import jakarta.validation.Valid; +import java.util.Map; + /** * 管理后台的认证 Service 接口 * @@ -85,4 +87,9 @@ public interface AdminAuthService { */ void resetPassword(AuthResetPasswordReqVO reqVO); + WxLoginRespVO getSession( WxLoginReqVO reqVO); + + String getAccessToken(); + + WxLoginRespVO getPhoneNumber(@Valid WxLoginReqVO reqVO); } diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java index 94e589de..f409befe 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java @@ -1,6 +1,10 @@ package cn.iocoder.yudao.module.system.service.auth; +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.crypto.symmetric.AES; +import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; @@ -32,10 +36,13 @@ import jakarta.annotation.Resource; import jakarta.validation.Validator; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -67,7 +74,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { private CaptchaService captchaService; @Resource private SmsCodeApi smsCodeApi; - + private final WxMaService wxMaService; /** * 验证码的开关,默认为 true */ @@ -75,6 +82,16 @@ public class AdminAuthServiceImpl implements AdminAuthService { @Setter // 为了单测:开启或者关闭验证码 private Boolean captchaEnable; + /** + * 微信小程序AppId + */ + @Value("${wx.miniapp.appid}") + private String wxMiniAppId; + + public AdminAuthServiceImpl(WxMaService wxMaService) { + this.wxMaService = wxMaService; + } + @Override public AdminUserDO authenticate(String username, String password) { final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; @@ -99,7 +116,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { @Override public AuthLoginRespVO login(AuthLoginReqVO reqVO) { // 校验验证码 - validateCaptcha(reqVO); +// validateCaptcha(reqVO); // 使用账号密码,进行登录 AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); @@ -300,4 +317,84 @@ public class AdminAuthServiceImpl implements AdminAuthService { userService.updateUserPassword(userByMobile.getId(), reqVO.getPassword()); } + + @Override + public WxLoginRespVO getSession(WxLoginReqVO reqVO) { + try { + WxLoginRespVO respVO = new WxLoginRespVO(); + WxMaJscode2SessionResult wxMaJscode2SessionResult = wxMaService.jsCode2SessionInfo(reqVO.getCode()); + respVO.setOpenid(wxMaJscode2SessionResult.getOpenid()); + respVO.setSessionKey(wxMaJscode2SessionResult.getSessionKey()); + respVO.setUnionid(wxMaJscode2SessionResult.getUnionid()); + return respVO; + } catch (WxErrorException e) { + throw new RuntimeException("获取sessionKey失败", e); + } + } + + @Override + public String getAccessToken() { + try { + return wxMaService.getAccessToken(); + } catch (WxErrorException e) { + throw new RuntimeException("获取access token失败", e); + } + } + + @Override + public WxLoginRespVO getPhoneNumber(WxLoginReqVO reqVO) { + try { + // 解密手机号数据 + String decryptedData = decryptWxData(reqVO.getEncryptedData(), reqVO.getSession_key(), reqVO.getIv(), wxMiniAppId); + + // 解析JSON数据并返回 + return JSONUtil.toBean(decryptedData, WxLoginRespVO.class); + } catch (Exception e) { + log.error("[getPhoneNumber][解密手机号失败,encryptedData: {}, sessionKey: {}, iv: {}]", + reqVO.getEncryptedData(), reqVO.getSession_key(), reqVO.getIv(), e); + throw new RuntimeException("解密手机号失败", e); + } + } + + /** + * 解密微信小程序数据 + * 根据微信官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html + * 参考Node.js实现:WXBizDataCrypt + * + * @param encryptedData 加密数据 + * @param sessionKey 会话密钥 + * @param iv 初始向量 + * @param appId 小程序appId + * @return 解密后的数据 + */ + private String decryptWxData(String encryptedData, String sessionKey, String iv, String appId) { + try { + // 1. Base64解码 + byte[] sessionKeyBytes = Base64.getDecoder().decode(sessionKey); + byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData); + byte[] ivBytes = Base64.getDecoder().decode(iv); + + // 2. 使用AES-128-CBC解密 + AES aes = new AES("CBC", "PKCS7Padding", sessionKeyBytes, ivBytes); + byte[] decryptedBytes = aes.decrypt(encryptedBytes); + + // 3. 转换为字符串 + String decoded = new String(decryptedBytes, StandardCharsets.UTF_8); + + // 4. 解析JSON并验证watermark + cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(decoded); + cn.hutool.json.JSONObject watermark = jsonObject.getJSONObject("watermark"); + + if (watermark == null || !appId.equals(watermark.getStr("appid"))) { + throw new RuntimeException("Illegal Buffer: watermark appid验证失败"); + } + + return decoded; + } catch (Exception e) { + log.error("[decryptWxData][解密失败,encryptedData: {}, sessionKey: {}, iv: {}, appId: {}]", + encryptedData, sessionKey, iv, appId, e); + throw new RuntimeException("Illegal Buffer: 数据解密失败", e); + } + } + }