add 新增 密码登录支持OTP验证

This commit is contained in:
knight 2024-08-26 17:36:51 +08:00
parent c87016c1af
commit 5c66ebe374
15 changed files with 142 additions and 6 deletions

View File

@ -40,6 +40,7 @@
<justauth.version>1.16.6</justauth.version> <justauth.version>1.16.6</justauth.version>
<!-- 离线IP地址定位库 --> <!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version> <ip2region.version>2.7.0</ip2region.version>
<googleauth.version>1.4.0</googleauth.version>
<undertow.version>2.3.15.Final</undertow.version> <undertow.version>2.3.15.Final</undertow.version>
<!-- OSS 配置 --> <!-- OSS 配置 -->
@ -312,6 +313,13 @@
<version>${ip2region.version}</version> <version>${ip2region.version}</version>
</dependency> </dependency>
<!-- OTP谷歌验证 -->
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>${googleauth.version}</version>
</dependency>
<dependency> <dependency>
<groupId>io.undertow</groupId> <groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId> <artifactId>undertow-core</artifactId>

View File

@ -5,6 +5,7 @@ import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.warrenstrange.googleauth.GoogleAuthenticator;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
@ -46,6 +47,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
private final CaptchaProperties captchaProperties; private final CaptchaProperties captchaProperties;
private final SysLoginService loginService; private final SysLoginService loginService;
private final SysUserMapper userMapper; private final SysUserMapper userMapper;
private final GoogleAuthenticator googleAuthenticator;
@Override @Override
public LoginVo login(String body, SysClientVo client) { public LoginVo login(String body, SysClientVo client) {
@ -65,6 +67,13 @@ public class PasswordAuthStrategy implements IAuthStrategy {
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> { LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByUsername(username); SysUserVo user = loadUserByUsername(username);
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword())); 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 // 此处可根据登录用户的数据不同 自行创建 loginUser
return loginService.buildLoginUser(user); return loginService.buildLoginUser(user);
}); });

View File

@ -16,6 +16,8 @@ user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成
user.username.length.valid=账户长度必须在{min}到{max}个字符之间 user.username.length.valid=账户长度必须在{min}到{max}个字符之间
user.password.not.blank=用户密码不能为空 user.password.not.blank=用户密码不能为空
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间 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.password.not.valid=* 5-50个字符
user.email.not.valid=邮箱格式错误 user.email.not.valid=邮箱格式错误
user.email.not.blank=邮箱不能为空 user.email.not.blank=邮箱不能为空

View File

@ -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.not.blank=Password cannot be empty
user.password.length.valid=Password length must be between {min} and {max} characters user.password.length.valid=Password length must be between {min} and {max} characters
user.password.not.valid=* 5-50 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.valid=Mailbox format error
user.email.not.blank=Mailbox cannot be blank user.email.not.blank=Mailbox cannot be blank
user.phonenumber.not.blank=Phone number cannot be blank user.phonenumber.not.blank=Phone number cannot be blank

View File

@ -17,6 +17,8 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间
user.password.not.blank=用户密码不能为空 user.password.not.blank=用户密码不能为空
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间 user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
user.password.not.valid=* 5-50个字符 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.valid=邮箱格式错误
user.email.not.blank=邮箱不能为空 user.email.not.blank=邮箱不能为空
user.phonenumber.not.blank=用户手机号不能为空 user.phonenumber.not.blank=用户手机号不能为空

View File

@ -1,6 +1,7 @@
package org.dromara.common.core.domain.model; package org.dromara.common.core.domain.model;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Length; 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}") @Length(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
private String password; private String password;
/**
* OTP验证码
*/
@NotNull(message = "{user.password.otp.code.not.blank}")
private Integer otpCode;
} }

View File

@ -70,6 +70,11 @@
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId> <artifactId>hutool-crypto</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -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();
}
}

View File

@ -221,6 +221,18 @@ public class SysUserController extends BaseController {
return R.ok(userService.selectUserByIds(userIds == null ? null : List.of(userIds), deptId)); 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<Void> resetOtpSecret(@PathVariable Long userId) {
userService.checkUserDataScope(userId);
return toAjax(userService.resetOtpSecret(userId));
}
/** /**
* 重置密码 * 重置密码
*/ */

View File

@ -103,6 +103,15 @@ public class SysUser extends TenantEntity {
*/ */
private String remark; private String remark;
/**
* One-Time Password 秘钥
*/
private String otpSecret;
/**
* One-Time Password 秘钥绑定链接
*/
private String otpUrl;
public SysUser(Long userId) { public SysUser(Long userId) {
this.userId = userId; this.userId = userId;

View File

@ -103,6 +103,16 @@ public class SysUserVo implements Serializable {
*/ */
private Date loginDate; private Date loginDate;
/**
* One-Time Password 秘钥
*/
private String otpSecret;
/**
* One-Time Password 秘钥绑定链接
*/
private String otpUrl;
/** /**
* 备注 * 备注
*/ */

View File

@ -187,6 +187,14 @@ public interface ISysUserService {
*/ */
boolean updateUserAvatar(Long userId, Long avatar); boolean updateUserAvatar(Long userId, Long avatar);
/**
* 重置用户OTP秘钥
*
* @param userId 用户ID
* @return 结果
*/
int resetOtpSecret(Long userId);
/** /**
* 重置用户密码 * 重置用户密码
* *

View File

@ -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.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.CacheNames;
import org.dromara.common.core.constant.UserConstants; import org.dromara.common.core.constant.UserConstants;
import org.dromara.common.core.domain.dto.UserDTO; import org.dromara.common.core.domain.dto.UserDTO;
import org.dromara.common.core.exception.ServiceException; 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.service.UserService;
import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
@ -59,6 +64,8 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
private final SysPostMapper postMapper; private final SysPostMapper postMapper;
private final SysUserRoleMapper userRoleMapper; private final SysUserRoleMapper userRoleMapper;
private final SysUserPostMapper userPostMapper; private final SysUserPostMapper userPostMapper;
private final GoogleAuthenticator googleAuthenticator;
private final RuoYiConfig ruoYiConfig;
@Override @Override
public TableDataInfo<SysUserVo> selectPageUserList(SysUserBo user, PageQuery pageQuery) { public TableDataInfo<SysUserVo> selectPageUserList(SysUserBo user, PageQuery pageQuery) {
@ -304,6 +311,13 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int insertUser(SysUserBo user) { public int insertUser(SysUserBo user) {
SysUser sysUser = MapstructUtils.convert(user, SysUser.class); 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); int rows = baseMapper.insert(sysUser);
user.setUserId(sysUser.getUserId()); user.setUserId(sysUser.getUserId());
@ -412,6 +426,30 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
.eq(SysUser::getUserId, userId)) > 0; .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<SysUser>()
.set(SysUser::getOtpSecret, credentials.getKey())
.set(SysUser::getOtpUrl, otpAuthTotpURL)
.eq(SysUser::getUserId, userId));
}
/** /**
* 重置用户密码 * 重置用户密码
* *

View File

@ -18,7 +18,8 @@
</if> </if>
<if test="ew.getSqlSelect == null"> <if test="ew.getSqlSelect == null">
u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, 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
</if> </if>
from sys_user u from sys_user u
${ew.getCustomSqlSegment} ${ew.getCustomSqlSegment}
@ -31,7 +32,8 @@
</if> </if>
<if test="ew.getSqlSelect == null"> <if test="ew.getSqlSelect == null">
u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, 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
</if> </if>
from sys_user u from sys_user u
${ew.getCustomSqlSegment} ${ew.getCustomSqlSegment}
@ -39,7 +41,7 @@
<select id="selectUserExportList" resultMap="SysUserExportResult"> <select id="selectUserExportList" resultMap="SysUserExportResult">
select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, select 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,
d.dept_name, d.leader, u1.user_name as leaderName d.dept_name, d.leader, u1.user_name as leaderName
from sys_user u from sys_user u
left join sys_dept d on u.dept_id = d.dept_id left join sys_dept d on u.dept_id = d.dept_id

View File

@ -152,6 +152,8 @@ create table sys_user (
del_flag char(1) default '0' comment '删除标志0代表存在 2代表删除', del_flag char(1) default '0' comment '删除标志0代表存在 2代表删除',
login_ip varchar(128) default '' comment '最后登录IP', login_ip varchar(128) default '' comment '最后登录IP',
login_date datetime comment '最后登录时间', login_date datetime comment '最后登录时间',
otp_secret varchar(100) default '' comment 'One-Time Password 秘钥',
otp_url varchar(200) default '' comment 'One-Time Password 秘钥绑定链接',
create_dept bigint(20) default null comment '创建部门', create_dept bigint(20) default null comment '创建部门',
create_by bigint(20) default null comment '创建者', create_by bigint(20) default null comment '创建者',
create_time datetime comment '创建时间', create_time datetime comment '创建时间',
@ -164,9 +166,9 @@ create table sys_user (
-- ---------------------------- -- ----------------------------
-- 初始化-用户信息表数据 -- 初始化-用户信息表数据
-- ---------------------------- -- ----------------------------
insert into sys_user values(1, '000000', 103, 'admin', '疯狂的狮子Li', 'sys_user', 'crazyLionLi@163.com', '15888888888', '1', null, '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), 103, 1, sysdate(), null, null, '管理员'); insert into sys_user values(1, '000000', 103, 'admin', '疯狂的狮子Li', 'sys_user', 'crazyLionLi@163.com', '15888888888', '1', null, '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), 'E3NX2Q3F2WE2ASGE', 'otpauth://totp/RuoYi-Vue-Plus:%E7%96%AF%E7%8B%82%E7%9A%84%E7%8B%AE%E5%AD%90Li?secret=E3NX2Q3F2WE2ASGE&issuer=RuoYi-Vue-Plus&algorithm=SHA1&digits=6&period=30', 103, 1, sysdate(), null, null, '管理员');
insert into sys_user values(3, '000000', 108, 'test', '本部门及以下 密码666666', 'sys_user', '', '', '0', null, '$2a$10$b8yUzN0C71sbz.PhNOCgJe.Tu1yWC3RNrTyjSQ8p1W0.aaUXUJ.Ne', '0', '0', '127.0.0.1', sysdate(), 103, 1, sysdate(), 3, sysdate(), null); insert into sys_user values(3, '000000', 108, 'test', '本部门及以下 密码666666', 'sys_user', '', '', '0', null, '$2a$10$b8yUzN0C71sbz.PhNOCgJe.Tu1yWC3RNrTyjSQ8p1W0.aaUXUJ.Ne', '0', '0', '127.0.0.1', sysdate(), '', '', 1, 103,sysdate(), 3, sysdate(), null);
insert into sys_user values(4, '000000', 102, 'test1', '仅本人 密码666666', 'sys_user', '', '', '0', null, '$2a$10$b8yUzN0C71sbz.PhNOCgJe.Tu1yWC3RNrTyjSQ8p1W0.aaUXUJ.Ne', '0', '0', '127.0.0.1', sysdate(), 103, 1, sysdate(), 4, sysdate(), null); insert into sys_user values(4, '000000', 102, 'test1', '仅本人 密码666666', 'sys_user', '', '', '0', null, '$2a$10$b8yUzN0C71sbz.PhNOCgJe.Tu1yWC3RNrTyjSQ8p1W0.aaUXUJ.Ne', '0', '0', '127.0.0.1', sysdate(), '', '', 103, 1, sysdate(), 4, sysdate(), null);
-- ---------------------------- -- ----------------------------
-- 3、岗位信息表 -- 3、岗位信息表