farm模块-小程序登录

This commit is contained in:
顾君禹
2025-09-04 19:42:39 +08:00
parent 7814f63606
commit 6fb4ded814
8 changed files with 290 additions and 5 deletions

View File

@@ -31,3 +31,54 @@ GET {{baseUrl}}/system/list-menus
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
#Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a #Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a
tenant-id: {{adminTenantId}} 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=="
}

View File

@@ -122,6 +122,26 @@ public class AuthController {
return success(authService.register(registerReqVO)); return success(authService.register(registerReqVO));
} }
// ========== 小程序登录相关 ==========
@PostMapping("/get-access-token")
@Operation(summary = "小程序调用凭据")
public CommonResult<String> getAccessToken() {
return success(authService.getAccessToken());
}
@PostMapping("/get-session")
@Operation(summary = "小程序session获取")
public CommonResult<WxLoginRespVO> getSession(@RequestBody @Valid WxLoginReqVO reqVO) {
return success(authService.getSession(reqVO));
}
@PostMapping("/get-phone-number")
@Operation(summary = "小程序手机号解密及绑定用户")
public CommonResult<WxLoginRespVO> getPhoneNumber(@RequestBody @Valid WxLoginReqVO reqVO) {
return success(authService.getPhoneNumber(reqVO));
}
// ========== 短信登录相关 ========== // ========== 短信登录相关 ==========
@PostMapping("/sms-login") @PostMapping("/sms-login")

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -39,7 +39,7 @@ public class AdminUserDO extends TenantBaseDO {
private String username; private String username;
/** /**
* 加密后的密码 * 加密后的密码
* * <p>
* 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐 * 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐
*/ */
private String password; private String password;
@@ -70,7 +70,7 @@ public class AdminUserDO extends TenantBaseDO {
private String mobile; private String mobile;
/** /**
* 用户性别 * 用户性别
* * <p>
* 枚举类 {@link SexEnum} * 枚举类 {@link SexEnum}
*/ */
private Integer sex; private Integer sex;
@@ -80,7 +80,7 @@ public class AdminUserDO extends TenantBaseDO {
private String avatar; private String avatar;
/** /**
* 帐号状态 * 帐号状态
* * <p>
* 枚举 {@link CommonStatusEnum} * 枚举 {@link CommonStatusEnum}
*/ */
private Integer status; private Integer status;
@@ -93,4 +93,9 @@ public class AdminUserDO extends TenantBaseDO {
*/ */
private LocalDateTime loginDate; private LocalDateTime loginDate;
private String openid;
private String unionid;
private String miniProgramAvatar;
private String miniProgramNickname;
} }

View File

@@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.Map;
/** /**
* 管理后台的认证 Service 接口 * 管理后台的认证 Service 接口
* *
@@ -85,4 +87,9 @@ public interface AdminAuthService {
*/ */
void resetPassword(AuthResetPasswordReqVO reqVO); void resetPassword(AuthResetPasswordReqVO reqVO);
WxLoginRespVO getSession( WxLoginReqVO reqVO);
String getAccessToken();
WxLoginRespVO getPhoneNumber(@Valid WxLoginReqVO reqVO);
} }

View File

@@ -1,6 +1,10 @@
package cn.iocoder.yudao.module.system.service.auth; 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.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.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
@@ -32,10 +36,13 @@ import jakarta.annotation.Resource;
import jakarta.validation.Validator; import jakarta.validation.Validator;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects; import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -67,7 +74,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
private CaptchaService captchaService; private CaptchaService captchaService;
@Resource @Resource
private SmsCodeApi smsCodeApi; private SmsCodeApi smsCodeApi;
private final WxMaService wxMaService;
/** /**
* 验证码的开关,默认为 true * 验证码的开关,默认为 true
*/ */
@@ -75,6 +82,16 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Setter // 为了单测:开启或者关闭验证码 @Setter // 为了单测:开启或者关闭验证码
private Boolean captchaEnable; private Boolean captchaEnable;
/**
* 微信小程序AppId
*/
@Value("${wx.miniapp.appid}")
private String wxMiniAppId;
public AdminAuthServiceImpl(WxMaService wxMaService) {
this.wxMaService = wxMaService;
}
@Override @Override
public AdminUserDO authenticate(String username, String password) { public AdminUserDO authenticate(String username, String password) {
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
@@ -99,7 +116,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Override @Override
public AuthLoginRespVO login(AuthLoginReqVO reqVO) { public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
// 校验验证码 // 校验验证码
validateCaptcha(reqVO); // validateCaptcha(reqVO);
// 使用账号密码,进行登录 // 使用账号密码,进行登录
AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
@@ -300,4 +317,84 @@ public class AdminAuthServiceImpl implements AdminAuthService {
userService.updateUserPassword(userByMobile.getId(), reqVO.getPassword()); 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);
}
}
} }