diff --git a/pom.xml b/pom.xml
index 750673b87..1e7747569 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,6 +40,7 @@
1.16.6
2.7.0
+ 1.4.0
2.3.15.Final
@@ -312,6 +313,13 @@
${ip2region.version}
+
+
+ com.warrenstrange
+ googleauth
+ ${googleauth.version}
+
+
io.undertow
undertow-core
diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
index f28024f35..2b9d36ae5 100644
--- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
+++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
@@ -5,6 +5,7 @@ import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.warrenstrange.googleauth.GoogleAuthenticator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants;
@@ -46,6 +47,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
private final CaptchaProperties captchaProperties;
private final SysLoginService loginService;
private final SysUserMapper userMapper;
+ private final GoogleAuthenticator googleAuthenticator;
@Override
public LoginVo login(String body, SysClientVo client) {
@@ -65,6 +67,13 @@ public class PasswordAuthStrategy implements IAuthStrategy {
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByUsername(username);
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
+
+ // 校验otp code
+ boolean otpAuthorize = googleAuthenticator.authorize(user.getOtpSecret(), loginBody.getOtpCode());
+ if (!otpAuthorize) {
+ throw new UserException("user.password.otp.code.error");
+ }
+
// 此处可根据登录用户的数据不同 自行创建 loginUser
return loginService.buildLoginUser(user);
});
diff --git a/ruoyi-admin/src/main/resources/i18n/messages.properties b/ruoyi-admin/src/main/resources/i18n/messages.properties
index cce11c85d..9f2bf69d2 100644
--- a/ruoyi-admin/src/main/resources/i18n/messages.properties
+++ b/ruoyi-admin/src/main/resources/i18n/messages.properties
@@ -16,6 +16,8 @@ user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,
user.username.length.valid=账户长度必须在{min}到{max}个字符之间
user.password.not.blank=用户密码不能为空
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
+user.password.otp.code.not.blank=OTP验证码不能为空
+user.password.otp.code.error=OTP验证码错误
user.password.not.valid=* 5-50个字符
user.email.not.valid=邮箱格式错误
user.email.not.blank=邮箱不能为空
diff --git a/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties b/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
index f948c4ab8..3b39e9dcf 100644
--- a/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
+++ b/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
@@ -17,6 +17,8 @@ user.username.length.valid=Account length must be between {min} and {max} charac
user.password.not.blank=Password cannot be empty
user.password.length.valid=Password length must be between {min} and {max} characters
user.password.not.valid=* 5-50 characters
+user.password.otp.code.not.blank=OTP code cannot be empty
+user.password.otp.code.error=OTP code error
user.email.not.valid=Mailbox format error
user.email.not.blank=Mailbox cannot be blank
user.phonenumber.not.blank=Phone number cannot be blank
diff --git a/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties b/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
index cce11c85d..a74b833e6 100644
--- a/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
+++ b/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
@@ -17,6 +17,8 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间
user.password.not.blank=用户密码不能为空
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
user.password.not.valid=* 5-50个字符
+user.password.otp.code.not.blank=OTP验证码不能为空
+user.password.otp.code.error=OTP验证码错误
user.email.not.valid=邮箱格式错误
user.email.not.blank=邮箱不能为空
user.phonenumber.not.blank=用户手机号不能为空
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java
index 22de8f2c4..b410cdb64 100644
--- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/PasswordLoginBody.java
@@ -1,6 +1,7 @@
package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Length;
@@ -30,4 +31,10 @@ public class PasswordLoginBody extends LoginBody {
@Length(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
private String password;
+ /**
+ * OTP验证码
+ */
+ @NotNull(message = "{user.password.otp.code.not.blank}")
+ private Integer otpCode;
+
}
diff --git a/ruoyi-common/ruoyi-common-web/pom.xml b/ruoyi-common/ruoyi-common-web/pom.xml
index 5e366bc3b..0a73d1c30 100644
--- a/ruoyi-common/ruoyi-common-web/pom.xml
+++ b/ruoyi-common/ruoyi-common-web/pom.xml
@@ -70,6 +70,11 @@
cn.hutool
hutool-crypto
+
+
+ com.warrenstrange
+ googleauth
+
diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/GoogleAuthConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/GoogleAuthConfig.java
new file mode 100644
index 000000000..b431d37e1
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/GoogleAuthConfig.java
@@ -0,0 +1,20 @@
+package org.dromara.common.web.config;
+
+import com.warrenstrange.googleauth.GoogleAuthenticator;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 谷歌验证配置
+ *
+ * @author knight
+ */
+@Configuration(proxyBeanMethods = false)
+public class GoogleAuthConfig {
+
+ @Bean
+ public GoogleAuthenticator googleAuthenticator() {
+ return new GoogleAuthenticator();
+ }
+
+}
diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java
index 36104d627..bf10f5952 100644
--- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java
+++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java
@@ -221,6 +221,18 @@ public class SysUserController extends BaseController {
return R.ok(userService.selectUserByIds(userIds == null ? null : List.of(userIds), deptId));
}
+ /**
+ * 重置OTP秘钥
+ */
+ @ApiEncrypt
+ @SaCheckPermission("system:user:resetPwd")
+ @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+ @PutMapping("/resetOtpSecret/{userId}")
+ public R resetOtpSecret(@PathVariable Long userId) {
+ userService.checkUserDataScope(userId);
+ return toAjax(userService.resetOtpSecret(userId));
+ }
+
/**
* 重置密码
*/
diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUser.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUser.java
index 8dde40b35..aca24b952 100644
--- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUser.java
+++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUser.java
@@ -103,6 +103,15 @@ public class SysUser extends TenantEntity {
*/
private String remark;
+ /**
+ * One-Time Password 秘钥
+ */
+ private String otpSecret;
+
+ /**
+ * One-Time Password 秘钥绑定链接
+ */
+ private String otpUrl;
public SysUser(Long userId) {
this.userId = userId;
diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java
index 86249d20e..d3773b98e 100644
--- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java
+++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java
@@ -103,6 +103,16 @@ public class SysUserVo implements Serializable {
*/
private Date loginDate;
+ /**
+ * One-Time Password 秘钥
+ */
+ private String otpSecret;
+
+ /**
+ * One-Time Password 秘钥绑定链接
+ */
+ private String otpUrl;
+
/**
* 备注
*/
diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java
index 0325a2558..895e714c5 100644
--- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java
+++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java
@@ -187,6 +187,14 @@ public interface ISysUserService {
*/
boolean updateUserAvatar(Long userId, Long avatar);
+ /**
+ * 重置用户OTP秘钥
+ *
+ * @param userId 用户ID
+ * @return 结果
+ */
+ int resetOtpSecret(Long userId);
+
/**
* 重置用户密码
*
diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java
index 254060688..01e97c416 100644
--- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java
+++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java
@@ -11,12 +11,17 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.warrenstrange.googleauth.GoogleAuthenticator;
+import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
+import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.config.RuoYiConfig;
import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.constant.UserConstants;
import org.dromara.common.core.domain.dto.UserDTO;
import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.service.UserService;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.SpringUtils;
@@ -59,6 +64,8 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
private final SysPostMapper postMapper;
private final SysUserRoleMapper userRoleMapper;
private final SysUserPostMapper userPostMapper;
+ private final GoogleAuthenticator googleAuthenticator;
+ private final RuoYiConfig ruoYiConfig;
@Override
public TableDataInfo selectPageUserList(SysUserBo user, PageQuery pageQuery) {
@@ -304,6 +311,13 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
@Transactional(rollbackFor = Exception.class)
public int insertUser(SysUserBo user) {
SysUser sysUser = MapstructUtils.convert(user, SysUser.class);
+
+ GoogleAuthenticatorKey credentials = googleAuthenticator.createCredentials();
+ String otpAuthTotpURL = GoogleAuthenticatorQRGenerator
+ .getOtpAuthTotpURL(ruoYiConfig.getName(), sysUser.getNickName(), credentials);
+ sysUser.setOtpSecret(credentials.getKey());
+ sysUser.setOtpUrl(otpAuthTotpURL);
+
// 新增用户信息
int rows = baseMapper.insert(sysUser);
user.setUserId(sysUser.getUserId());
@@ -412,6 +426,30 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
.eq(SysUser::getUserId, userId)) > 0;
}
+ /**
+ * 重置用户OTP秘钥
+ *
+ * @param userId 用户ID
+ * @return 结果
+ */
+ @Override
+ public int resetOtpSecret(Long userId) {
+ SysUser sysUser = baseMapper.selectById(userId);
+ if (ObjectUtil.isNull(sysUser)) {
+ throw new UserException("user.not.exists", userId);
+ }
+
+ GoogleAuthenticatorKey credentials = googleAuthenticator.createCredentials();
+ String otpAuthTotpURL = GoogleAuthenticatorQRGenerator
+ .getOtpAuthTotpURL(ruoYiConfig.getName(), sysUser.getNickName(), credentials);
+
+ return baseMapper.update(null,
+ new LambdaUpdateWrapper()
+ .set(SysUser::getOtpSecret, credentials.getKey())
+ .set(SysUser::getOtpUrl, otpAuthTotpURL)
+ .eq(SysUser::getUserId, userId));
+ }
+
/**
* 重置用户密码
*
diff --git a/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
index ded6fa858..87d30a229 100644
--- a/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
+++ b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
@@ -18,7 +18,8 @@
u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex,
- u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark
+ u.status, u.del_flag, u.login_ip, u.login_date, u.otp_secret, u.otp_url, u.create_by, u.create_time,
+ u.remark
from sys_user u
${ew.getCustomSqlSegment}
@@ -31,7 +32,8 @@
u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex,
- u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark
+ u.status, u.del_flag, u.login_ip, u.login_date, u.otp_secret, u.otp_url, u.create_by, u.create_time,
+ u.remark
from sys_user u
${ew.getCustomSqlSegment}
@@ -39,7 +41,7 @@