farm模块-小程序登录
This commit is contained in:
@@ -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=="
|
||||
}
|
||||
@@ -122,6 +122,26 @@ public class AuthController {
|
||||
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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -39,7 +39,7 @@ public class AdminUserDO extends TenantBaseDO {
|
||||
private String username;
|
||||
/**
|
||||
* 加密后的密码
|
||||
*
|
||||
* <p>
|
||||
* 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐
|
||||
*/
|
||||
private String password;
|
||||
@@ -70,7 +70,7 @@ public class AdminUserDO extends TenantBaseDO {
|
||||
private String mobile;
|
||||
/**
|
||||
* 用户性别
|
||||
*
|
||||
* <p>
|
||||
* 枚举类 {@link SexEnum}
|
||||
*/
|
||||
private Integer sex;
|
||||
@@ -80,7 +80,7 @@ public class AdminUserDO extends TenantBaseDO {
|
||||
private String avatar;
|
||||
/**
|
||||
* 帐号状态
|
||||
*
|
||||
* <p>
|
||||
* 枚举 {@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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user