This commit is contained in:
yuhaiming
2026-05-09 10:12:36 +08:00
parent def9b89ecd
commit 39fe8065da
80 changed files with 3883 additions and 6102 deletions

View File

@@ -15,9 +15,11 @@ import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.model.ForgotLoginBody;
import org.dromara.common.core.domain.model.LoginBody;
import org.dromara.common.core.domain.model.RegisterBody;
import org.dromara.common.core.domain.model.SocialLoginBody;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.*;
import org.dromara.common.encrypt.annotation.ApiEncrypt;
import org.dromara.common.json.utils.JsonUtils;
@@ -40,6 +42,7 @@ import org.dromara.system.service.ISysTenantService;
import org.dromara.web.domain.vo.LoginTenantVo;
import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.domain.vo.TenantListVo;
import org.dromara.web.service.ForgotPasswordService;
import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService;
import org.dromara.web.service.SysRegisterService;
@@ -75,7 +78,7 @@ public class AuthController {
private final ISysSocialService socialUserService;
private final ISysClientService clientService;
private final ScheduledExecutorService scheduledExecutorService;
private final ForgotPasswordService forgotPasswordService;
/**
* 登录方法
@@ -83,12 +86,13 @@ public class AuthController {
* @param body 登录信息
* @return 结果
*/
@ApiEncrypt
@PostMapping("/login")
public R<LoginVo> login(@RequestBody String body) {
LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
ValidatorUtils.validate(loginBody);
// 授权类型和客户端id
// 授权类型和客户端id
String clientId = loginBody.getClientId();
String grantType = loginBody.getGrantType();
SysClientVo client = clientService.queryByClientId(clientId);
@@ -114,6 +118,7 @@ public class AuthController {
return R.ok(loginVo);
}
/**
* 获取跳转URL
*
@@ -136,6 +141,7 @@ public class AuthController {
return R.ok("操作成功", authorizeUrl);
}
/**
* 前端回调绑定授权(需要token)
*
@@ -196,6 +202,7 @@ public class AuthController {
return R.ok();
}
/**
* 登录页面租户下拉框
*
@@ -240,4 +247,19 @@ public class AuthController {
return R.ok(result);
}
/**
* app用户忘记密码
*/
@ApiEncrypt
@PutMapping("/forgot")
public R<Void> forgot(@RequestBody String body) {
ForgotLoginBody loginBody = JsonUtils.parseObject(body, ForgotLoginBody.class);
boolean forgotFlag = forgotPasswordService.forgotPasswordService(loginBody);
if (!forgotFlag){
throw new UserException("忘记密码修改失败");
}
return R.ok();
}
}

View File

@@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import jakarta.validation.constraints.NotBlank;
@@ -52,6 +53,40 @@ public class CaptchaController {
private final CaptchaProperties captchaProperties;
private final MailProperties mailProperties;
/**
* 验证码
*
* @param username 用户手机号
*/
@RateLimiter(key = "#username", time = 60, count = 1)
@GetMapping("/resource/code")
public R<Void> code(@NotBlank(message = "{user.username.not.blank}") String username) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + username;
String code = RandomUtil.randomNumbers(6);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
if (Validator.isMobile(username)){
// 验证码模板id 自行处理 (查数据库或写死均可)
String templateId = "";
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put("code", code);
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
SmsResponse smsResponse = smsBlend.sendMessage(username, templateId, map);
if (!smsResponse.isSuccess()) {
log.error("验证码短信发送异常 => {}", smsResponse);
return R.fail(smsResponse.getData().toString());
}
}else {
emailCodeImpl(username);
}
return R.ok();
}
/**
* 短信验证码
*
@@ -97,7 +132,7 @@ public class CaptchaController {
@RateLimiter(key = "#email", time = 60, count = 1)
public void emailCodeImpl(String email) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
String code = RandomUtil.randomNumbers(4);
String code = RandomUtil.randomNumbers(6);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
try {
MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");

View File

@@ -0,0 +1,62 @@
package org.dromara.web.service;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.domain.model.ForgotLoginBody;
import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.mapper.SysUserMapper;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class ForgotPasswordService {
private final SysUserMapper userMapper;
public boolean forgotPasswordService(ForgotLoginBody loginBody) {
SysUserVo appUserVo =new SysUserVo();
if(Validator.isEmail(loginBody.getUsername())){
userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, loginBody.getUsername()));
}else if (Validator.isMobile(loginBody.getUsername())){
appUserVo = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, loginBody.getUsername()));
}else {
appUserVo = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, loginBody.getUsername()));
}
if (ObjectUtil.isNull(appUserVo)){
throw new UserException("账号未注册");
}
boolean validateFlag = validateSmsCode(loginBody.getUsername(), loginBody.getSmsCode());
if (!validateFlag){
throw new UserException("验证码无效");
}
appUserVo.setPassword(BCrypt.hashpw(loginBody.getPassword()));
SysUser update = MapstructUtils.convert(appUserVo, SysUser.class);
userMapper.updateById(update);
return userMapper.updateById(update) > 0;
}
/**
* 校验短信验证码
*/
private boolean validateSmsCode(String username, String smsCode) {
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + username);
if (StringUtils.isBlank(code)) {
throw new CaptchaExpireException();
}
return code.equals(smsCode);
}
}

View File

@@ -1,7 +1,9 @@
package org.dromara.web.service;
import cn.hutool.core.lang.Validator;
import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
@@ -24,6 +26,8 @@ import org.dromara.system.mapper.SysUserMapper;
import org.dromara.system.service.ISysUserService;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* 注册校验方法
*
@@ -44,24 +48,52 @@ public class SysRegisterService {
String tenantId = registerBody.getTenantId();
String username = registerBody.getUsername();
String password = registerBody.getPassword();
String code = registerBody.getCode();
// 校验用户类型是否存在
String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
boolean captchaEnabled = captchaProperties.getEnable();
// 验证码开关
if (captchaEnabled) {
validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid());
// boolean captchaEnabled = captchaProperties.getEnable();
// // 验证码开关
// if (captchaEnabled) {
// validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid());
// }
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
String captcha = RedisUtils.getCacheObject(verifyKey);
if (captcha==null){
throw new CaptchaExpireException();
}
if (!StringUtils.equalsIgnoreCase(code, captcha)){
throw new CaptchaExpireException();
}
SysUserBo sysUser = new SysUserBo();
sysUser.setUserName(username);
sysUser.setNickName(username);
// sysUser.setUserName(username);
sysUser.setNickName("用户"+new Date().getTime());
sysUser.setPassword(BCrypt.hashpw(password));
sysUser.setUserType(userType);
boolean exist;
if(Validator.isEmail(username)){
sysUser.setEmail(username);
exist = TenantHelper.dynamic(tenantId, () -> {
return userMapper.exists(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getEmail, sysUser.getEmail()));
});
}else if(Validator.isMobile(username)) {
sysUser.setPhonenumber(username);
exist = TenantHelper.dynamic(tenantId, () -> {
return userMapper.exists(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getPhonenumber, sysUser.getPhonenumber()));
});
}else {
sysUser.setUserName(username);
exist = TenantHelper.dynamic(tenantId, () -> {
return userMapper.exists(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUserName, sysUser.getUserName()));
});
}
boolean exist = TenantHelper.dynamic(tenantId, () -> {
return userMapper.exists(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUserName, sysUser.getUserName()));
});
if (exist) {
throw new UserException("user.register.save.error", username);
}

View File

@@ -2,6 +2,7 @@ package org.dromara.web.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -57,13 +58,25 @@ public class PasswordAuthStrategy implements IAuthStrategy {
String code = loginBody.getCode();
String uuid = loginBody.getUuid();
boolean captchaEnabled = captchaProperties.getEnable();
// 验证码开关
if (captchaEnabled) {
validateCaptcha(tenantId, username, code, uuid);
}
// boolean captchaEnabled = captchaProperties.getEnable();
// // 验证码开关
// if (captchaEnabled) {
// validateCaptcha(tenantId, username, code, uuid);
// }
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByUsername(username);
SysUserVo user ;
if(Validator.isEmail(username)){
user = loadUserByEmail(username);
}else if (Validator.isMobile(username)){
user = loadUserByPhone(username);
}else {
user = loadUserByUsername(username);
}
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
// 此处可根据登录用户的数据不同 自行创建 loginUser
return loginService.buildLoginUser(user);
@@ -120,4 +133,28 @@ public class PasswordAuthStrategy implements IAuthStrategy {
return user;
}
private SysUserVo loadUserByEmail(String username) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, username));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new UserException("user.not.exists", username);
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new UserException("user.blocked", username);
}
return user;
}
private SysUserVo loadUserByPhone(String username) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, username));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new UserException("user.not.exists", username);
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new UserException("user.blocked", username);
}
return user;
}
}

View File

@@ -133,17 +133,17 @@ redisson:
--- # mail 邮件发送
mail:
enabled: false
enabled: true
host: smtp.163.com
port: 465
# 是否需要用户名密码验证
auth: true
# 发送方遵循RFC-822标准
from: xxx@163.com
from: watexdevice@163.com
# 用户名注意如果使用foxmail邮箱此处user为qq号
user: xxx@163.com
user: watexdevice@163.com
# 密码注意某些邮箱需要为SMTP服务单独设置密码详情查看相关帮助
pass: xxxxxxxxxx
pass: XZVDXjRdVrjiX8N3
# 使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展。
starttlsEnable: true
# 使用SSL安全连接

View File

@@ -19,7 +19,6 @@ server:
io: 8
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
worker: 256
captcha:
# 是否启用验证码校验
enable: true
@@ -120,7 +119,7 @@ security:
# 多租户配置
tenant:
# 是否开启
enable: true
enable: false
# 排除表
excludes:
- sys_menu
@@ -169,7 +168,7 @@ mybatis-encryptor:
# api接口加密
api-decrypt:
# 是否开启全局接口加密
enabled: true
enabled: false
# AES 加密头标识
headerFlag: encrypt-key
# 响应加密公钥 非对称算法的公私钥 如SM2RSA 使用者请自行更换

View File

@@ -1,62 +1 @@
#错误消息
not.null=* 必须填写
user.jcaptcha.error=验证码错误
user.jcaptcha.expire=验证码已失效
user.not.exists=对不起, 您的账号:{0} 不存在.
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次,账户锁定{1}分钟
user.password.delete=对不起,您的账号:{0} 已被删除
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
role.blocked=角色已封禁,请联系管理员
user.logout.success=退出成功
length.not.valid=长度必须在{min}到{max}个字符之间
user.username.not.blank=用户名不能为空
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成且必须以非数字开头
user.username.length.valid=账户长度必须在{min}到{max}个字符之间
user.password.not.blank=用户密码不能为空
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
user.password.not.valid=* 5-50个字符
user.password.format.valid=密码必须包含大写字母、小写字母、数字和特殊字符
user.email.not.valid=邮箱格式错误
user.email.not.blank=邮箱不能为空
user.phonenumber.not.blank=用户手机号不能为空
user.mobile.phone.number.not.valid=手机号格式错误
user.login.success=登录成功
user.register.success=注册成功
user.register.save.error=保存用户 {0} 失败,注册账号已存在
user.register.error=注册失败,请联系系统管理人员
user.notfound=请重新登录
user.forcelogout=管理员强制退出,请重新登录
user.unknown.error=未知错误,请重新登录
auth.grant.type.error=认证权限类型错误
auth.grant.type.blocked=认证权限类型已禁用
auth.grant.type.not.blank=认证权限类型不能为空
auth.clientid.not.blank=认证客户端id不能为空
##文件上传消息
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB
upload.filename.exceed.length=上传的文件名最长{0}个字符
##权限
no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
repeat.submit.message=不允许重复提交,请稍候再试
rate.limiter.message=访问过于频繁,请稍候再试
sms.code.not.blank=短信验证码不能为空
sms.code.retry.limit.count=短信验证码输入错误{0}次
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,账户锁定{1}分钟
email.code.not.blank=邮箱验证码不能为空
email.code.retry.limit.count=邮箱验证码输入错误{0}次
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,账户锁定{1}分钟
xcx.code.not.blank=小程序[code]不能为空
social.source.not.blank=第三方登录平台[source]不能为空
social.code.not.blank=第三方登录平台[code]不能为空
social.state.not.blank=第三方登录平台[state]不能为空
##租户
tenant.number.not.blank=租户编号不能为空
tenant.not.exists=对不起, 您的租户不存在,请联系管理员
tenant.blocked=对不起,您的租户已禁用,请联系管理员
tenant.expired=对不起,您的租户已过期,请联系管理员
#错误消息