add 增加 邮箱验证码发送接口

add 增加 邮箱登陆接口
This commit is contained in:
疯狂的狮子li 2023-03-30 10:19:04 +08:00
parent 0c1d6e111a
commit 6ed424f89e
11 changed files with 172 additions and 5 deletions

View File

@ -3,6 +3,7 @@ package com.ruoyi.web.controller;
import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.domain.model.EmailLoginBody;
import com.ruoyi.common.core.domain.model.LoginBody; import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.core.domain.model.RegisterBody; import com.ruoyi.common.core.domain.model.RegisterBody;
import com.ruoyi.common.core.domain.model.SmsLoginBody; import com.ruoyi.common.core.domain.model.SmsLoginBody;
@ -62,7 +63,7 @@ public class AuthController {
} }
/** /**
* 短信登录(示例) * 短信登录
* *
* @param body 登录信息 * @param body 登录信息
* @return 结果 * @return 结果
@ -76,6 +77,21 @@ public class AuthController {
return R.ok(loginVo); return R.ok(loginVo);
} }
/**
* 邮件登录
*
* @param body 登录信息
* @return 结果
*/
@PostMapping("/emailLogin")
public R<LoginVo> emailLogin(@Validated @RequestBody EmailLoginBody body) {
LoginVo loginVo = new LoginVo();
// 生成令牌
String token = loginService.emailLogin(body.getTenantId(), body.getEmail(), body.getEmailCode());
loginVo.setToken(token);
return R.ok(loginVo);
}
/** /**
* 小程序登录(示例) * 小程序登录(示例)
* *

View File

@ -11,6 +11,8 @@ import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.SpringUtils; import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.StringUtils; import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.reflect.ReflectUtils; import com.ruoyi.common.core.utils.reflect.ReflectUtils;
import com.ruoyi.common.mail.config.properties.MailProperties;
import com.ruoyi.common.mail.utils.MailUtils;
import com.ruoyi.common.redis.utils.RedisUtils; import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.sms.config.properties.SmsProperties; import com.ruoyi.common.sms.config.properties.SmsProperties;
import com.ruoyi.common.sms.core.SmsTemplate; import com.ruoyi.common.sms.core.SmsTemplate;
@ -46,6 +48,7 @@ public class CaptchaController {
private final CaptchaProperties captchaProperties; private final CaptchaProperties captchaProperties;
private final SmsProperties smsProperties; private final SmsProperties smsProperties;
private final MailProperties mailProperties;
/** /**
* 短信验证码 * 短信验证码
@ -53,8 +56,7 @@ public class CaptchaController {
* @param phonenumber 用户手机号 * @param phonenumber 用户手机号
*/ */
@GetMapping("/sms/code") @GetMapping("/sms/code")
public R<Void> smsCaptcha(@NotBlank(message = "{user.phonenumber.not.blank}") public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
String phonenumber) {
if (!smsProperties.getEnabled()) { if (!smsProperties.getEnabled()) {
return R.fail("当前系统没有开启短信功能!"); return R.fail("当前系统没有开启短信功能!");
} }
@ -74,6 +76,28 @@ public class CaptchaController {
return R.ok(); return R.ok();
} }
/**
* 邮箱验证码
*
* @param email 邮箱
*/
@GetMapping("/email/code")
public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
if (!mailProperties.getEnabled()) {
return R.fail("当前系统没有开启邮箱功能!");
}
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
try {
MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
} catch (Exception e) {
log.error("验证码短信发送异常 => {}", e.getMessage());
return R.fail(e.getMessage());
}
return R.ok();
}
/** /**
* 生成验证码 * 生成验证码
*/ */

View File

@ -112,6 +112,23 @@ public class SysLoginService {
return StpUtil.getTokenValue(); return StpUtil.getTokenValue();
} }
public String emailLogin(String tenantId, String email, String emailCode) {
// 校验租户
checkTenant(tenantId);
// 通过手机号查找用户
SysUserVo user = loadUserByEmail(tenantId, email);
checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
// 此处可根据登录用户的数据不同 自行创建 loginUser
LoginUser loginUser = buildLoginUser(user);
// 生成token
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
recordLoginInfo(user.getUserId());
return StpUtil.getTokenValue();
}
public String xcxLogin(String xcxCode) { public String xcxLogin(String xcxCode) {
// xcxCode 小程序调用 wx.login 授权后获取 // xcxCode 小程序调用 wx.login 授权后获取
@ -184,6 +201,18 @@ public class SysLoginService {
return code.equals(smsCode); return code.equals(smsCode);
} }
/**
* 校验邮箱验证码
*/
private boolean validateEmailCode(String tenantId, String email, String emailCode) {
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
if (StringUtils.isBlank(code)) {
recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException();
}
return code.equals(emailCode);
}
/** /**
* 校验验证码 * 校验验证码
* *
@ -241,6 +270,24 @@ public class SysLoginService {
return userMapper.selectUserByPhonenumber(phonenumber); return userMapper.selectUserByPhonenumber(phonenumber);
} }
private SysUserVo loadUserByEmail(String tenantId, String email) {
SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.select(SysUser::getPhonenumber, SysUser::getStatus)
.eq(TenantHelper.isEnable(), SysUser::getTenantId, tenantId)
.eq(SysUser::getEmail, email));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", email);
throw new UserException("user.not.exists", email);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", email);
throw new UserException("user.blocked", email);
}
if (TenantHelper.isEnable()) {
return userMapper.selectTenantUserByEmail(email, tenantId);
}
return userMapper.selectUserByEmail(email);
}
private SysUserVo loadUserByOpenid(String openid) { private SysUserVo loadUserByOpenid(String openid) {
// 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户 // 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
// todo 自行实现 userService.selectUserByOpenid(openid); // todo 自行实现 userService.selectUserByOpenid(openid);

View File

@ -18,6 +18,7 @@ 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.email.not.valid=邮箱格式错误 user.email.not.valid=邮箱格式错误
user.email.not.blank=邮箱不能为空
user.phonenumber.not.blank=用户手机号不能为空 user.phonenumber.not.blank=用户手机号不能为空
user.mobile.phone.number.not.valid=手机号格式错误 user.mobile.phone.number.not.valid=手机号格式错误
user.login.success=登录成功 user.login.success=登录成功
@ -42,6 +43,9 @@ rate.limiter.message=访问过于频繁,请稍候再试
sms.code.not.blank=短信验证码不能为空 sms.code.not.blank=短信验证码不能为空
sms.code.retry.limit.count=短信验证码输入错误{0}次 sms.code.retry.limit.count=短信验证码输入错误{0}次
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟 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不能为空 xcx.code.not.blank=小程序code不能为空
##租户 ##租户
tenant.number.not.blank=租户编号不能为空 tenant.number.not.blank=租户编号不能为空

View File

@ -18,6 +18,7 @@ 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.email.not.valid=Mailbox format 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 user.phonenumber.not.blank=Phone number cannot be blank
user.mobile.phone.number.not.valid=Phone number format error user.mobile.phone.number.not.valid=Phone number format error
user.login.success=Login successful user.login.success=Login successful
@ -42,6 +43,9 @@ rate.limiter.message=Visit too frequently, please try again later
sms.code.not.blank=Sms code cannot be blank sms.code.not.blank=Sms code cannot be blank
sms.code.retry.limit.count=Sms code input error {0} times sms.code.retry.limit.count=Sms code input error {0} times
sms.code.retry.limit.exceed=Sms code input error {0} times, account locked for {1} minutes sms.code.retry.limit.exceed=Sms code input error {0} times, account locked for {1} minutes
email.code.not.blank=Email code cannot be blank
email.code.retry.limit.count=Email code input error {0} times
email.code.retry.limit.exceed=Email code input error {0} times, account locked for {1} minutes
xcx.code.not.blank=Mini program code cannot be blank xcx.code.not.blank=Mini program code cannot be blank
##租户 ##租户
tenant.number.not.blank=Tenant number cannot be blank tenant.number.not.blank=Tenant number cannot be blank

View File

@ -18,6 +18,7 @@ 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.email.not.valid=邮箱格式错误 user.email.not.valid=邮箱格式错误
user.email.not.blank=邮箱不能为空
user.phonenumber.not.blank=用户手机号不能为空 user.phonenumber.not.blank=用户手机号不能为空
user.mobile.phone.number.not.valid=手机号格式错误 user.mobile.phone.number.not.valid=手机号格式错误
user.login.success=登录成功 user.login.success=登录成功
@ -42,6 +43,9 @@ rate.limiter.message=访问过于频繁,请稍候再试
sms.code.not.blank=短信验证码不能为空 sms.code.not.blank=短信验证码不能为空
sms.code.retry.limit.count=短信验证码输入错误{0}次 sms.code.retry.limit.count=短信验证码输入错误{0}次
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟 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不能为空 xcx.code.not.blank=小程序code不能为空
##租户 ##租户
tenant.number.not.blank=租户编号不能为空 tenant.number.not.blank=租户编号不能为空

View File

@ -0,0 +1,35 @@
package com.ruoyi.common.core.domain.model;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 短信登录对象
*
* @author Lion Li
*/
@Data
public class EmailLoginBody {
/**
* 租户ID
*/
@NotBlank(message = "{tenant.number.not.blank}")
private String tenantId;
/**
* 邮箱
*/
@NotBlank(message = "{user.email.not.blank}")
@Email(message = "{user.email.not.valid}")
private String email;
/**
* 邮箱code
*/
@NotBlank(message = "{email.code.not.blank}")
private String emailCode;
}

View File

@ -20,13 +20,13 @@ public class SmsLoginBody {
private String tenantId; private String tenantId;
/** /**
* 用户名 * 手机号
*/ */
@NotBlank(message = "{user.phonenumber.not.blank}") @NotBlank(message = "{user.phonenumber.not.blank}")
private String phonenumber; private String phonenumber;
/** /**
* 用户密码 * 短信code
*/ */
@NotBlank(message = "{sms.code.not.blank}") @NotBlank(message = "{sms.code.not.blank}")
private String smsCode; private String smsCode;

View File

@ -22,6 +22,11 @@ public enum LoginType {
*/ */
SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"), SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"),
/**
* 邮箱登录
*/
EMAIL("email.code.retry.limit.exceed", "email.code.retry.limit.count"),
/** /**
* 小程序登录 * 小程序登录
*/ */

View File

@ -78,6 +78,14 @@ public interface SysUserMapper extends BaseMapperPlus<SysUser, SysUserVo> {
*/ */
SysUserVo selectUserByPhonenumber(String phonenumber); SysUserVo selectUserByPhonenumber(String phonenumber);
/**
* 通过邮箱查询用户
*
* @param email 邮箱
* @return 用户对象信息
*/
SysUserVo selectUserByEmail(String email);
/** /**
* 通过用户名查询用户(不走租户插件) * 通过用户名查询用户(不走租户插件)
* *
@ -98,6 +106,16 @@ public interface SysUserMapper extends BaseMapperPlus<SysUser, SysUserVo> {
@InterceptorIgnore(tenantLine = "true") @InterceptorIgnore(tenantLine = "true")
SysUserVo selectTenantUserByPhonenumber(String phonenumber, String tenantId); SysUserVo selectTenantUserByPhonenumber(String phonenumber, String tenantId);
/**
* 通过邮箱查询用户(不走租户插件)
*
* @param email 邮箱
* @param tenantId 租户id
* @return 用户对象信息
*/
@InterceptorIgnore(tenantLine = "true")
SysUserVo selectTenantUserByEmail(String email, String tenantId);
/** /**
* 通过用户ID查询用户 * 通过用户ID查询用户
* *

View File

@ -102,6 +102,11 @@
where u.del_flag = '0' and u.phonenumber = #{phonenumber} where u.del_flag = '0' and u.phonenumber = #{phonenumber}
</select> </select>
<select id="selectUserByEmail" parameterType="String" resultMap="SysUserResult">
<include refid="selectUserVo"/>
where u.del_flag = '0' and u.email = #{email}
</select>
<select id="selectTenantUserByUserName" parameterType="String" resultMap="SysUserResult"> <select id="selectTenantUserByUserName" parameterType="String" resultMap="SysUserResult">
<include refid="selectUserVo"/> <include refid="selectUserVo"/>
where u.del_flag = '0' and u.user_name = #{userName} and u.tenant_id = #{tenantId} where u.del_flag = '0' and u.user_name = #{userName} and u.tenant_id = #{tenantId}
@ -112,6 +117,11 @@
where u.del_flag = '0' and u.phonenumber = #{phonenumber} and u.tenant_id = #{tenantId} where u.del_flag = '0' and u.phonenumber = #{phonenumber} and u.tenant_id = #{tenantId}
</select> </select>
<select id="selectTenantUserByEmail" parameterType="String" resultMap="SysUserResult">
<include refid="selectUserVo"/>
where u.del_flag = '0' and u.email = #{email} and u.tenant_id = #{tenantId}
</select>
<select id="selectUserById" parameterType="Long" resultMap="SysUserResult"> <select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
<include refid="selectUserVo"/> <include refid="selectUserVo"/>
where u.del_flag = '0' and u.user_id = #{userId} where u.del_flag = '0' and u.user_id = #{userId}