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 1/2] =?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); + } + } + } From 3b3bedb7d077080525ff12cd65a0732174245d1a 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:43:00 +0800 Subject: [PATCH 2/2] =?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 --- yudao-server/src/main/resources/application-dev.yaml | 8 ++++---- yudao-server/src/main/resources/application-local.yaml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index 4a2e2a15..5c627331 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -154,16 +154,16 @@ logging: wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档 mp: # 公众号配置(必填) - app-id: wx041349c6f39b268b - secret: 5abee519483bc9f8cb37ce280e814bd0 + app-id: wxde87f69bb4a2cf5c + secret: a5e114df50a86c2578af255adba1c3e7 # 存储配置,解决 AccessToken 的跨节点的共享 config-storage: type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 key-prefix: wx # Redis Key 的前缀 http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档 - appid: wx63c280fe3248a3e7 - secret: 6f270509224a7ae1296bbf1c8cb97aed + appid: wxde87f69bb4a2cf5c + secret: a5e114df50a86c2578af255adba1c3e7 config-storage: type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 key-prefix: wa # Redis Key 的前缀 diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index bdd0eef5..3f573a08 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -197,8 +197,8 @@ wx: # secret: 5abee519483bc9f8cb37ce280e814bd0 # app-id: wx5b23ba7a5589ecbb # 测试号(自己的) # secret: 2a7b3b20c537e52e74afd395eb85f61f - app-id: wxf56b1542b9e85f8a # 测试号(Kongdy 提供的) - secret: 496379dcef1ba869e9234de8d598cfd3 + app-id: wxde87f69bb4a2cf5c # 测试号(Kongdy 提供的) + secret: a5e114df50a86c2578af255adba1c3e7 # 存储配置,解决 AccessToken 的跨节点的共享 config-storage: type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 @@ -209,8 +209,8 @@ wx: # secret: 333ae72f41552af1e998fe1f54e1584a # appid: wx63c280fe3248a3e7 # wenhualian的接口测试号 # secret: 6f270509224a7ae1296bbf1c8cb97aed - appid: wxc4598c446f8a9cb3 # 测试号(Kongdy 提供的) - secret: 4a1a04e07f6a4a0751b39c3064a92c8b + appid: wxde87f69bb4a2cf5c # 测试号(Kongdy 提供的) + secret: a5e114df50a86c2578af255adba1c3e7 # appid: wx66186af0759f47c9 # 测试号(puhui 提供的) # secret: 3218bcbd112cbc614c7264ceb20144ac config-storage: