Springboot整合Redis实现邮箱验证码

把帆帆喂饱
• 阅读 884

Springboot整合Redis实现邮箱验证码

开启邮箱服务

打开https://mail.qq.com/ 登录你自己的qq账号

选择账户

Springboot整合Redis实现邮箱验证码

点击开启 STMP服务:

Springboot整合Redis实现邮箱验证码

发送短信:

Springboot整合Redis实现邮箱验证码

发送完,点击我已发送,然后得到密码:

Springboot整合Redis实现邮箱验证码

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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;您好," +
      "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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;您好," +
      "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);
  }
}
点赞
收藏
评论区
推荐文章
菜鸟阿都 菜鸟阿都
3年前
python实现邮件发送
前言使用python的第三方库yagmail实现邮件发送的功能yagmail官网文档:第一步:申请一个邮箱作为发送邮箱此处以网易邮箱为例,因为使用python代码实现邮件的发送,需要开启邮箱的授权密码功能,用生成的授权密码作为发送邮件的密码,以下步骤为开启网易邮箱的授权密码功能。第二步:安装yagmail库languagepipinstally
桃浪十七丶 桃浪十七丶
3年前
PDF文件转化成mobi格式,亲测kindle或者iReader可用!
点击连接,然后选择要转换的文件比如我的是MySQL电子书书,选择输入文件和输出文件的格式,转换,对了记得输入邮箱号码,转化完毕会发送连接到邮箱提供下载。或者,网络流畅的情况下不用输入邮箱号码,转化完毕会自动重定向到下载页面。
Wesley13 Wesley13
3年前
java实现使用QQ邮箱发送验证码功能
首先当然是导入jar包了啊如果是maven项目可以进maven资源库进行搜索导入,在此附上地址:https://mvnrepository.com这是需要导入的jar包 commonsemail1.x.jar、mail.jar  activation.jar,其中activation.jar我并没有导入,但是还是发送成功了,但看网上有蛮多都说需
代码哈士奇 代码哈士奇
3年前
云函数手撸用户体系
使用云函数实现用户系统数据库为腾讯云TDSQL其它服务商云函数通用只需修改index.js返回参数即可主要有用户注册用户登陆邮箱发送验证码邮箱验证码校检邮箱绑定邮箱解绑邮箱验证码登陆生成token校验token其它功能可以在此基础上拓展纯手撸代码云函数环境为nodejs12.13由于我比较穷就不带大家使用短信服务了
Easter79 Easter79
3年前
SpringBoot入门 (十) 发送邮件
本文记录学习在SpringBoot中发送邮件。一邮件发送过程发送邮件是一个我们在项目中经常会用到的功能,如在用户注册时发送验证码,账户激活等都会用到。完整的一个邮件发送过程主要包含以下几个步骤:1发件人在用户邮件代理上写邮件内容及收件人的邮箱地址;2用户邮件代理根据发件人填写的邮件信息,生成一封符合邮件格式的邮件;
Stella981 Stella981
3年前
Spring Boot demo系列(七):邮件服务
2021.2.24更新1概述SpringBoot整合邮件服务,包括发送普通的文本邮件以及带附件的邮件。2邮箱选择这里选择的是QQ邮箱作为发送的邮箱,当然也可以选择其他的邮箱,只是具体的配置不一样。使用QQ邮箱的话,需要在个人设置中开启SMTP服务:!在这里插入
Stella981 Stella981
3年前
SpringBoot入门 (十) 发送邮件
本文记录学习在SpringBoot中发送邮件。一邮件发送过程发送邮件是一个我们在项目中经常会用到的功能,如在用户注册时发送验证码,账户激活等都会用到。完整的一个邮件发送过程主要包含以下几个步骤:1发件人在用户邮件代理上写邮件内容及收件人的邮箱地址;2用户邮件代理根据发件人填写的邮件信息,生成一封符合邮件格式的邮件;
Stella981 Stella981
3年前
SpirngBoot后台使用QQ邮箱发送验证码实现全过程
SpirngBoot后台使用QQ邮箱发送验证码在学校自己搞项目的时候想多搞点功能,短信验证码又要收费,所以搞个白嫖邮箱验证哈哈哈哈而在百度查资料的时候,发现大佬们都喜欢只给一两句关键代码,这让我这种菜鸟就比较为难,所以我自己不断一点点百度并整理了这份资料,并且自己也将功能实现了,在此分享给大家,
Stella981 Stella981
3年前
SpringBoot 2.x 集成QQ邮箱、网易系邮箱、Gmail邮箱发送邮件
在Spring中提供了非常好用的JavaMailSender接口实现邮件发送,在SpringBoot的Starter模块中也为此提供了自动化配置。项目源码已托管在GiteeSpringBoot\_Guide(https://gitee.com/javen205/SpringBoot_Guide.git"SpringBoot_Guide")
Stella981 Stella981
3年前
Jenkins设置运行结果自动发送邮箱通知
Jenkins设置运行结果自动发送邮箱通知1获取邮箱授权码登录邮箱选择“设置”开启IMAP/SMTP服务开启后需要通过手机发送短信到线上获取授权码!(https://img2018.cnblogs.com/blog/1798505/201909/1798505201909280544535181522354728.png)例如我
把帆帆喂饱
把帆帆喂饱
Lv1
自卑溢出来就变成了安静和温柔。
文章
7
粉丝
6
获赞
6