手把手教你通过SpringBoot实现邮箱注册码验证

x33g5p2x  于2021-12-30 转载在 Spring  
字(12.5k)|赞(0)|评价(0)|浏览(826)

一、前言

注册一个系统成为用户,一般会要求用户留下一个邮件地址作为联系方式,就象我们去银行开户时银行会让我们留个手机号码一样。为了证明注册的邮箱地址是本人的,系统会向邮箱发送一串验证码,用户收取该验证码后在注册页面上输入验证码连同其他信息发往后台进行验证。

二、创建SpringBoot工程项目

  • 该工程项目主要实现步骤如下:
  1. JWT认证
  2. 集成mybatis-plus实现用户的增删改查
  3. 编写Email工具类实现邮件的发送
  4. 验证码邮件发送与验证码后台验证
  5. 前端联调测试

2.1 JWT认证

  • 前后端分离目前已成为互联网项目开发的业界标准,其核心思想就是前端(APP、小程序、H5页面等)通过调用后端的API接口,提交及返回JSON数据进行交互。

  • 在前后端分离项目中,首先要解决的就是登录及授权的问题。微服务架构下,传统的session认证限制了应用的扩展能力,无状态的JWT认证方法应运而生,该认证机制特别适用于分布式站点的单点登录(SSO)场景

  • 关于SpringBoot实现JWT的具体细节,请参考本人博文:
    《SpringBoot整合SpringSecurity实现JWT认证》

2.2 集成mybatis-plus实现用户的增删改查

  1. 添加maven依赖
<!-- mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
  1. 在 application.yml 配置文件中添加 mysql 数据库的相关配置:
spring:
  datasource:
    druid:
      db-type: com.alibaba.druid.pool.DruidDataSource
      driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
      url: jdbc:log4jdbc:mysql://localhost:3306/startup_backend?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root
  1. 编写用户表实体类:
/** * 用户表 * * @author zhuhuix * @date 2020-04-03 */
@ApiModel(value = "用户信息")
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_user")
public class SysUser implements Serializable {

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private String userName;

    @JsonIgnore
    private String password;

    private String nickName;

    /** * 性别 0-未知 1-male,2-female */
    private Integer gender;

    /** * 头像地址 */
    private String avatarUrl;

    private String country;

    private String province;

    private String city;

    @Email
    private String email;

    private String phone;

    private String remarks;

    private Boolean enabled;

    private Timestamp lastPasswordResetTime;

    @Builder.Default
    private Timestamp createTime = Timestamp.valueOf(LocalDateTime.now());

    @Builder.Default
    private Timestamp updateTime = Timestamp.valueOf(LocalDateTime.now());

}
  1. 新增Mapper类 SysUserMapper.java:
  • 直接继承 BaseMapper,这是 mybatis-plus 封装好的类,已经实现了基本的增删改查。
/** * 用户DAO接口 * * @author zhuhuix * @date 2021-07-19 */
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {

}
  1. 编写用户增删改查服务接口与实现类:
/** * 用户增删改查服务接口 * * @author zhuhuix * @date 2020-04-03 */
public interface SysUserService {

    /** * 增加用户 * * @param user 待新增的用户 * @return 增加成功的用户 */
    SysUser create(SysUser user);

    /** * 删除用户 * * @param user 待删除的用户 * @return 删除成功的用户 */
    Result<SysUser> delete(SysUser user);

    /** * 修改用户 * * @param user 待修改的用户 * @return 修改成功的用户 */
    Result<SysUser> update(SysUser user);

    /** * 根据userName查找用户 * * @param userName 用户帐号 * @return 用户帐号对应的用户 */
    SysUser findByUserName(String userName);

    /** * 判断注册使用的邮箱是否存在 * * @param email 邮箱号 * @return 是否找到 */
    boolean registerEmailExist(String email);
}
/** * 用户增删改查实现类 * * @author zhuhuix * @date 2020-04-03 */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class SysUserServiceImpl implements SysUserService {

    private final SysUserMapper sysUserMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public SysUser create(SysUser user) {
        return sysUserMapper.insert(user) > 0 ? user : null;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result<SysUser> delete(SysUser user) {
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(SysUser::getUserName, user.getUserName());
        return sysUserMapper.delete(queryWrapper) > 0 ? new Result<SysUser>().ok(user) : new Result<SysUser>().error("删除用户失败");
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result<SysUser> update(SysUser user) {
        return sysUserMapper.updateById(user) > 0 ? new Result<SysUser>().ok(user) : new Result<SysUser>().error("更新用户失败");
    }

    @Override
    public SysUser findByUserName(String userName) {
        return sysUserMapper.selectOne(new QueryWrapper<SysUser>().lambda().eq(SysUser::getUserName, userName));
    }

    @Override
    public boolean registerEmailExist(String email) {
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(SysUser::getEmail, email);
        return sysUserMapper.selectOne(queryWrapper) != null;
    }
}

2.3 编写Email工具类实现邮件的发送

  1. 定义一个邮件发送信息传输类
/** * 邮件信息 * @author zhuhuix * @date 2021-07-19 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmailDto {

    /** * 发送邮箱列表 */
    @NotEmpty
    private List<String> tos;

    /** * 主题 */
    @NotBlank
    private String subject;

    /** * 内容 */
    @NotBlank
    private String content;
}
  1. 定义邮件发送服务接口及编写实现类
/** * 邮箱服务接口 * * @author zhuhuix * @date 2021-07-19 */
public interface EmailService {

    /** * 发送邮件 * @param emailDto 邮箱列表 */
    void send(EmailDto emailDto);
}
/** * 邮箱发送接口实现类 * * @author zhuhuix * @date 2021-07-19 */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class EmailServiceImpl implements EmailService {

    @Value("${spring.mail.email}")
    private String email;
    @Value("${spring.mail.host}")
    private String host;
    @Value("${spring.mail.port}")
    private String port;
    @Value("${spring.mail.username}")
    private String username;
    @Value("${spring.mail.password}")
    private String password;

    @Override
    public void send(EmailDto emailDto) {

        // 读取邮箱配置
        if (email == null || host == null || port == null || username == null || password == null) {
            throw new RuntimeException("邮箱配置异常");
        }

        // 设置
        MailAccount account = new MailAccount();
        account.setHost(host);
        account.setPort(Integer.parseInt(port));
        // 设置发送人邮箱
        account.setFrom(username + "<" + email + ">");
        // 设置发送人名称
        account.setUser(username);
        // 设置发送授权码
        account.setPass(password);
        account.setAuth(true);
        // ssl方式发送
        account.setSslEnable(true);
        // 使用安全连接
        account.setStarttlsEnable(true);

        // 发送邮件
        try {
            int size = emailDto.getTos().size();
            Mail.create(account)
                    .setTos(emailDto.getTos().toArray(new String[size]))
                    .setTitle(emailDto.getSubject())
                    .setContent(emailDto.getContent())
                    .setHtml(true)
                    //关闭session
                    .setUseGlobalSession(false)
                    .send();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
    }
}
  • 注意:该实现类中使用了hutool工具包的Mail与MailAccount,具体要查阅hutool相关API:

  1. 在项目配置文件中配置好邮箱信息
# application.yml
server:
  port: 8000

spring:
    
    mail:
      email: xxxx@163.com
      host: smtp.163.com
      port: 465
      username: xxxx
      # 授权密码, 非邮箱密码,授权码是用于登录第三方邮件客户端的专用密码。
      password: xxxxxxxx
  1. 设计邮箱验证码模板
  • 为了让用户收到美观的邮箱验证码邮件,我们设计一个模板,该模板需要将后台动态生成的验证码传入,生成HTML内容后向用户邮箱进行发送。
    – 加入模板引擎依赖
<!--模板引擎-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>

– 设置模板

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <style> @page { margin: 0; } </style>
</head>
<body>
<div class="header">
    <div style="padding: 10px;padding-bottom: 0px;">
        <p style="margin-bottom: 10px;padding-bottom: 0px;">尊敬的用户,您好:</p>
        <p style="text-indent: 2em; margin-bottom: 10px;">您正在申请邮箱验证,您的验证码为:</p>
        <p class="code-text">${code}</p>
        <div class="footer">
        </div>
    </div>
</div>
</body>
</html>

<style lang="css"> body { margin: 0px; padding: 0px; font: 100% SimSun, Microsoft YaHei, Times New Roman, Verdana, Arial, Helvetica, sans-serif; color: #000; } .header { height: auto; width: 820px; min-width: 820px; margin: 0 auto; margin-top: 20px; border: 1px solid #eee; } .code-text { text-align: center; font-family: Times New Roman; font-size: 22px; color: #C60024; padding: 20px 0px; margin-bottom: 10px; font-weight: bold; background: #ebebeb; } .footer { margin: 0 auto; z-index: 111; width: 800px; margin-top: 30px; border-top: 1px solid #DA251D; } </style>

– 编写调用模板引擎发送邮件的方法

public void sendMailCode(String email) {

        // 获取发送邮箱验证码的HTML模板
        TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH));
        Template template = engine.getTemplate("email-code.ftl");

        
        // 发送验证码
        emailService.send(new EmailDto(Collections.singletonList(email),
                "邮箱验证码", template.render(Dict.create().set("code", code))));

    }

2.4 验证码邮件发送与验证码后台验证

  • 接下来我们要编写两个后台接口:1、验证码邮件发送;2、用户注册:判断注册时用户填写的验证码是否有效。

/** * 登录授权服务接口 * * @author zhuhuix * @date 2020-04-07 */
public interface AuthService {

    /** * 向指定邮箱发送验证码 * * @param email 邮箱号 */
    void sendMailCode(String email);

    /** * 注册 * * @param authUserDto 认证用户请求信息 * @return 是否成功 */
    boolean register(AuthUserDto authUserDto);

}

```java
/** * 认证用户 * * @author zhuhuix * @date 2020-04-03 */
@ApiModel(value = "授权用户信息")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthUserDto {

    @ApiModelProperty(value = "用户名")
    private String userName;

    @ApiModelProperty(value = "密码")
    private String password;

    @ApiModelProperty(value = "临时登录凭证")
    private String code;

    @ApiModelProperty(value = "邮箱")
    private String email ;

}
  1. 验证码邮件发送的具体实现:需要将发送的验证码放入Redis缓存,并设置过期时间

/** * 授权登录接口实现类 * * @author zhuhuix * @date 2020-06-15 */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class AuthServiceImpl implements AuthService {

	// 验证码放入redis缓存过期时间
    @Value("${code.expiration}")
    private Long expiration;

    private final RedisUtils redisUtils;
    private final EmailService emailService;
    private final SysUserService sysUserService;

    @Override
    public void sendMailCode(String email) {

        // 查看注册邮箱是否存在
        if (sysUserService.registerEmailExist(email)) {
            throw new RuntimeException("注册邮箱已存在");
        }

        // 获取发送邮箱验证码的HTML模板
        TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH));
        Template template = engine.getTemplate("email-code.ftl");

        // 从redis缓存中尝试获取验证码
        Object code = redisUtils.get(email);
        if (code == null) {
            // 如果在缓存中未获取到验证码,则产生6位随机数,放入缓存中
            code = RandomUtil.randomNumbers(6);
            if (!redisUtils.set(email, code, expiration)) {
                throw new RuntimeException("后台缓存服务异常");
            }
        }
        // 发送验证码
        emailService.send(new EmailDto(Collections.singletonList(email),
                "邮箱验证码", template.render(Dict.create().set("code", code))));

    }
}
  1. 用户注册:判断注册时用户填写的验证码是否有效。
  • 在用户注册过程中,我们需要把用户上传信息中的验证码与缓存中的验证码进行比对验证
/** * 授权登录接口实现类 * * @author zhuhuix * @date 2020-06-15 */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class AuthServiceImpl implements AuthService {

    @Value("${rsa.private-key}")
    private String privateKey;

    private final RedisUtils redisUtils;
    private final EmailService emailService;
    private final PasswordEncoder passwordEncoder;
    private final SysUserService sysUserService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean register(AuthUserDto authUserDto) {
        // 通过email获取redis中的code
        Object value = redisUtils.get(authUserDto.getEmail());
        if (value == null || !value.toString().equals(authUserDto.getCode())) {
            throw new RuntimeException("无效验证码");
        } else {
            redisUtils.del(authUserDto.getEmail());
        }

        // 如果前端没有传入用户名,则以邮箱号作为用户名进行注册
        String userName = StringUtils.isEmpty(authUserDto.getUserName()) ? authUserDto.getEmail() : authUserDto.getUserName();

        if (userService.findByUserName(userName) != null) {
            throw new RuntimeException("用户名已存在");
        }

        // 创建用户
        SysUser sysUser = new SysUser();
        sysUser.setUserName(userName);
        try {
            sysUser.setPassword(passwordEncoder.encode(RsaUtils.decryptByPrivateKey(privateKey, authUserDto.getPassword())));
        } catch (Exception e) {
            throw new RuntimeException("注册密码异常");
        }
        sysUser.setEmail(authUserDto.getEmail());
        return sysUserService.create(sysUser) != null;
    } 
}
  1. 编写API接口
/** * api登录授权 * * @author zhuhuix * @date 2020-03-30 */
@Slf4j
@RestController
@RequestMapping("/api/auth")
@Api(tags = "系统授权接口")
public class AuthController {

    private final AuthService authService;

    public AuthController(AuthService authService) {
        this.authService = authService;
    }

    @ApiOperation("发送邮箱验证码")
    @PostMapping(value = "/getEmailCode")
    public ResponseEntity<Object> getEmailCode(@RequestParam String email) {
        authService.sendMailCode(email);
        return new ResponseEntity<>(HttpStatus.OK);
    }

    @ApiOperation("注册")
    @PostMapping(value = "/register")
    public ResponseEntity<Object> register(@RequestBody AuthUserDto authUserDto) {
        return ResponseEntity.ok(authService.register(authUserDto));
    }

}

2.5 前后端联调测试

  • 终于到了前后端联调的步骤了,我们先准备好前端页面,具体可参考前面的文章
    《手把手教你使用Vue搭建注册登录界面及前端源码》

  • 然后编写前端访问后端api的接口
import request from '@/utils/request'

export function getEmailCode(email) {
  return request({
    url: '/api/auth/getEmailCode?email=' + email,
    method: 'post'
  })
}

export function register(data) {
  return request({
    url: '/api/auth/register',
    method: 'post',
    data
  })
}
  • 接下来进行联调

  • 收取到的验证码邮件

  • 后台注册成功后的表信息

三、源码

相关文章