From 5c66ebe3748d96ea470653dfa3aca53df5e29b91 Mon Sep 17 00:00:00 2001 From: knight Date: Mon, 26 Aug 2024 17:36:51 +0800 Subject: [PATCH] =?UTF-8?q?add=20=E6=96=B0=E5=A2=9E=20=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=94=AF=E6=8C=81OTP=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 8 ++++ .../service/impl/PasswordAuthStrategy.java | 9 +++++ .../main/resources/i18n/messages.properties | 2 + .../resources/i18n/messages_en_US.properties | 2 + .../resources/i18n/messages_zh_CN.properties | 2 + .../core/domain/model/PasswordLoginBody.java | 7 ++++ ruoyi-common/ruoyi-common-web/pom.xml | 5 +++ .../common/web/config/GoogleAuthConfig.java | 20 ++++++++++ .../controller/system/SysUserController.java | 12 ++++++ .../org/dromara/system/domain/SysUser.java | 9 +++++ .../dromara/system/domain/vo/SysUserVo.java | 10 +++++ .../system/service/ISysUserService.java | 8 ++++ .../service/impl/SysUserServiceImpl.java | 38 +++++++++++++++++++ .../resources/mapper/system/SysUserMapper.xml | 8 ++-- script/sql/ry_vue_5.X.sql | 8 ++-- 15 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/GoogleAuthConfig.java 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 @@