Springboot整合Redis实现邮箱验证码
开启邮箱服务
打开https://mail.qq.com/ 登录你自己的qq账号
选择账户
点击开启 STMP服务:
发送短信:
发送完,点击我已发送,然后得到密码:
Springboot配置邮箱
pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
yml配置
spring:
mail:
# 配置 SMTP 服务器地址
host: smtp.qq.com
# 发送者邮箱
username: XXXXXXXX
# 配置密码,注意不是真正的密码,而是刚刚申请到的授权码
password: XXXXXXXX
# 端口号465或587
port: 587
# 默认的邮件编码为UTF-8
default-encoding: UTF-8
编写邮箱工具类
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.Date;
@Component
@Slf4j
public class EmailUtils {
@Autowired
JavaMailSender javaMailSender;
@Value("${spring.mail.username}")
String username;
public void sendHtml(String title, String html, String to) {
MimeMessage mailMessage = javaMailSender.createMimeMessage();
//需要借助Helper类
MimeMessageHelper helper = new MimeMessageHelper(mailMessage);
try {
helper.setFrom(username); // 必填
helper.setTo(to); // 必填
// helper.setBcc("密送人"); // 选填
helper.setSubject(title); // 必填
helper.setSentDate(new Date());//发送时间
helper.setText(html, true); // 必填 第一个参数要发送的内容,第二个参数是不是Html格式。
javaMailSender.send(mailMessage);
} catch (MessagingException e) {
log.error("发送邮件失败", e);
}
}
}
Springboot整合Redis
导入maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置Redis
spring:
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: XXX.XXX.XXX.XXX
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
# password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
Redis配置防止序列化
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 key的序列化方式 防止默认的jdk序列化方式出现二进制码 看不懂
redisTemplate.setKeySerializer(new StringRedisSerializer());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value的序列化类型
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
Redis工具类
@SuppressWarnings(value = {"unchecked"})
@Component
@Slf4j
public class RedisUtils {
private static RedisTemplate<String, Object> staticRedisTemplate;
private final RedisTemplate<String, Object> redisTemplate;
public RedisUtils(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// Springboot启动成功之后会调用这个方法
@PostConstruct
public void initRedis() {
// 初始化设置 静态staticRedisTemplate对象,方便后续操作数据
staticRedisTemplate = redisTemplate;
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public static <T> void setCacheObject(final String key, final T value) {
staticRedisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public static <T> void setCacheObject(final String key, final T value, final long timeout, final TimeUnit timeUnit) {
staticRedisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public static <T> T getCacheObject(final String key) {
return (T) staticRedisTemplate.opsForValue().get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public static boolean deleteObject(final String key) {
return Boolean.TRUE.equals(staticRedisTemplate.delete(key));
}
/**
* 获取单个key的过期时间
*
* @param key
* @return
*/
public static Long getExpireTime(final String key) {
return staticRedisTemplate.getExpire(key);
}
/**
* 发送ping命令
* redis 返回pong
*/
public static void ping() {
String res = staticRedisTemplate.execute(RedisConnectionCommands::ping);
log.info("Redis ping ==== {}", res);
}
}
验证码发送及过期实现
业务:涉及到邮箱验证码的功能:用户注册,密码重置
思路分析:首先通过hutool工具类随机生成验证码,通过邮箱发送工具发送。验证码过期使用Redis设计过期时间实现,唯一性则通过key值保证,键由 功能前缀+邮箱(邮箱设置索引保证唯一性)+业务功能 来组成。举个例子
String key = Constants.EMAIL_CODE + EmailCodeEnum.RESET_PASSWORD.getValue() + email;
所涉及到的枚举类如下。
@Getter
public enum EmailCodeEnum {
REGISTER("REGISTER", "register:"),
RESET_PASSWORD("RESETPASSWORD", "resetPassword:"),
LOGIN("LOGIN", "login:"),
CHANGE_PASSWORD("CHANGEPASSWORD", "changePassword:"),
UNKNOWN("", "");
private final String type;
private final String value;
EmailCodeEnum(String type, String value) {
this.type = type;
this.value = value;
}
public static String getValue(String type) {
EmailCodeEnum[] values = values();
for (EmailCodeEnum codeEnum : values) {
if (type.equals(codeEnum.type)) {
return codeEnum.value;
}
}
return "";
}
public static EmailCodeEnum getEnum(String type) {
EmailCodeEnum[] values = values();
for (EmailCodeEnum codeEnum : values) {
if (type.equals(codeEnum.type)) {
return codeEnum;
}
}
return UNKNOWN;
}
}
public interface Constants {
// 用户名次前缀
String USER_NAME_PREFIX = "partner_";
// 时间的规则
String DATE_RULE_YYYYMMDD = "yyyyMMdd";
String EMAIL_CODE = "email.code.";
}
发送验证码时,设置过期时间,并通过合理的key设计,保证唯一性
RedisUtils.setCacheObject(key, code, TIME_IN_MS5, TimeUnit.MILLISECONDS);
Redis存储设置过期时间
/**
* 邮箱发送验证码
* @param email
* @param type
*/
@Override
public void sendEmail(String email, String type) {
String emailPrefix = EmailCodeEnum.getValue(type);
if (StrUtil.isBlank(emailPrefix)) {
throw new ServiceException("不支持的邮箱验证类型");
}
String key = Constants.EMAIL_CODE + emailPrefix + email;
Long expireTime = RedisUtils.getExpireTime(key);
// 限制超过1分钟才可以继续发送邮件,判断过期时间是否大于4分钟
if (expireTime != null && expireTime > 4 * 60) {
throw new ServiceException("发送邮箱验证过于频繁");
}
Integer code = Integer.valueOf(RandomUtil.randomNumbers(6));
log.info("本次验证的code是:{}", code);
String context = "<b>尊敬的用户:</b><br><br><br> 您好," +
"Odin交友网提醒您本次的验证码是:<b>{}</b>," +
"有效期5分钟。<br><br><br><b>Odin交友网</b>";
String html = StrUtil.format(context, code);
if ("REGISTER".equals(type) ) {
User dbUser = getOne(new QueryWrapper<User>().eq("email", email));
if (dbUser != null) {
throw new ServiceException("邮箱已经注册!");
}
}
// 线程异步执行,防止网络阻塞
ThreadUtil.execAsync(() -> {
emailUtils.sendHtml("【Odin交友网】验证提醒", html, email);
});
RedisUtils.setCacheObject(key, code, TIME_IN_MS5, TimeUnit.MILLISECONDS);
}
为了保证验证码不被重复使用,在验证通过之后直接删除Redis中的数据。
private void validateEmail(String emailKey,String emailCode) {
// 从Redis中获取验证码
Integer code = RedisUtils.getCacheObject(emailKey);
if (code == null) {
throw new ServiceException("验证码失效");
}
if (!emailCode.equals(code.toString())) {
throw new ServiceException("验证码错误");
}
// 验证通过后立即删除
RedisUtils.deleteObject(emailKey);
}
完整业务代码如下
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
private static final long TIME_IN_MS5 = 5 * 60 * 1000; // 表示5分钟的毫秒数
@Resource
private EmailUtils emailUtils;
/**
* 用户登陆
* @param user
* @return
*/
@Override
public User login(UserRequest user) {
User dbUser = null;
try {
dbUser = getOne(new UpdateWrapper<User>().eq("username", user.getUsername())
.or().eq("email", user.getUsername()));
} catch (Exception e) {
throw new RuntimeException("系统异常");
}
if (dbUser == null) {
throw new ServiceException("未找到用户");
}
if (!user.getPassword().equals(dbUser.getPassword())) {
throw new ServiceException("用户名或密码错误");
}
return dbUser;
}
@Override
public User register(UserRequest user) {
String key = Constants.EMAIL_CODE + EmailCodeEnum.REGISTER.getValue() + user.getEmail();
String emailCode = user.getEmailCode();
validateEmail(key ,emailCode);
try {
User saveUser = new User();
BeanUtils.copyProperties(user, saveUser);
return saveUser(saveUser);
} catch (Exception e) {
throw new RuntimeException("数据库异常",e);
}
}
/**
* 邮箱发送验证码
* @param email
* @param type
*/
@Override
public void sendEmail(String email, String type) {
String emailPrefix = EmailCodeEnum.getValue(type);
if (StrUtil.isBlank(emailPrefix)) {
throw new ServiceException("不支持的邮箱验证类型");
}
String key = Constants.EMAIL_CODE + emailPrefix + email;
Long expireTime = RedisUtils.getExpireTime(key);
// 限制超过1分钟才可以继续发送邮件,判断过期时间是否大于4分钟
if (expireTime != null && expireTime > 4 * 60) {
throw new ServiceException("发送邮箱验证过于频繁");
}
Integer code = Integer.valueOf(RandomUtil.randomNumbers(6));
log.info("本次验证的code是:{}", code);
String context = "<b>尊敬的用户:</b><br><br><br> 您好," +
"Odin交友网提醒您本次的验证码是:<b>{}</b>," +
"有效期5分钟。<br><br><br><b>Odin交友网</b>";
String html = StrUtil.format(context, code);
if ("REGISTER".equals(type) ) {
User dbUser = getOne(new QueryWrapper<User>().eq("email", email));
if (dbUser != null) {
throw new ServiceException("邮箱已经注册!");
}
}
// 线程异步执行,防止网络阻塞
ThreadUtil.execAsync(() -> {
emailUtils.sendHtml("【Odin交友网】验证提醒", html, email);
});
RedisUtils.setCacheObject(key, code, TIME_IN_MS5, TimeUnit.MILLISECONDS);
}
/**
* 重置密码功能
* @param userRequest
* @return
*/
@Override
public String passwordReset(UserRequest userRequest) {
String email = userRequest.getEmail();
String key = Constants.EMAIL_CODE + EmailCodeEnum.RESET_PASSWORD.getValue() + email;
User dbUser = getOne(new UpdateWrapper<User>().eq("email", email));
if (dbUser == null) {
throw new ServiceException("未找到当前用户");
}
validateEmail(key,userRequest.getEmailCode());
String newPass = "admin";
dbUser.setPassword(newPass);
try {
updateById(dbUser); // 设置到数据库
} catch (Exception e) {
throw new RuntimeException("重置失败", e);
}
return newPass;
}
/**
* 用户注册
* @param user 前端传入用户
* @return 注册用户信息
*/
private User saveUser(User user) {
User dbUser = getOne(new UpdateWrapper<User>().eq("username", user.getUsername()));
if (dbUser != null) {
throw new ServiceException("用户已存在");
}
if (StrUtil.isBlank(user.getName())) {
String name = Constants.USER_NAME_PREFIX + DateUtil.format(new Date(), DATE_RULE_YYYYMMDD)
+ RandomUtil.randomString(4);
user.setName(name);
}
if (StrUtil.isBlank(user.getPassword())) {
user.setPassword("admin"); // 设置默认密码
}
// 设置用户唯一uid
user.setUid(IdUtil.fastSimpleUUID());
boolean save = save(user);
if (!save) {
throw new RuntimeException("注册失败");
}
return user;
}
/**
* 邮箱验证码验证功能
* @param emailKey Redis中存储的key
* @param emailCode 前端传入的验证码
*/
private void validateEmail(String emailKey,String emailCode) {
// 从Redis中获取验证码
Integer code = RedisUtils.getCacheObject(emailKey);
if (code == null) {
throw new ServiceException("验证码失效");
}
if (!emailCode.equals(code.toString())) {
throw new ServiceException("验证码错误");
}
// 验证通过后立即删除
RedisUtils.deleteObject(emailKey);
}
}