feat: 新增用户注册,忘记密码接口,修复第三方注册默认权限和删除报错问题

This commit is contained in:
King
2025-03-20 02:43:21 +00:00
committed by Charles7c
parent 603b12d10d
commit 94b093e9d4
20 changed files with 193 additions and 33 deletions

View File

@@ -74,6 +74,11 @@ public class CaptchaProperties {
*/
@Data
public static class CaptchaSms {
/**
* 万能验证码
*/
private String code;
/**
* 内容长度
*/

View File

@@ -54,6 +54,11 @@ public class SysConstants {
*/
public static final String SUPER_ROLE_CODE = "admin";
/**
* 普通角色编码
*/
public static final String GENERAL_ROLE_CODE = "general";
/**
* 超管角色 ID
*/

View File

@@ -60,6 +60,7 @@ public class AccountLoginHandler extends AbstractLoginHandler<AccountLoginReq> {
// 验证用户名密码
String username = req.getUsername();
UserDO user = userService.getByUsername(username);
ValidationUtils.throwIfNull(user, "用户不存在");
boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(rawPassword, user.getPassword());
// 检查账号锁定状态
this.checkUserLocked(req.getUsername(), request, isError);

View File

@@ -17,11 +17,14 @@
package top.continew.admin.auth.handler;
import jakarta.servlet.http.HttpServletRequest;
import jodd.util.StringUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import top.continew.admin.auth.AbstractLoginHandler;
import top.continew.admin.auth.enums.AuthTypeEnum;
import top.continew.admin.auth.model.req.EmailLoginReq;
import top.continew.admin.auth.model.resp.LoginResp;
import top.continew.admin.common.config.properties.CaptchaProperties;
import top.continew.admin.common.constant.CacheConstants;
import top.continew.admin.system.model.entity.user.UserDO;
import top.continew.admin.system.model.resp.ClientResp;
@@ -36,7 +39,9 @@ import top.continew.starter.core.validation.ValidationUtils;
* @since 2024/12/22 14:58
*/
@Component
@RequiredArgsConstructor
public class EmailLoginHandler extends AbstractLoginHandler<EmailLoginReq> {
private final CaptchaProperties captchaProperties;
@Override
public LoginResp login(EmailLoginReq req, ClientResp client, HttpServletRequest request) {
@@ -53,11 +58,19 @@ public class EmailLoginHandler extends AbstractLoginHandler<EmailLoginReq> {
@Override
public void preLogin(EmailLoginReq req, ClientResp client, HttpServletRequest request) {
String email = req.getEmail();
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email;
String captcha = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR);
RedisUtils.delete(captchaKey);
// String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email;
// String captcha = RedisUtils.get(captchaKey);
// ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
// ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR);
// RedisUtils.delete(captchaKey);
String captcha = req.getCaptcha();
if (!StringUtil.equals(captcha, captchaProperties.getSms().getCode())) {
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email;
String captcha1 = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
ValidationUtils.throwIfNotEqualIgnoreCase(captcha1, captcha, CAPTCHA_ERROR);
RedisUtils.delete(captchaKey);
}
}
@Override

View File

@@ -17,11 +17,14 @@
package top.continew.admin.auth.handler;
import jakarta.servlet.http.HttpServletRequest;
import jodd.util.StringUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import top.continew.admin.auth.AbstractLoginHandler;
import top.continew.admin.auth.enums.AuthTypeEnum;
import top.continew.admin.auth.model.req.PhoneLoginReq;
import top.continew.admin.auth.model.resp.LoginResp;
import top.continew.admin.common.config.properties.CaptchaProperties;
import top.continew.admin.common.constant.CacheConstants;
import top.continew.admin.system.model.entity.user.UserDO;
import top.continew.admin.system.model.resp.ClientResp;
@@ -36,8 +39,11 @@ import top.continew.starter.core.validation.ValidationUtils;
* @since 2024/12/22 14:59
*/
@Component
@RequiredArgsConstructor
public class PhoneLoginHandler extends AbstractLoginHandler<PhoneLoginReq> {
private final CaptchaProperties captchaProperties;
@Override
public LoginResp login(PhoneLoginReq req, ClientResp client, HttpServletRequest request) {
// 验证手机号
@@ -53,11 +59,19 @@ public class PhoneLoginHandler extends AbstractLoginHandler<PhoneLoginReq> {
@Override
public void preLogin(PhoneLoginReq req, ClientResp client, HttpServletRequest request) {
String phone = req.getPhone();
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone;
String captcha = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR);
RedisUtils.delete(captchaKey);
// String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone;
// String captcha = RedisUtils.get(captchaKey);
// ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
// ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR);
// RedisUtils.delete(captchaKey);
String captcha = req.getCaptcha();
if (!StringUtil.equals(captcha, captchaProperties.getSms().getCode())) {
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone;
String captcha1 = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha1, CAPTCHA_EXPIRED);
ValidationUtils.throwIfNotEqualIgnoreCase(captcha, captcha1, CAPTCHA_ERROR);
RedisUtils.delete(captchaKey);
}
}
@Override

View File

@@ -110,7 +110,8 @@ public class SocialLoginHandler extends AbstractLoginHandler<SocialLoginReq> {
user.setStatus(DisEnableStatusEnum.ENABLE);
userService.save(user);
Long userId = user.getId();
RoleDO role = roleService.getByCode(SysConstants.SUPER_ROLE_CODE);
// RoleDO role = roleService.getByCode(SysConstants.SUPER_ROLE_CODE);
RoleDO role = roleService.getByCode(SysConstants.GENERAL_ROLE_CODE);
userRoleService.assignRolesToUser(Collections.singletonList(role.getId()), userId);
userSocial = new UserSocialDO();
userSocial.setUserId(userId);

View File

@@ -49,8 +49,8 @@ public class PhoneLoginReq extends LoginReq {
/**
* 验证码
*/
@Schema(description = "验证码", example = "8888")
@Schema(description = "验证码", example = "888888")
@NotBlank(message = "验证码不能为空")
@Length(max = 4, message = "验证码非法")
@Length(max = 6, message = "验证码非法")
private String captcha;
}

View File

@@ -63,7 +63,7 @@ public interface UserMapper extends DataPermissionMapper<UserDO> {
* @param username 用户名
* @return 用户信息
*/
@Select("SELECT * FROM sys_user WHERE username = #{username}")
@Select("SELECT * FROM sys_user WHERE username =#{username} OR nickname = #{username}")
UserDO selectByUsername(@Param("username") String username);
/**

View File

@@ -16,6 +16,7 @@
package top.continew.admin.system.mapper.user;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import top.continew.admin.system.model.entity.user.UserSocialDO;
import top.continew.starter.data.mp.base.BaseMapper;

View File

@@ -16,9 +16,13 @@
package top.continew.admin.system.model.req.user;
import cn.hutool.core.lang.RegexPool;
import org.hibernate.validator.constraints.Length;
import top.continew.admin.common.constant.RegexConstants;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import jakarta.validation.constraints.Pattern;
import java.io.Serial;
import java.io.Serializable;
@@ -36,10 +40,35 @@ public class UserPasswordUpdateReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户名
*/
@Schema(description = "用户名", example = "zhangsan")
// @NotBlank(message = "用户名不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "用户名长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String username;
/**
* 邮箱
*/
@Schema(description = "邮箱", example = "123456789@qq.com")
// @NotBlank(message = "邮箱不能为空")
@Pattern(regexp = RegexPool.EMAIL, message = "邮箱格式错误")
private String email;
/**
* 验证码
*/
@Schema(description = "验证码", example = "888888")
// @NotBlank(message = "验证码不能为空")
@Length(max = 6, message = "验证码非法")
private String captcha;
/**
* 当前密码(加密)
*/
@Schema(description = "当前密码(加密)", example = "E7c72TH+LDxKTwavjM99W1MdI9Lljh79aPKiv3XB9MXcplhm7qJ1BJCj28yaflbdVbfc366klMtjLIWQGqb0qw==")
// @NotBlank(message = "当前密码不能为空")
private String oldPassword;
/**

View File

@@ -50,9 +50,9 @@ public class UserPhoneUpdateReq implements Serializable {
/**
* 验证码
*/
@Schema(description = "验证码", example = "8888")
@Schema(description = "验证码", example = "888888")
@NotBlank(message = "验证码不能为空")
@Length(max = 4, message = "验证码非法")
@Length(max = 6, message = "验证码非法")
private String captcha;
/**

View File

@@ -48,7 +48,8 @@ public class UserReq implements Serializable {
*/
@Schema(description = "用户名", example = "zhangsan")
@NotBlank(message = "用户名不能为空")
@Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头")
// @Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "用户名长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String username;
/**
@@ -115,4 +116,16 @@ public class UserReq implements Serializable {
*/
@Schema(description = "状态", example = "1")
private DisEnableStatusEnum status;
/**
* 验证码
*/
@Schema(description = "验证码", example = "ABCD")
private String captcha;
/**
* 验证码标识
*/
@Schema(description = "验证码标识", example = "090b9a2c-1691-4fca-99db-e4ed0cff362f")
private String uuid;
}

View File

@@ -61,6 +61,13 @@ public interface UserSocialService {
*/
void bind(AuthUser authUser, Long userId);
/**
* 根据用户 ID 删除
*
* @param userIds 用户 ID 列表
*/
void deleteByUserIds(List<Long> userIds);
/**
* 根据来源和用户 ID 删除
*

View File

@@ -116,6 +116,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
private final RoleService roleService;
private final FileService fileService;
private final FileStorageService fileStorageService;
private final UserSocialService userSocialService;
@Resource
private DeptService deptService;
@@ -209,6 +210,8 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
userRoleService.deleteByUserIds(ids);
// 删除历史密码
userPasswordHistoryService.deleteByUserIds(ids);
// 删除用户绑定第三方信息
userSocialService.deleteByUserIds(ids);
// 删除用户
super.delete(ids);
// 踢出在线用户
@@ -418,11 +421,13 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
@Override
@Transactional(rollbackFor = Exception.class)
public void updatePassword(String oldPassword, String newPassword, Long id) {
CheckUtils.throwIfEqual(newPassword, oldPassword, "新密码不能与当前密码相同");
UserDO user = super.getById(id);
String password = user.getPassword();
if (StrUtil.isNotBlank(password)) {
CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, password), "当前密码错误");
if (StrUtil.isNotBlank(oldPassword)) {
CheckUtils.throwIfEqual(newPassword, oldPassword, "新密码不能与当前密码相同");
if (StrUtil.isNotBlank(password)) {
CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, password), "当前密码错误");
}
}
// 校验密码合法性
int passwordRepetitionTimes = this.checkPassword(newPassword, user);
@@ -440,10 +445,12 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
@Override
public void updatePhone(String newPhone, String oldPassword, Long id) {
UserDO user = super.getById(id);
CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, user.getPassword()), "当前密码错误");
CheckUtils.throwIf(this.isPhoneExists(newPhone, id), "手机号已绑定其他账号,请更换其他手机号");
CheckUtils.throwIfEqual(newPhone, user.getPhone(), "新手机号不能与当前手机号相同");
if (StrUtil.isNotBlank(oldPassword)) {
UserDO user = super.getById(id);
CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, user.getPassword()), "当前密码错误");
CheckUtils.throwIfEqual(newPhone, user.getPhone(), "新手机号不能与当前手机号相同");
}
// 更新手机号
baseMapper.lambdaUpdate().set(UserDO::getPhone, newPhone).eq(UserDO::getId, id).update();
}

View File

@@ -86,6 +86,10 @@ public class UserSocialServiceImpl implements UserSocialService {
baseMapper.insert(userSocial);
}
public void deleteByUserIds(List<Long> userIds) {
baseMapper.lambdaUpdate().in(UserSocialDO::getUserId, userIds).remove();
}
@Override
public void deleteBySourceAndUserId(String source, Long userId) {
baseMapper.lambdaUpdate().eq(UserSocialDO::getSource, source).eq(UserSocialDO::getUserId, userId).remove();

View File

@@ -26,6 +26,7 @@ import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.x.file.storage.spring.EnableFileStorage;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
@@ -52,6 +53,7 @@ import top.continew.starter.web.model.R;
@RestController
@SpringBootApplication
@RequiredArgsConstructor
@MapperScan("top.continew.admin.system.mapper")
public class ContiNewAdminApplication implements ApplicationRunner {
private final ProjectProperties projectProperties;

View File

@@ -24,24 +24,26 @@ import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import jodd.util.StringUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import top.continew.admin.common.config.properties.CaptchaProperties;
import top.continew.admin.common.constant.CacheConstants;
import top.continew.admin.common.controller.BaseController;
import top.continew.admin.common.constant.RegexConstants;
import top.continew.admin.common.util.SecureUtils;
import top.continew.admin.system.model.entity.UserDO;
import top.continew.admin.system.model.query.UserQuery;
import top.continew.admin.system.model.req.user.UserImportReq;
import top.continew.admin.system.model.req.user.UserPasswordResetReq;
import top.continew.admin.system.model.req.user.UserReq;
import top.continew.admin.system.model.req.user.UserRoleUpdateReq;
import top.continew.admin.system.model.req.user.*;
import top.continew.admin.system.model.resp.user.UserDetailResp;
import top.continew.admin.system.model.resp.user.UserImportParseResp;
import top.continew.admin.system.model.resp.user.UserImportResp;
import top.continew.admin.system.model.resp.user.UserResp;
import top.continew.admin.system.service.UserService;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.util.ExceptionUtils;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
@@ -65,6 +67,56 @@ import java.io.IOException;
Api.EXPORT})
public class UserController extends BaseController<UserService, UserResp, UserDetailResp, UserQuery, UserReq> {
private final UserService userService;
private final CaptchaProperties captchaProperties;
@Operation(summary = "用户注册", description = "用户注册")
@PostMapping(value = "/signup")
public BaseIdResp<Long> signup(@Validated(CrudValidationGroup.Add.class) @RequestBody UserReq req) {
String captcha = req.getCaptcha();
if (!StringUtil.equals(captcha, captchaProperties.getSms().getCode())) {
String key = StringUtil.isNotBlank(req.getUuid())
? req.getUuid()
: StringUtil.isNotBlank(req.getPhone())
? req.getPhone()
: StringUtil.isNotBlank(req.getEmail()) ? req.getEmail() : "";
ValidationUtils.throwIfBlank(captcha, "验证码不能为空");
ValidationUtils.throwIfBlank(key, "验证码标识不能为空");
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + key;
String captcha1 = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha1, "验证码已失效");
ValidationUtils.throwIfNotEqualIgnoreCase(captcha, captcha1, "验证码错误");
// RedisUtils.delete(captchaKey);
}
String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword()));
ValidationUtils.throwIfNull(rawPassword, "密码解密失败");
ValidationUtils.throwIf(!ReUtil
.isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字");
req.setPassword(rawPassword);
return super.add(req);
}
@Operation(summary = "修改密码", description = "修改用户登录密码")
@PostMapping("/password")
public void updatePassword(@Validated @RequestBody UserPasswordUpdateReq updateReq) {
String captcha = updateReq.getCaptcha();
if (!StringUtil.equals(captcha, captchaProperties.getSms().getCode())) {
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + updateReq.getEmail();
String captcha1 = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha1, "验证码已失效");
ValidationUtils.throwIfNotEqualIgnoreCase(captcha, captcha1, "验证码错误");
RedisUtils.delete(captchaKey);
}
String newPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(updateReq
.getNewPassword()));
ValidationUtils.throwIfNull(newPassword, "新密码解密失败");
ValidationUtils.throwIf(!ReUtil
.isMatch(RegexConstants.PASSWORD, newPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字");
UserDO user = userService.getByUsername(updateReq.getUsername());
ValidationUtils.throwIfEmpty(user, "用户名错误或不存在");
userService.updatePassword("", newPassword, user.getId());
}
@Override
@Operation(summary = "新增数据", description = "新增数据")
public BaseIdResp<Long> add(@Validated(CrudValidationGroup.Add.class) @RequestBody UserReq req) {

View File

@@ -12,7 +12,7 @@ server:
spring.datasource:
type: com.zaxxer.hikari.HikariDataSource
# 请务必提前创建好名为 continew_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置
url: jdbc:p6spy:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
url: jdbc:p6spy:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:continew_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&maxReconnects=10&failOverReadOnly=false
username: ${DB_USER:root}
password: ${DB_PWD:123456}
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
@@ -126,8 +126,10 @@ captcha:
templatePath: mail/captcha.ftl
## 短信验证码配置
sms:
# 万能验证码(限调试时使用)
code: 111111
# 内容长度
length: 4
length: 6
# 过期时间
expirationInMinutes: 5
# 模板 ID
@@ -232,6 +234,8 @@ sa-token.extension:
- /swagger-ui/**
- /swagger-resources/**
- /*/api-docs/**
- /system/user/signup
- /system/user/password
# 本地存储资源
- /file/**

View File

@@ -156,8 +156,9 @@ INSERT INTO `sys_role`
(`id`, `name`, `code`, `data_scope`, `description`, `sort`, `is_system`, `create_user`, `create_time`)
VALUES
(1, '系统管理员', 'admin', 1, '系统初始角色', 1, b'1', 1, NOW()),
(547888897925840927, '测试人员', 'tester', 5, NULL, 2, b'0', 1, NOW()),
(547888897925840928, '研发人员', 'developer', 4, NULL, 3, b'0', 1, NOW());
(2, '普通用户', 'general', 4, '系统初始角色', 2, b'0', 1, NOW()),
(547888897925840927, '测试人员', 'tester', 5, NULL, 3, b'0', 1, NOW()),
(547888897925840928, '研发人员', 'developer', 4, NULL, 4, b'0', 1, NOW());
-- 初始化默认用户admin/admin123test/test123
INSERT INTO `sys_user`

View File

@@ -156,8 +156,9 @@ INSERT INTO "sys_role"
("id", "name", "code", "data_scope", "description", "sort", "is_system", "create_user", "create_time")
VALUES
(1, '系统管理员', 'admin', 1, '系统初始角色', 1, true, 1, NOW()),
(547888897925840927, '测试人员', 'tester', 5, NULL, 2, false, 1, NOW()),
(547888897925840928, '研发人员', 'developer', 4, NULL, 3, false, 1, NOW());
(2, '普通用户', 'general', 4, '系统初始角色', 2, false, 1, NOW()),
(547888897925840927, '测试人员', 'tester', 5, NULL, 3, false, 1, NOW()),
(547888897925840928, '研发人员', 'developer', 4, NULL, 4, false, 1, NOW());
-- 初始化默认用户admin/admin123test/test123
INSERT INTO "sys_user"