Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into pay_extension

 Conflicts:
	sql/ruoyi-vue-pro.sql
	yudao-admin-server/src/main/resources/application.yaml
	yudao-admin-server/src/test/resources/sql/clean.sql
	yudao-admin-server/src/test/resources/sql/create_tables.sql
	yudao-admin-ui/src/utils/dict.js
	yudao-user-server/src/main/resources/application-local.yaml
This commit is contained in:
YunaiV
2021-12-16 09:45:50 +08:00
801 changed files with 64443 additions and 2416 deletions

View File

@ -1,9 +0,0 @@
package cn.iocoder.yudao.userserver.framework.async.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfiguration {
}

View File

@ -1,4 +0,0 @@
/**
* 异步执行,基于 Spring @Async 实现
*/
package cn.iocoder.yudao.userserver.framework.async;

View File

@ -1,11 +1,11 @@
### 请求 /system/user/profile/get 接口 => 没有权限
GET {{userServerUrl}}/system/user/profile/get
### 请求 /member/user/profile/get 接口 => 没有权限
GET {{userServerUrl}}/member/user/profile/get
Authorization: Bearer test245
### 请求 /system/user/profile/revise-nickname 接口 成功
PUT {{userServerUrl}}/system/user/profile/update-nickname?nickName=yunai222
### 请求 /member/user/profile/revise-nickname 接口 成功
PUT {{userServerUrl}}/member/user/profile/update-nickname?nickName=yunai222
Authorization: Bearer test245
### 请求 /system/user/profile/get-user-info 接口 成功
GET {{userServerUrl}}/system/user/profile/get-user-info?id=245
Authorization: Bearer test245
### 请求 /member/user/profile/get-user-info 接口 成功
GET {{userServerUrl}}/member/user/profile/get-user-info?id=245
Authorization: Bearer test245

View File

@ -5,6 +5,9 @@ import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO;
import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@ -13,16 +16,18 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.io.IOException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.userserver.modules.member.enums.MbrErrorCodeConstants.FILE_IS_EMPTY;
@Api(tags = "用户个人中心")
@RestController
@RequestMapping("/system/user/profile")
@RequestMapping("/member/user/profile")
@Validated
@Slf4j
public class SysUserProfileController {
@ -30,11 +35,14 @@ public class SysUserProfileController {
@Resource
private MbrUserService userService;
@Resource
private SysSmsCodeService smsCodeService;
@PutMapping("/update-nickname")
@ApiOperation("修改用户昵称")
@PreAuthenticated
public CommonResult<Boolean> updateNickname(@RequestParam("nickName") String nickName) {
userService.updateNickname(getLoginUserId(), nickName);
public CommonResult<Boolean> updateNickname(@RequestParam("nickname") String nickname) {
userService.updateNickname(getLoginUserId(), nickname);
return success(true);
}
@ -49,12 +57,24 @@ public class SysUserProfileController {
return success(avatar);
}
@GetMapping("/get-user-info")
@ApiOperation("取用户头像与昵称")
@GetMapping("/get")
@ApiOperation("得基本信息")
@PreAuthenticated
public CommonResult<MbrUserInfoRespVO> getUserInfo() {
return success(userService.getUserInfo(getLoginUserId()));
}
@PostMapping("/update-mobile")
@ApiOperation(value = "修改用户手机")
@PreAuthenticated
public CommonResult<Boolean> updateMobile(@RequestBody @Valid MbrUserUpdateMobileReqVO reqVO) {
// 校验验证码
// TODO @宋天:统一到 userService.updateMobile 方法里
smsCodeService.useSmsCode(reqVO.getMobile(),SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene(), reqVO.getCode(),getClientIP());
userService.updateMobile(getLoginUserId(), reqVO);
return success(true);
}
}

View File

@ -13,7 +13,7 @@ import lombok.NoArgsConstructor;
public class MbrUserInfoRespVO {
@ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
private String nickName;
private String nickname;
@ApiModelProperty(value = "用户头像", required = true, example = "/infra/file/get/35a12e57-4297-4faa-bf7d-7ed2f211c952")
private String avatar;

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.userserver.modules.member.controller.user.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@ApiModel("修改手机 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MbrUserUpdateMobileReqVO {
@ApiModelProperty(value = "手机验证码", required = true, example = "1024")
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String code;
@ApiModelProperty(value = "手机号",required = true,example = "15823654487")
@NotBlank(message = "手机号不能为空")
// TODO @宋天:手机校验,直接使用 @Mobile 哈
@Length(min = 8, max = 11, message = "手机号码长度为 8-11 位")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误")
private String mobile;
}

View File

@ -0,0 +1,15 @@
package cn.iocoder.yudao.userserver.modules.member.convert.user;
import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserInfoRespVO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface UserProfileConvert {
UserProfileConvert INSTANCE = Mappers.getMapper(UserProfileConvert.class);
MbrUserInfoRespVO convert(MbrUserDO bean);
}

View File

@ -1,55 +0,0 @@
package cn.iocoder.yudao.userserver.modules.member.enums.social;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.List;
/**
* 社交平台的类型枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum SysSocialTypeEnum implements IntArrayValuable {
GITEE(10, "GITEE"), // https://gitee.com/api/v5/oauth_doc#/
DINGTALK(20, "DINGTALK"), // https://developers.dingtalk.com/document/app/obtain-identity-credentials
WECHAT_ENTERPRISE(30, "WECHAT_ENTERPRISE"), // https://xkcoding.com/2019/08/06/use-justauth-integration-wechat-enterprise.html
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SysSocialTypeEnum::getType).toArray();
public static final List<Integer> WECHAT_ALL = ListUtil.toList(WECHAT_ENTERPRISE.type);
/**
* 类型
*/
private final Integer type;
/**
* 类型的标识
*/
private final String source;
@Override
public int[] array() {
return ARRAYS;
}
public static SysSocialTypeEnum valueOfType(Integer type) {
return ArrayUtil.firstMatch(o -> o.getType().equals(type), values());
}
public static List<Integer> getRelationTypes(Integer type) {
if (WECHAT_ALL.contains(type)) {
return WECHAT_ALL;
}
return ListUtil.toList(type);
}
}

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.userserver.modules.member.service.user;
import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserInfoRespVO;
import cn.iocoder.yudao.framework.common.validation.Mobile;
import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO;
import java.io.InputStream;
@ -50,9 +51,9 @@ public interface MbrUserService {
/**
* 修改用户昵称
* @param userId 用户id
* @param nickName 用户新昵称
* @param nickname 用户新昵称
*/
void updateNickname(Long userId, String nickName);
void updateNickname(Long userId, String nickname);
/**
* 修改用户头像
@ -64,9 +65,17 @@ public interface MbrUserService {
/**
* 根据用户id获取用户头像与昵称
*
* @param userId 用户id
* @return 用户响应实体类
*/
MbrUserInfoRespVO getUserInfo(Long userId);
/**
* 修改手机
* @param userId 用户id
* @param reqVO 请求实体
*/
void updateMobile(Long userId, MbrUserUpdateMobileReqVO reqVO);
}

View File

@ -6,8 +6,11 @@ import cn.iocoder.yudao.coreservice.modules.infra.service.file.InfFileCoreServic
import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserInfoRespVO;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO;
import cn.iocoder.yudao.userserver.modules.member.convert.user.UserProfileConvert;
import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper;
import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
@ -40,6 +43,9 @@ public class MbrUserServiceImpl implements MbrUserService {
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private SysAuthService sysAuthService;
@Override
public MbrUserDO getUserByMobile(String mobile) {
return userMapper.selectByMobile(mobile);
@ -80,15 +86,15 @@ public class MbrUserServiceImpl implements MbrUserService {
}
@Override
public void updateNickname(Long userId, String nickName) {
public void updateNickname(Long userId, String nickname) {
MbrUserDO user = this.checkUserExists(userId);
// 仅当新昵称不等于旧昵称时进行修改
if (nickName.equals(user.getNickname())){
if (nickname.equals(user.getNickname())){
return;
}
MbrUserDO userDO = new MbrUserDO();
userDO.setId(user.getId());
userDO.setNickname(nickName);
userDO.setNickname(nickname);
userMapper.updateById(userDO);
}
@ -110,10 +116,20 @@ public class MbrUserServiceImpl implements MbrUserService {
public MbrUserInfoRespVO getUserInfo(Long userId) {
MbrUserDO user = this.checkUserExists(userId);
// 拼接返回结果
MbrUserInfoRespVO userResp = new MbrUserInfoRespVO();
userResp.setNickName(user.getNickname());
userResp.setAvatar(user.getAvatar());
return userResp;
return UserProfileConvert.INSTANCE.convert(user);
}
@Override
public void updateMobile(Long userId, MbrUserUpdateMobileReqVO reqVO) {
// 检测用户是否存在
MbrUserDO userDO = checkUserExists(userId);
// 检测手机与验证码是否匹配
// TODO @宋天:修改手机的时候。应该要校验,老手机 + 老手机 code新手机 + 新手机 code
sysAuthService.checkIfMobileMatchCodeAndDeleteCode(userDO.getMobile(),reqVO.getCode());
// 更新用户手机
// TODO @宋天:更新的时候,单独创建对象。直接全量更新,会可能导致属性覆盖。可以看看打印出来的 SQL 哈
userDO.setMobile(reqVO.getMobile());
userMapper.updateById(userDO);
}
@VisibleForTesting

View File

@ -1,9 +1,14 @@
package cn.iocoder.yudao.userserver.modules.system.controller.auth;
import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialCoreService;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.*;
import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService;
import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
import com.alibaba.fastjson.JSON;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
@ -13,11 +18,13 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getUserAgent;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Api(tags = "认证")
@RestController
@ -30,6 +37,8 @@ public class SysAuthController {
private SysAuthService authService;
@Resource
private SysSmsCodeService smsCodeService;
@Resource
private SysSocialCoreService socialService;
@PostMapping("/login")
@ApiOperation("使用手机 + 密码登录")
@ -48,16 +57,49 @@ public class SysAuthController {
}
@PostMapping("/send-sms-code")
@ApiOperation("发送手机验证码")
@ApiOperation(value = "发送手机验证码",notes = "不检测该手机号是否已被注册")
public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid SysAuthSendSmsReqVO reqVO) {
smsCodeService.sendSmsCode(reqVO.getMobile(), reqVO.getScene(), getClientIP());
return success(true);
}
@PostMapping("/send-sms-new-code")
@ApiOperation(value = "发送手机验证码",notes = "检测该手机号是否已被注册,用于修改手机时使用")
public CommonResult<Boolean> sendSmsNewCode(@RequestBody @Valid SysAuthSendSmsReqVO reqVO) {
smsCodeService.sendSmsNewCode(reqVO);
return success(true);
}
@GetMapping("/send-sms-code-login")
@ApiOperation(value = "向已登录用户发送验证码",notes = "修改手机时验证原手机号使用")
public CommonResult<Boolean> sendSmsCodeLogin() {
smsCodeService.sendSmsCodeLogin(getLoginUserId());
return success(true);
}
@PostMapping("/reset-password")
@ApiOperation(value = "重置密码", notes = "用户忘记密码时使用")
@PreAuthenticated
public CommonResult<Boolean> resetPassword(@RequestBody @Valid MbrAuthResetPasswordReqVO reqVO) {
return null;
authService.resetPassword(reqVO);
return success(true);
}
@PostMapping("/update-password")
@ApiOperation(value = "修改用户密码",notes = "用户修改密码时使用")
@PreAuthenticated
public CommonResult<Boolean> updatePassword(@RequestBody @Valid MbrAuthUpdatePasswordReqVO reqVO) {
authService.updatePassword(getLoginUserId(), reqVO);
return success(true);
}
@PostMapping("/check-sms-code")
@ApiOperation(value = "校验验证码是否正确")
@PreAuthenticated
public CommonResult<Boolean> checkSmsCode(@RequestBody @Valid SysAuthSmsLoginReqVO reqVO) {
// TODO @宋天check 的时候,不应该使用 useSmsCode 哈这样验证码就直接被使用了。另外check 开头的方法,更多是校验的逻辑,不会有 update 数据的动作。这点,在方法命名上,也是要注意的
smsCodeService.useSmsCode(reqVO.getMobile(),SysSmsSceneEnum.CHECK_CODE_BY_SMS.getScene(),reqVO.getCode(),getClientIP());
return success(true);
}
// ========== 社交登录相关 ==========
@ -70,42 +112,37 @@ public class SysAuthController {
})
public CommonResult<String> socialAuthRedirect(@RequestParam("type") Integer type,
@RequestParam("redirectUri") String redirectUri) {
// return CommonResult.success(socialService.getAuthorizeUrl(type, redirectUri));
return null;
return CommonResult.success(socialService.getAuthorizeUrl(type, redirectUri));
}
@PostMapping("/social-login")
@ApiOperation("社交登录,使用 code 授权码")
public CommonResult<SysAuthLoginRespVO> socialLogin(@RequestBody @Valid MbrAuthSocialLoginReqVO reqVO) {
// String token = authService.socialLogin(reqVO, getClientIP(), getUserAgent());
// // 返回结果
// return success(MbrAuthLoginRespVO.builder().token(token).build());
return null;
public CommonResult<SysAuthLoginRespVO> socialLogin(@RequestBody @Valid MbrAuthSocialLoginReqVO reqVO) {
String token = authService.socialLogin(reqVO, getClientIP(), getUserAgent());
return success(SysAuthLoginRespVO.builder().token(token).build());
}
@PostMapping("/social-login2")
@ApiOperation("社交登录,使用 code 授权码 + 账号密")
@ApiOperation("社交登录,使用 手机号 + 手机验证")
public CommonResult<SysAuthLoginRespVO> socialLogin2(@RequestBody @Valid MbrAuthSocialLogin2ReqVO reqVO) {
// String token = authService.socialLogin2(reqVO, getClientIP(), getUserAgent());
// // 返回结果
// return success(MbrAuthLoginRespVO.builder().token(token).build());
return null;
String token = authService.socialLogin2(reqVO, getClientIP(), getUserAgent());
return success(SysAuthLoginRespVO.builder().token(token).build());
}
@PostMapping("/social-bind")
@ApiOperation("社交绑定,使用 code 授权码")
public CommonResult<Boolean> socialBind(@RequestBody @Valid MbrAuthSocialBindReqVO reqVO) {
// authService.socialBind(getLoginUserId(), reqVO);
// return CommonResult.success(true);
return null;
authService.socialBind(getLoginUserId(), reqVO);
return CommonResult.success(true);
}
@DeleteMapping("/social-unbind")
@ApiOperation("取消社交绑定")
public CommonResult<Boolean> socialUnbind(@RequestBody MbrAuthSocialUnbindReqVO reqVO) {
// socialService.unbindSocialUser(getLoginUserId(), reqVO.getType(), reqVO.getUnionId());
// return CommonResult.success(true);
return null;
socialService.unbindSocialUser(getLoginUserId(), reqVO.getType(), reqVO.getUnionId(), UserTypeEnum.MEMBER);
return CommonResult.success(true);
}
}

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.userserver.modules.system.controller.auth.vo;
import cn.iocoder.yudao.coreservice.modules.system.enums.social.SysSocialTypeEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.userserver.modules.member.enums.social.SysSocialTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.userserver.modules.system.controller.auth.vo;
import cn.iocoder.yudao.coreservice.modules.system.enums.social.SysSocialTypeEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.userserver.modules.member.enums.social.SysSocialTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
@ -34,15 +34,16 @@ public class MbrAuthSocialLogin2ReqVO {
@NotEmpty(message = "state 不能为空")
private String state;
@ApiModelProperty(value = "", required = true, example = "yudaoyuanma")
@NotEmpty(message = "登录账号不能为空")
@Length(min = 4, max = 16, message = "账号长度为 4-16 位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;
@ApiModelProperty(value = "手机", required = true, example = "15119100000")
@NotEmpty(message = "手机号不能为空")
@Length(min = 11, max = 11, message = "手机号是11位数字")
private String mobile;
@ApiModelProperty(value = "手机验证码", required = true, example = "1024")
@NotEmpty(message = "手机验证码不能为空")
@Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
private String smsCode;
@ApiModelProperty(value = "密码", required = true, example = "buzhidao")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
}

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.userserver.modules.system.controller.auth.vo;
import cn.iocoder.yudao.coreservice.modules.system.enums.social.SysSocialTypeEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.userserver.modules.member.enums.social.SysSocialTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.userserver.modules.system.controller.auth.vo;
import cn.iocoder.yudao.coreservice.modules.system.enums.social.SysSocialTypeEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.userserver.modules.member.enums.social.SysSocialTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.userserver.modules.system.controller.auth.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
@ApiModel("修改密码 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MbrAuthUpdatePasswordReqVO {
@ApiModelProperty(value = "用户旧密码", required = true, example = "123456")
@NotBlank(message = "旧密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String oldPassword;
@ApiModelProperty(value = "新密码", required = true, example = "buzhidao")
@NotEmpty(message = "新密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
}

View File

@ -14,6 +14,7 @@ public interface SysErrorCodeConstants {
ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1005000001, "登录失败,账号被禁用");
ErrorCode AUTH_LOGIN_FAIL_UNKNOWN = new ErrorCode(1005000002, "登录失败"); // 登录失败的兜底,未知原因
ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1005000003, "Token 已经过期");
ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1005000004, "未绑定账号,需要进行绑定");
// ========== SMS CODE 模块 1005001000 ==========
ErrorCode USER_SMS_CODE_NOT_FOUND = new ErrorCode(1005001000, "验证码不存在");
@ -22,4 +23,10 @@ public interface SysErrorCodeConstants {
ErrorCode USER_SMS_CODE_NOT_CORRECT = new ErrorCode(1005001003, "验证码不正确");
ErrorCode USER_SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1005001004, "超过每日短信发送数量");
ErrorCode USER_SMS_CODE_SEND_TOO_FAST = new ErrorCode(1005001005, "短信发送过于频率");
ErrorCode USER_SMS_CODE_IS_EXISTS = new ErrorCode(1005001006, "手机号已被使用");
// ========== 用户模块 1005002000 ==========
ErrorCode USER_NOT_EXISTS = new ErrorCode(1005002001, "用户不存在");
ErrorCode USER_CODE_FAILED = new ErrorCode(1005002002, "验证码不匹配");
ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1005002003, "密码校验失败");
}

View File

@ -17,7 +17,9 @@ public enum SysSmsSceneEnum implements IntArrayValuable {
LOGIN_BY_SMS(1, "手机号登陆"),
CHANGE_MOBILE_BY_SMS(2, "更换手机号"),
;
FORGET_MOBILE_BY_SMS(3, "忘记密码"),
CHECK_CODE_BY_SMS(4, "审核验证码"),
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SysSmsSceneEnum::getScene).toArray();

View File

@ -1,8 +1,7 @@
package cn.iocoder.yudao.userserver.modules.system.service.auth;
import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthLoginReqVO;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthSmsLoginReqVO;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.*;
import javax.validation.Valid;
@ -35,4 +34,52 @@ public interface SysAuthService extends SecurityAuthFrameworkService {
*/
String smsLogin(@Valid SysAuthSmsLoginReqVO reqVO, String userIp, String userAgent);
/**
* 社交登录,使用 code 授权码
*
* @param reqVO 登录信息
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return 身份令牌,使用 JWT 方式
*/
String socialLogin(@Valid MbrAuthSocialLoginReqVO reqVO, String userIp, String userAgent);
/**
* 社交登录,使用 手机号 + 手机验证码
*
* @param reqVO 登录信息
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return 身份令牌,使用 JWT 方式
*/
String socialLogin2(@Valid MbrAuthSocialLogin2ReqVO reqVO, String userIp, String userAgent);
/**
* 社交绑定,使用 code 授权码
*
* @param userId 用户编号
* @param reqVO 绑定信息
*/
void socialBind(Long userId, @Valid MbrAuthSocialBindReqVO reqVO);
/**
* 修改用户密码
* @param userId 用户id
* @param userReqVO 用户请求实体类
*/
void updatePassword(Long userId, @Valid MbrAuthUpdatePasswordReqVO userReqVO);
/**
* 忘记密码
* @param userReqVO 用户请求实体类
*/
void resetPassword(MbrAuthResetPasswordReqVO userReqVO);
/**
* 检测手机与验证码是否匹配
* @param phone 手机号
* @param code 验证码
*/
void checkIfMobileMatchCodeAndDeleteCode(String phone,String code);
}

View File

@ -1,26 +1,32 @@
package cn.iocoder.yudao.userserver.modules.system.service.auth.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.social.SysSocialUserDO;
import cn.iocoder.yudao.coreservice.modules.system.enums.logger.SysLoginLogTypeEnum;
import cn.iocoder.yudao.coreservice.modules.system.enums.logger.SysLoginResultEnum;
import cn.iocoder.yudao.coreservice.modules.system.service.auth.SysUserSessionCoreService;
import cn.iocoder.yudao.coreservice.modules.system.service.logger.SysLoginLogCoreService;
import cn.iocoder.yudao.coreservice.modules.system.service.logger.dto.SysLoginLogCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialCoreService;
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;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper;
import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthLoginReqVO;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthSmsLoginReqVO;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.*;
import cn.iocoder.yudao.userserver.modules.system.convert.auth.SysAuthConvert;
import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService;
import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
@ -29,13 +35,17 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConstants.*;
/**
@ -47,6 +57,8 @@ import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConst
@Slf4j
public class SysAuthServiceImpl implements SysAuthService {
private static final UserTypeEnum USER_TYPE_ENUM = UserTypeEnum.MEMBER;
@Resource
@Lazy // 延迟加载,因为存在相互依赖的问题
private AuthenticationManager authenticationManager;
@ -59,6 +71,17 @@ public class SysAuthServiceImpl implements SysAuthService {
private SysLoginLogCoreService loginLogCoreService;
@Resource
private SysUserSessionCoreService userSessionCoreService;
@Resource
private SysSocialCoreService socialService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private MbrUserMapper userMapper;
private static final UserTypeEnum userTypeEnum = UserTypeEnum.MEMBER;
@Override
public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
@ -99,6 +122,65 @@ public class SysAuthServiceImpl implements SysAuthService {
return userSessionCoreService.createUserSession(loginUser, userIp, userAgent);
}
@Override
public String socialLogin(MbrAuthSocialLoginReqVO reqVO, String userIp, String userAgent) {
// 使用 code 授权码,进行登录
AuthUser authUser = socialService.getAuthUser(reqVO.getType(), reqVO.getCode(), reqVO.getState());
org.springframework.util.Assert.notNull(authUser, "授权用户不为空");
// 如果未绑定 SysSocialUserDO 用户,则无法自动登录,进行报错
String unionId = socialService.getAuthUserUnionId(authUser);
List<SysSocialUserDO> socialUsers = socialService.getAllSocialUserList(reqVO.getType(), unionId, USER_TYPE_ENUM);
if (CollUtil.isEmpty(socialUsers)) {
throw exception(AUTH_THIRD_LOGIN_NOT_BIND);
}
// 自动登录
MbrUserDO user = userService.getUser(socialUsers.get(0).getUserId());
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
this.createLoginLog(user.getMobile(), SysLoginLogTypeEnum.LOGIN_SOCIAL, SysLoginResultEnum.SUCCESS);
// 创建 LoginUser 对象
LoginUser loginUser = SysAuthConvert.INSTANCE.convert(user);
// 绑定社交用户(更新)
socialService.bindSocialUser(loginUser.getId(), reqVO.getType(), authUser, USER_TYPE_ENUM);
// 缓存登录用户到 Redis 中,返回 sessionId 编号
return userSessionCoreService.createUserSession(loginUser, userIp, userAgent);
}
@Override
public String socialLogin2(MbrAuthSocialLogin2ReqVO reqVO, String userIp, String userAgent) {
AuthUser authUser = socialService.getAuthUser(reqVO.getType(), reqVO.getCode(), reqVO.getState());
org.springframework.util.Assert.notNull(authUser, "授权用户不为空");
// 使用手机号、手机验证码登录
SysAuthSmsLoginReqVO loginReqVO = SysAuthSmsLoginReqVO
.builder()
.mobile(reqVO.getMobile())
.code(reqVO.getSmsCode())
.build();
String sessionId = this.smsLogin(loginReqVO, userIp, userAgent);
LoginUser loginUser = userSessionCoreService.getLoginUser(sessionId);
// 绑定社交用户(新增)
socialService.bindSocialUser(loginUser.getId(), reqVO.getType(), authUser, USER_TYPE_ENUM);
return sessionId;
}
@Override
public void socialBind(Long userId, MbrAuthSocialBindReqVO reqVO) {
// 使用 code 授权码,进行登录
AuthUser authUser = socialService.getAuthUser(reqVO.getType(), reqVO.getCode(), reqVO.getState());
org.springframework.util.Assert.notNull(authUser, "授权用户不为空");
// 绑定社交用户(新增)
socialService.bindSocialUser(userId, reqVO.getType(), authUser, USER_TYPE_ENUM);
}
private LoginUser login0(String username, String password) {
final SysLoginLogTypeEnum logTypeEnum = SysLoginLogTypeEnum.LOGIN_USERNAME;
// 用户验证
@ -136,12 +218,12 @@ public class SysAuthServiceImpl implements SysAuthService {
}
reqDTO.setUsername(mobile);
reqDTO.setUserAgent(ServletUtils.getUserAgent());
reqDTO.setUserIp(ServletUtils.getClientIP());
reqDTO.setUserIp(getClientIP());
reqDTO.setResult(loginResult.getResult());
loginLogCoreService.createLoginLog(reqDTO);
// 更新最后登录时间
if (user != null && Objects.equals(SysLoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) {
userService.updateUserLogin(user.getId(), ServletUtils.getClientIP());
userService.updateUserLogin(user.getId(), getClientIP());
}
}
@ -202,15 +284,78 @@ public class SysAuthServiceImpl implements SysAuthService {
this.createLogoutLog(loginUser.getId(), loginUser.getUsername());
}
@Override
public void updatePassword(Long userId, @Valid MbrAuthUpdatePasswordReqVO reqVO) {
// 检验旧密码
MbrUserDO userDO = checkOldPassword(userId, reqVO.getOldPassword());
// 更新用户密码
// TODO @宋天:不要更新整个对象哈
userDO.setPassword(passwordEncoder.encode(reqVO.getPassword()));
userMapper.updateById(userDO);
}
@Override
public void resetPassword(MbrAuthResetPasswordReqVO reqVO) {
// 根据验证码取出手机号,并查询用户
String mobile = stringRedisTemplate.opsForValue().get(reqVO.getCode());
MbrUserDO userDO = userMapper.selectByMobile(mobile);
if (userDO == null){
throw exception(USER_NOT_EXISTS);
}
// TODO @芋艿 这一步没必要检验验证码与手机是否匹配因为是根据验证码去redis中查找手机号然后根据手机号查询用户
// 也就是说 即便黑客以其他方式将验证码发送到自己手机上,最终还是会根据手机号查询用户然后进行重置密码的操作,不存在安全问题
// TODO @宋天:这块微信在讨论下哈~~~
// 校验验证码
smsCodeService.useSmsCode(userDO.getMobile(), SysSmsSceneEnum.FORGET_MOBILE_BY_SMS.getScene(), reqVO.getCode(),getClientIP());
// 更新密码
userDO.setPassword(passwordEncoder.encode(reqVO.getPassword()));
userMapper.updateById(userDO);
}
@Override
public void checkIfMobileMatchCodeAndDeleteCode(String phone, String code) {
// 检验用户手机与验证码是否匹配
String mobile = stringRedisTemplate.opsForValue().get(code);
if (!phone.equals(mobile)){
throw exception(USER_CODE_FAILED);
}
// 销毁redis中此验证码
stringRedisTemplate.delete(code);
}
/**
* 校验旧密码
*
* @param id 用户 id
* @param oldPassword 旧密码
* @return MbrUserDO 用户实体
*/
@VisibleForTesting
public MbrUserDO checkOldPassword(Long id, String oldPassword) {
MbrUserDO user = userMapper.selectById(id);
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
// 参数:未加密密码,编码后的密码
if (!passwordEncoder.matches(oldPassword,user.getPassword())) {
throw exception(USER_PASSWORD_FAILED);
}
return user;
}
private void createLogoutLog(Long userId, String username) {
SysLoginLogCreateReqDTO reqDTO = new SysLoginLogCreateReqDTO();
reqDTO.setLogType(SysLoginLogTypeEnum.LOGOUT_SELF.getType());
reqDTO.setTraceId(TracerUtils.getTraceId());
reqDTO.setUserId(userId);
reqDTO.setUserType(UserTypeEnum.MEMBER.getValue());
reqDTO.setUserType(USER_TYPE_ENUM.getValue());
reqDTO.setUsername(username);
reqDTO.setUserAgent(ServletUtils.getUserAgent());
reqDTO.setUserIp(ServletUtils.getClientIP());
reqDTO.setUserIp(getClientIP());
reqDTO.setResult(SysLoginResultEnum.SUCCESS.getResult());
loginLogCoreService.createLoginLog(reqDTO);
}

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.userserver.modules.system.service.sms;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.validation.Mobile;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthSendSmsReqVO;
import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
/**
@ -20,6 +21,12 @@ public interface SysSmsCodeService {
*/
void sendSmsCode(@Mobile String mobile, Integer scene, String createIp);
/**
* 发送短信验证码,并检测手机号是否已被注册
* @param reqVO 请求实体
*/
void sendSmsNewCode(SysAuthSendSmsReqVO reqVO);
/**
* 验证短信验证码,并进行使用
* 如果正确,则将验证码标记成已使用
@ -32,4 +39,9 @@ public interface SysSmsCodeService {
*/
void useSmsCode(@Mobile String mobile, Integer scene, String code, String usedIp);
/**
* 根据用户id发送验证码
* @param userId 用户id
*/
void sendSmsCodeLogin(Long userId);
}

View File

@ -1,20 +1,27 @@
package cn.iocoder.yudao.userserver.modules.system.service.sms.impl;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
import cn.iocoder.yudao.coreservice.modules.system.service.sms.SysSmsCoreService;
import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthSendSmsReqVO;
import cn.iocoder.yudao.userserver.modules.system.dal.dataobject.sms.SysSmsCodeDO;
import cn.iocoder.yudao.userserver.modules.system.dal.mysql.sms.SysSmsCodeMapper;
import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsTemplateCodeConstants;
import cn.iocoder.yudao.userserver.modules.system.framework.sms.SmsCodeProperties;
import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import static cn.hutool.core.util.RandomUtil.randomInt;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConstants.*;
/**
@ -26,22 +33,52 @@ import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConst
@Validated
public class SysSmsCodeServiceImpl implements SysSmsCodeService {
/**
* 验证码 + 手机 在redis中存储的有效时间单位分钟
*/
private static final Long CODE_TIME = 10L;
@Resource
private SmsCodeProperties smsCodeProperties;
@Resource
private SysSmsCodeMapper smsCodeMapper;
@Resource
private MbrUserService mbrUserService;
@Resource
private SysSmsCoreService smsCoreService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void sendSmsCode(String mobile, Integer scene, String createIp) {
// 创建验证码
String code = this.createSmsCode(mobile, scene, createIp);
// 发送验证码
// TODO @宋天:这里可以拓展下 SysSmsSceneEnum支持设置对应的短信模板编号不同场景的短信文案是不同的)、是否要校验手机号已经注册。这样 Controller 就可以收口成一个接口了。相当于说,不同场景,不同策略
smsCoreService.sendSingleSmsToMember(mobile, null, SysSmsTemplateCodeConstants.USER_SMS_LOGIN,
MapUtil.of("code", code));
// 存储手机号与验证码到redis用于标记
// TODO @宋天SysSmsCodeDO 表应该足够,无需增加额外的 redis 存储哇
// TODO @宋天Redis 相关的操作,不要散落到业务层,而是写一个它对应的 RedisDAO。这样实现业务与技术的解耦
// TODO @宋天:直接使用 code 作为 key会存在 2 个问题1code 可能会冲突多个手机号之间2缺少前缀。例如说 sms_code_${code}
stringRedisTemplate.opsForValue().set(code,mobile,CODE_TIME, TimeUnit.MINUTES);
}
@Override
public void sendSmsNewCode(SysAuthSendSmsReqVO reqVO) {
// 检测手机号是否已被使用
MbrUserDO userByMobile = mbrUserService.getUserByMobile(reqVO.getMobile());
if (userByMobile != null){
throw exception(USER_SMS_CODE_IS_EXISTS);
}
// 发送短信
this.sendSmsCode(reqVO.getMobile(),reqVO.getScene(),getClientIP());
}
private String createSmsCode(String mobile, Integer scene, String ip) {
@ -91,4 +128,14 @@ public class SysSmsCodeServiceImpl implements SysSmsCodeService {
.used(true).usedTime(new Date()).usedIp(usedIp).build());
}
@Override
public void sendSmsCodeLogin(Long userId) {
MbrUserDO user = mbrUserService.getUser(userId);
if (user == null){
throw exception(USER_NOT_EXISTS);
}
// 发送验证码
this.sendSmsCode(user.getMobile(),SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene(), getClientIP());
}
}

View File

@ -63,6 +63,11 @@ spring:
--- #################### 定时任务相关配置 ####################
# Quartz 配置项,对应 QuartzProperties 配置类
spring:
quartz:
auto-startup: false # 无需使用 Quartz交给 yudao-admin-server 环境
--- #################### 配置中心相关配置 ####################
# Apollo 配置中心
@ -141,3 +146,22 @@ yudao:
pay:
pay-notify-url: http://niubi.natapp1.cc/api/pay/order/notify
refund-notify-url: http://niubi.natapp1.cc/api/pay/refund/notify
justauth:
enabled: true
type:
WECHAT_MP: # 微信公众平台 - 移动端 H5 https://www.yuque.com/docs/share/a795bef6-ee8a-494a-8dc4-2ef41927743b?#%20%E3%80%8A%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E6%B5%8B%E8%AF%95%E3%80%8B
client-id: wxa5a05b85ac361f96
client-secret: 247073c7cebb67f27f0e769195c2a57e
ignore-check-redirect-uri: true
WECHAT_MINI_PROGRAM: # 微信小程序 https://www.yuque.com/docs/share/88e3d30a-6830-45fc-8c25-dae485aef3aa?#%20%E3%80%8A%E5%B0%8F%E7%A8%8B%E5%BA%8F%E6%8E%88%E6%9D%83%E7%99%BB%E5%BD%95%E3%80%8B
client-id: wx44d047d87e6284d8
client-secret: 21c3b7a8a51ee1b8f5cf875848ed4466
ignore-check-redirect-uri: true
ignore-check-state: true
cache:
type: REDIS
prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟

View File

@ -63,6 +63,11 @@ spring:
--- #################### 定时任务相关配置 ####################
# Quartz 配置项,对应 QuartzProperties 配置类
spring:
quartz:
auto-startup: false # 无需使用 Quartz交给 yudao-admin-server 环境
--- #################### 配置中心相关配置 ####################
# Apollo 配置中心
@ -156,3 +161,24 @@ yudao:
pay-notify-url: http://jg6rde.natappfree.cc/api/pay/order/notify
refund-notify-url: http://jg6rde.natappfree.cc/api/pay/refund/notify
pay-return-url: http://jg6rde.natappfree.cc/api/pay/order/return
justauth:
enabled: true
type:
WECHAT_MP: # 微信公众平台 - 移动端 H5 https://www.yuque.com/docs/share/a795bef6-ee8a-494a-8dc4-2ef41927743b?#%20%E3%80%8A%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E6%B5%8B%E8%AF%95%E3%80%8B
client-id: wxa5a05b85ac361f96
client-secret: 247073c7cebb67f27f0e769195c2a57e
# client-id: wx5b23ba7a5589ecbb # TODO 芋艿:自己的测试,后续可以删除
# client-secret: 2a7b3b20c537e52e74afd395eb85f61f
ignore-check-redirect-uri: true
WECHAT_MINI_PROGRAM: # 微信小程序 https://www.yuque.com/docs/share/88e3d30a-6830-45fc-8c25-dae485aef3aa?#%20%E3%80%8A%E5%B0%8F%E7%A8%8B%E5%BA%8F%E6%8E%88%E6%9D%83%E7%99%BB%E5%BD%95%E3%80%8B
client-id: wx44d047d87e6284d8
client-secret: 21c3b7a8a51ee1b8f5cf875848ed4466
# client-id: wx63c280fe3248a3e7 # TODO 芋艿:自己的测试,后续可以删除
# client-secret: 6f270509224a7ae1296bbf1c8cb97aed
ignore-check-redirect-uri: true
ignore-check-state: true
cache:
type: REDIS
prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟

View File

@ -0,0 +1,13 @@
## 微信公众号
参考文章https://www.yuque.com/docs/share/0e2966dd-89f8-4b69-980d-b876168725df
① 访问 social-login.html 选择【微信公众号】
② 微信公众号授权完成后,跳转回 social-login2.html输入手机号 + 密码,进行绑定
## 微信小程序
参考文章https://www.yuque.com/docs/share/88e3d30a-6830-45fc-8c25-dae485aef3aa
① 暂时使用 mini-program-test 项目

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
<title>社交登陆测试页</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
</head>
<body>
<div>点击如下按钮,发起登陆的测试</div>
<div>
<button id="wx_pub">微信公众号</button>
</div>
</body>
<script>
// let server = 'http://127.0.0.1:28080';
let server = 'http://192.168.1.2:28080';
// 微信公众号
$( "#wx_pub").on( "click", function() {
// 获得授权链接
$.ajax({
url: server + "/api/social-auth-redirect?type=31&redirectUri=" +
encodeURIComponent(server + '/static/social-login2.html'), //重定向地址
method: 'GET',
success: function( result ) {
if (result.code !== 0) {
alert('获得授权链接失败,原因:' + result.msg)
return;
}
// 跳转重定向
document.location.href = result.data;
}
})
});
</script>
</html>

View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
<title>社交登陆测试页</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
</head>
<body>
<div>点击如下按钮,授权登录</div>
<div>
手机号<input id="mobile" value="15601691300"><br>
手机验证码<input id="smsCode">
<button id="send_sms_code">发送手机验证码</button>
<br>
<button id="wx_pub">微信公众号授权登录</button>
</div>
</body>
<script>
// let server = 'http://127.0.0.1:28080';
let server = 'http://192.168.1.2:28080';
let type = 31; //登录类型 微信公众号
// 微信公众号
$("#wx_pub").on("click", function () {
let code = getUrlParam("code"); // 访问授权连接后会回调本页面地址参数在本页面url后面
let state = getUrlParam("state");
console.log("获取code: " + code + ", state: " + state)
let data = {
'mobile': $('#mobile').val(),
'smsCode': $('#smsCode').val(),
'code': code,
'state': state,
'type': type
}
// 调用授权登录接口
$.ajax({
url: server + "/api/social-login2",
method: 'POST',
data: JSON.stringify(data),
contentType: "application/json;charset=utf-8",
dataType: "json",
success: function( result ) {
if (result.code !== 0) {
alert('调用授权登录接口失败,原因:' + result.msg)
return;
}
alert("授权登录成功, token: "+result.data.token)
}
})
});
// 发送手机验证码
$("#send_sms_code").on("click", function () {
let data = {
'mobile': $('#mobile').val(),
'scene': 1 // 手机号登陆 类型
}
$.ajax({
url: server + "/api/send-sms-code",
method: 'POST',
data: JSON.stringify(data),
contentType: "application/json;charset=utf-8",
dataType: "json",
success: function (result) {
if (result.code !== 0) {
alert('发送手机验证码失败,原因:' + result.msg)
return;
}
alert("发送成功, 请查看日志");
}
})
})
//获取url中的参数
function getUrlParam(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); //构造一个含有目标参数的正则表达式对象
var r = window.location.search.substr(1).match(reg); //匹配目标参数
if (r != null) return unescape(r[2]);
return null; //返回参数值
}
</script>
</html>

View File

@ -0,0 +1,48 @@
package cn.iocoder.yudao.userserver;
import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import cn.iocoder.yudao.userserver.config.RedisTestConfiguration;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import org.redisson.spring.starter.RedissonAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
/**
* 依赖内存 DB + Redis 的单元测试
*
* 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis
*
* @author 芋道源码
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class)
@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
public class BaseDbAndRedisUnitTest {
@Import({
// DB 配置类
YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类
DataSourceAutoConfiguration.class, // Spring DB 自动配置类
DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
DruidDataSourceAutoConfigure.class, // Druid 自动配置类
// MyBatis 配置类
YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
// Redis 配置类
RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
RedisAutoConfiguration.class, // Spring Redis 自动配置类
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedissonAutoConfiguration.class, // Redisson 自动高配置类
})
public static class Application {
}
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.userserver.config;
import com.github.fppt.jedismock.RedisServer;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import java.io.IOException;
@Configuration(proxyBeanMethods = false)
@Lazy(false) // 禁止延迟加载
@EnableConfigurationProperties(RedisProperties.class)
public class RedisTestConfiguration {
/**
* 创建模拟的 Redis Server 服务器
*/
@Bean
public RedisServer redisServer(RedisProperties properties) throws IOException {
RedisServer redisServer = new RedisServer(properties.getPort());
// TODO 芋艿:一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样就导致端口被占用无法启动。。。
try {
redisServer.start();
} catch (Exception ignore) {}
return redisServer;
}
}

View File

@ -0,0 +1,62 @@
package cn.iocoder.yudao.userserver.modules.member.controller;
import cn.iocoder.yudao.userserver.modules.member.controller.user.SysUserProfileController;
import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* {@link SysUserProfileController} 的单元测试类
*
* @author 宋天
*/
// TODO @宋天controller 的单测可以不写哈,因为收益太低了。未来我们做 qa 自动化测试
public class SysUserProfileControllerTest {
private MockMvc mockMvc;
@InjectMocks
private SysUserProfileController sysUserProfileController;
@Mock
private MbrUserService userService;
@Mock
private SysSmsCodeService smsCodeService;
@Before // TODO @宋天:使用 junit5 哈
public void setup() {
// 初始化
MockitoAnnotations.openMocks(this);
// 构建mvc环境
mockMvc = MockMvcBuilders.standaloneSetup(sysUserProfileController).build();
}
@Test
@Ignore
public void testUpdateMobile_success() throws Exception {
//模拟接口调用
this.mockMvc.perform(post("/system/user/profile/update-mobile")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content("{\"mobile\":\"15819844280\",\"code\":\"123456\"}}"))
.andExpect(status().isOk())
.andDo(MockMvcResultHandlers.print());
// TODO @宋天:方法的结尾,不用空行哈
}
}

View File

@ -2,51 +2,64 @@ package cn.iocoder.yudao.userserver.modules.member.service;
import cn.iocoder.yudao.coreservice.modules.infra.service.file.InfFileCoreService;
import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.user.SysUserDO;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
import cn.iocoder.yudao.userserver.BaseDbUnitTest;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import cn.iocoder.yudao.userserver.BaseDbAndRedisUnitTest;
import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserInfoRespVO;
import cn.iocoder.yudao.userserver.modules.member.controller.user.vo.MbrUserUpdateMobileReqVO;
import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper;
import cn.iocoder.yudao.userserver.modules.member.service.user.impl.MbrUserServiceImpl;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.SysAuthSendSmsReqVO;
import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
import cn.iocoder.yudao.userserver.modules.system.service.auth.impl.SysAuthServiceImpl;
import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;
import javax.annotation.Resource;
import java.io.*;
import java.io.ByteArrayInputStream;
import java.util.function.Consumer;
import static cn.hutool.core.util.RandomUtil.randomBytes;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static cn.hutool.core.util.RandomUtil.randomEle;
import static cn.hutool.core.util.RandomUtil.*;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
// TODO @芋艿:单测的 review等逻辑都达成一致后
/**
* {@link MbrUserServiceImpl} 的单元测试类
*
* @author 宋天
*/
@Import(MbrUserServiceImpl.class)
public class MbrUserServiceImplTest extends BaseDbUnitTest {
@Import({MbrUserServiceImpl.class, YudaoRedisAutoConfiguration.class})
public class MbrUserServiceImplTest extends BaseDbAndRedisUnitTest {
@Resource
private MbrUserServiceImpl mbrUserService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private MbrUserMapper userMapper;
@MockBean
private SysAuthServiceImpl authService;
@MockBean
private InfFileCoreService fileCoreService;
@MockBean
private PasswordEncoder passwordEncoder;
@MockBean
private SysSmsCodeService sysSmsCodeService;
@Test
public void testUpdateNickName_success(){
// mock 数据
@ -94,6 +107,36 @@ public class MbrUserServiceImplTest extends BaseDbUnitTest {
assertEquals(avatar, str);
}
@Test
public void updateMobile_success(){
// mock数据
String oldMobile = randomNumbers(11);
MbrUserDO userDO = randomMbrUserDO();
userDO.setMobile(oldMobile);
userMapper.insert(userDO);
// 验证旧手机
sysSmsCodeService.sendSmsCodeLogin(userDO.getId());
// 验证旧手机验证码是否正确
sysSmsCodeService.useSmsCode(oldMobile,SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene(),"123","1.1.1.1");
// 验证新手机
SysAuthSendSmsReqVO smsReqVO = new SysAuthSendSmsReqVO();
smsReqVO.setMobile(oldMobile);
smsReqVO.setScene(SysSmsSceneEnum.CHANGE_MOBILE_BY_SMS.getScene());
sysSmsCodeService.sendSmsNewCode(smsReqVO);
// 更新手机号
String newMobile = randomNumbers(11);
String code = randomNumbers(4);
MbrUserUpdateMobileReqVO reqVO = new MbrUserUpdateMobileReqVO();
reqVO.setMobile(newMobile);
reqVO.setCode(code);
mbrUserService.updateMobile(userDO.getId(),reqVO);
assertEquals(mbrUserService.getUser(userDO.getId()).getMobile(),newMobile);
}
// ========== 随机对象 ==========
@SafeVarargs

View File

@ -0,0 +1,68 @@
package cn.iocoder.yudao.userserver.modules.system.controller;
import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialCoreService;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.SysAuthController;
import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService;
import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* {@link SysAuthController} 的单元测试类
*
* @author 宋天
*/
public class SysAuthControllerTest {
private MockMvc mockMvc;
@InjectMocks
private SysAuthController sysAuthController;
@Mock
private SysAuthService authService;
@Mock
private SysSmsCodeService smsCodeService;
@Mock
private SysSocialCoreService socialService;
@Before
public void setup() {
// 初始化
MockitoAnnotations.openMocks(this);
// 构建mvc环境
mockMvc = MockMvcBuilders.standaloneSetup(sysAuthController).build();
}
@Test
public void testResetPassword_success() throws Exception {
// 模拟接口调用
this.mockMvc.perform(post("/reset-password")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"password\":\"1123\",\"code\":\"123456\"}}"))
.andExpect(status().isOk())
.andDo(MockMvcResultHandlers.print());
}
@Test
public void testUpdatePassword_success() throws Exception {
// 模拟接口调用
this.mockMvc.perform(post("/update-password")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"password\":\"1123\",\"code\":\"123456\",\"oldPassword\":\"1123\"}}"))
.andExpect(status().isOk())
.andDo(MockMvcResultHandlers.print());
}
}

View File

@ -0,0 +1,130 @@
package cn.iocoder.yudao.userserver.modules.system.service;
import cn.iocoder.yudao.coreservice.modules.member.dal.dataobject.user.MbrUserDO;
import cn.iocoder.yudao.coreservice.modules.system.service.auth.SysUserSessionCoreService;
import cn.iocoder.yudao.coreservice.modules.system.service.logger.SysLoginLogCoreService;
import cn.iocoder.yudao.coreservice.modules.system.service.social.SysSocialCoreService;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import cn.iocoder.yudao.userserver.BaseDbAndRedisUnitTest;
import cn.iocoder.yudao.userserver.modules.member.dal.mysql.user.MbrUserMapper;
import cn.iocoder.yudao.userserver.modules.member.service.user.MbrUserService;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.MbrAuthResetPasswordReqVO;
import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.MbrAuthUpdatePasswordReqVO;
import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService;
import cn.iocoder.yudao.userserver.modules.system.service.auth.impl.SysAuthServiceImpl;
import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static cn.hutool.core.util.RandomUtil.randomEle;
import static cn.hutool.core.util.RandomUtil.randomNumbers;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
// TODO @芋艿:单测的 review等逻辑都达成一致后
/**
* {@link SysAuthService} 的单元测试类
*
* @author 宋天
*/
@Import({SysAuthServiceImpl.class, YudaoRedisAutoConfiguration.class})
public class SysAuthServiceTest extends BaseDbAndRedisUnitTest {
@MockBean
private AuthenticationManager authenticationManager;
@MockBean
private MbrUserService userService;
@MockBean
private SysSmsCodeService smsCodeService;
@MockBean
private SysLoginLogCoreService loginLogCoreService;
@MockBean
private SysUserSessionCoreService userSessionCoreService;
@MockBean
private SysSocialCoreService socialService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@MockBean
private PasswordEncoder passwordEncoder;
@Resource
private MbrUserMapper mbrUserMapper;
@Resource
private SysAuthServiceImpl authService;
@Test
public void testUpdatePassword_success(){
// 准备参数
MbrUserDO userDO = randomMbrUserDO();
mbrUserMapper.insert(userDO);
// 新密码
String newPassword = randomString();
// 请求实体
MbrAuthUpdatePasswordReqVO reqVO = MbrAuthUpdatePasswordReqVO.builder()
.oldPassword(userDO.getPassword())
.password(newPassword)
.build();
// 测试桩
// 这两个相等是为了返回ture这个结果
when(passwordEncoder.matches(reqVO.getOldPassword(),reqVO.getOldPassword())).thenReturn(true);
when(passwordEncoder.encode(newPassword)).thenReturn(newPassword);
// 更新用户密码
authService.updatePassword(userDO.getId(),reqVO);
assertEquals(mbrUserMapper.selectById(userDO.getId()).getPassword(),newPassword);
}
@Test
public void testResetPassword_success(){
// 准备参数
MbrUserDO userDO = randomMbrUserDO();
mbrUserMapper.insert(userDO);
// 随机密码
String password = randomNumbers(11);
// 随机验证码
String code = randomNumbers(4);
MbrAuthResetPasswordReqVO reqVO = MbrAuthResetPasswordReqVO.builder()
.password(password)
.code(code)
.build();
// 放入code+手机号
stringRedisTemplate.opsForValue().set(code,userDO.getMobile(),10, TimeUnit.MINUTES);
// mock
when(passwordEncoder.encode(password)).thenReturn(password);
// 更新用户密码
authService.resetPassword(reqVO);
assertEquals(mbrUserMapper.selectById(userDO.getId()).getPassword(),password);
}
// ========== 随机对象 ==========
@SafeVarargs
private static MbrUserDO randomMbrUserDO(Consumer<MbrUserDO>... consumers) {
Consumer<MbrUserDO> consumer = (o) -> {
o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
o.setPassword(randomString());
};
return randomPojo(MbrUserDO.class, ArrayUtils.append(consumer, consumers));
}
}

View File

@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS "mbr_user" (
"updater" varchar(64) NULL DEFAULT '' COMMENT '更新者',
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
"deleted" bit(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
"tenant_id" bigint not null default '0',
PRIMARY KEY ("id")
) COMMENT '会员表';