前端调用后端接口(SpringBoot3实战:快速实现安全可靠的邮箱验证功能)

前端调用后端接口(SpringBoot3实战:快速实现安全可靠的邮箱验证功能)
SpringBoot3实战:快速实现安全可靠的邮箱验证功能

随着互联网的发展,越来越多的应用需要确保用户的注册行为真实有效。其中一种常见的方式就是通过发送邮件进行身份认证——即我们常说的「邮箱验证」机制。本文将带你一步步搭建一个基于 Spring Boot 3.x 版本并集成 SMTP 邮件服务来实现用户注册后的邮箱激活流程。

大体流程也很简单:用户注册完,系统自动发个带验证链接的邮件,用户点一下链接,就能完成账号激活。

一、环境准备

首先说下开发环境:

  • JDK 17+;
  • Spring Boot 3.0+(本文用 3.2.5);
  • 邮箱服务商:QQ邮箱(个人开发最方便,不用额外配置)

接下来直接在pom.xml中添加以下依赖::

<!-- 邮件发送核心依赖 -->    org.springframework.boot    spring-boot-starter-mail<!-- 工具类依赖 -->    cn.hutool    hutool-all    5.8.25<!-- Web依赖 -->    org.springframework.boot    spring-boot-starter-web<!-- MyBatis-Plus 核心依赖 -->    com.baomidou    mybatis-plus-spring-boot3-starter    3.5.16<!-- 数据库驱动 -->    com.mysql    mysql-connector-j    runtime

二、邮箱配置

不管用哪种邮箱,核心就一件事:获取“授权码”,注意不是邮箱密码!然后把邮件参数配置到application.yml里。这里以QQ邮箱为例,步骤很简单:

2.1 获取QQ邮箱授权码

  1. 登录自己的QQ邮箱,点击右上角「设置」,再选「账号与安全-安全设置」;
  2. 找到「POP3/IMAP/SMTP/Exchange/CardDAV 服务」,把「IMAP/SMTP服务」打开;
  3. 按照提示发送短信验证,验证通过后,会生成一个授权码,一定要保存好,配置的时候要用,忘了就得重新获取。

如下图:

2.2 配置application.yml

把邮箱、数据库这些参数,都配置到application.yml里:

spring:  # 邮件配置  mail:    # 邮件服务器地址(QQ邮箱固定是smtp.qq.com,163邮箱是smtp.163.com)    host: smtp.qq.com    # 发送方邮箱(换成你自己的QQ邮箱,比如123456789@qq.com)    username: 123456789@qq.com    # 刚才获取的授权码(替换成你自己的,别直接复制我的)    password: abcdefghijklmnop    # 编码格式,固定UTF-8,避免中文乱码    default-encoding: UTF-8    # 端口配置(QQ邮箱SMTP端口465,必须开SSL;163邮箱用25或465都行)    port: 465    properties:      mail:        smtp:          auth: true          ssl:            enable: true          starttls:            enable: true            required: true  # 数据库配置(MyBatis-Plus必须配,不然连不上数据库)  datasource:    dataSourceClassName: com.zaxxer.hikari.HikariDataSource     driver-class-name: com.mysql.cj.jdbc.Driver    # 替换成自己的数据库地址、用户名、密码,数据库要提前建好    url: jdbc:mysql://localhost:3306/email_demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true    username: root    password: 123456# MyBatis-Plus 配置(可选,按需调整,新手默认这样就行)mybatis-plus:  mapper-locations: classpath:mapper/*.xml  # mapper.xml文件路径,没有就不用改  type-aliases-package: com.example.emaildemo.entity  # 实体类包路径,换成你自己的项目包名  configuration:    map-underscore-to-camel-case: true  # 开启下划线转驼峰,不用手动对应字段名    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 控制台打印SQL,开发时打开,上线关闭# 自定义配置(可根据自己需求改,不用动核心)email:  # 验证链接有效期(单位:分钟,这里设24小时,1440分钟,够长了)  validate-expire: 1440  # 前端跳转地址(用户点击验证链接后,跳转到你的前端页面,换成自己的)  front-redirect-url: http://localhost:8080/register/validate  # 邮件发送者昵称(收件箱显示的发件人名称,比如“XX系统通知”)  sender-name: 系统通知

三、核心功能实现

核心流程很简单:生成唯一的验证token → 拼接成验证链接 → 发邮件给用户 → 用户点击链接,前端调用后端接口 → 后端校验token是否有效 → 校验通过,激活账。

3.1 实体类

用户表核心字段就几个:邮箱、验证状态、验证token、token过期时间:

import com.baomidou.mybatisplus.annotation.*;import lombok.Data;import java.time.LocalDateTime;@Data@TableName("sys_user") // 对应数据库表名public class User {    // 主键自增    @TableId(type = IdType.AUTO)    private Long id;    // 邮箱(唯一,作为注册账号,添加唯一约束)    private String email;    // 密码(加密存储,本文不涉及密码加密,重点在邮箱验证)    private String password;    // 邮箱验证状态:0-未验证,1-已验证(设置默认值)    @TableField(value = "email_validated", fill = FieldFill.INSERT)    private Integer emailValidated = 0;    // 验证token(唯一,用于校验链接有效性)    private String validateToken;    // token过期时间    private LocalDateTime tokenExpireTime;    // 创建时间    @TableField(fill = FieldFill.INSERT)    private LocalDateTime createTime;}      

3.2 工具类(生成唯一token)

token要保证唯一,不然会出现验证混乱的问题。我直接用hutool工具类的方法:

import cn.hutool.core.util.IdUtil;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import java.time.LocalDateTime;import java.time.temporal.ChronoUnit;@Componentpublic class EmailValidateUtil {    // 从配置文件读取token有效期,不用硬编码,方便修改    @Value("${email.validate-expire}")    private Integer validateExpire;    // 生成唯一验证token,hutool的simpleUUID,去掉了横线,更简洁    public String generateValidateToken() {        return IdUtil.simpleUUID();    }    // 计算token过期时间,当前时间加上配置的有效期    public LocalDateTime calculateExpireTime() {        return LocalDateTime.now().plus(validateExpire, ChronoUnit.MINUTES);    }    // 校验token是否过期,当前时间超过过期时间,就失效    public boolean isTokenExpired(LocalDateTime expireTime) {        return LocalDateTime.now().isAfter(expireTime);    }}      

3.3 邮件发送服务

邮件发送是核心,这里要封装成一个服务,支持HTML格式(因为验证链接要能点击,普通文本不行),还要处理发送异常:

import jakarta.mail.MessagingException;import jakarta.mail.internet.MimeMessage;import org.springframework.beans.factory.annotation.Value;import org.springframework.mail.javamail.JavaMailSender;import org.springframework.mail.javamail.MimeMessageHelper;import org.springframework.stereotype.Service;@Servicepublic class EmailSendService {    // 注入SpringBoot自动配置的邮件发送对象    private final JavaMailSender javaMailSender;    // 从配置文件读取发送方邮箱    @Value("${spring.mail.username}")    private String senderEmail;    // 从配置文件读取发送者昵称    @Value("${email.sender-name}")    private String senderName;    // 从配置文件读取前端跳转地址    @Value("${email.front-redirect-url}")    private String frontRedirectUrl;    // 构造方法注入(SpringBoot3推荐构造方法注入,避免字段注入的弊端)    public EmailSendService(JavaMailSender javaMailSender) {        this.javaMailSender = javaMailSender;    }    /**     * 发送邮箱验证邮件     * @param recipientEmail 收件人邮箱     * @param validateToken 验证token     * @throws MessagingException 邮件发送异常     */    public void sendValidateEmail(String recipientEmail, String validateToken) throws Exception{        // 1. 创建MimeMessage对象(用于发送HTML邮件)        MimeMessage mimeMessage = javaMailSender.createMimeMessage();        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");            // 2. 设置邮件基本信息        helper.setFrom(senderEmail, senderName); // 发件人(邮箱+昵称)        helper.setTo(recipientEmail); // 收件人邮箱        helper.setSubject("账号邮箱验证通知"); // 邮件标题            // 3. 拼接验证链接(前端地址+token参数,前端拿到token后调用后端验证接口)        String validateUrl = frontRedirectUrl + "?token=" + validateToken;            // 4. 构建HTML邮件内容(简洁明了,避免冗余,适配手机和电脑端)        String emailContent = "" +                "

尊敬的用户,您好!

" + "

感谢您的注册,请点击下方链接完成邮箱验证,激活您的账号:

" + "点击验证邮箱" + "

温馨提示:链接有效期为24小时,过期后请重新申请验证

" + "

若您未进行注册操作,请忽略此邮件,感谢您的理解!

" + ""; // 5. 设置邮件内容(HTML格式) helper.setText(emailContent, true); // 6. 发送邮件 javaMailSender.send(mimeMessage); }}

3.4 业务逻辑层

这一层是核心,把注册、生成token、发邮件、验证token这些逻辑整合起来,还要处理重复注册、邮件发送失败等异常,加上事务管理,确保数据一致——比如邮件发失败了,用户注册也得回滚,不能出现“注册成功但收不到验证邮件”的情况:

前端调用后端接口(SpringBoot3实战:快速实现安全可靠的邮箱验证功能)

import cn.iocoder.boot.entity.User;import cn.iocoder.boot.mapper.UserMapper;import cn.iocoder.boot.utils.EmailValidateUtil;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import jakarta.annotation.Resource;import jakarta.mail.MessagingException;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;@Servicepublic class UserService {    @Resource    private UserMapper userMapper;    @Resource    private EmailValidateUtil emailValidateUtil;    @Resource    private EmailSendService emailSendService;    /**     * 用户注册+发送验证邮件     * @param email 注册邮箱     * @param password 注册密码(实际开发中需加密,如BCrypt)     */    @Transactional(rollbackFor = Exception.class) // 事务管理,发送失败回滚注册操作    public void registerAndSendValidateEmail(String email, String password) throws MessagingException {        // 1. 校验邮箱是否已注册(避免重复注册)        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();        queryWrapper.eq(User::getEmail, email);        User existUser = userMapper.selectOne(queryWrapper);        if (existUser != null) {            throw new RuntimeException("该邮箱已注册,请更换邮箱重试");        }            // 2. 生成验证token和过期时间        String validateToken = emailValidateUtil.generateValidateToken();        LocalDateTime tokenExpireTime = emailValidateUtil.calculateExpireTime();            // 3. 构建用户对象(未验证状态)        User user = new User();        user.setEmail(email);        user.setPassword(password); // 实际开发中需加密:BCrypt.hashpw(password, BCrypt.gensalt())        user.setValidateToken(validateToken);        user.setTokenExpireTime(tokenExpireTime);        user.setCreateTime(LocalDateTime.now());            // 4. 保存用户(事务管理,后续发送邮件失败会回滚)        userMapper.insert(user);            // 5. 发送验证邮件        try {            emailSendService.sendValidateEmail(email, validateToken);        } catch (Exception e) {            throw new RuntimeException(e);        }    }    /**     * 邮箱验证(用户点击链接后,前端调用此接口)     * @param token 验证token     */    @Transactional(rollbackFor = Exception.class)    public void validateEmail(String token) {        // 1. 根据token查询用户(唯一)        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();        queryWrapper.eq(User::getValidateToken, token);        User user = userMapper.selectOne(queryWrapper);        if (user == null) {            throw new RuntimeException("验证链接无效,请重新申请验证");        }        // 2. 校验用户是否已验证        if (user.getEmailValidated() == 1) {            throw new RuntimeException("该邮箱已验证,无需重复操作");        }        // 3. 校验token是否过期        boolean isExpired = emailValidateUtil.isTokenExpired(user.getTokenExpireTime());        if (isExpired) {            throw new RuntimeException("验证链接已过期,请重新申请验证");        }        // 4. 更新用户验证状态(设置为已验证,清空token和过期时间)        user.setEmailValidated(1);        user.setValidateToken(null);        user.setTokenExpireTime(null);        userMapper.updateById(user);    }}

补充一下UserMapper接口,MyBatis-Plus的核心,继承BaseMapper:

import com.baomidou.mybatisplus.core.mapper.BaseMapper;import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface UserMapper extends BaseMapper {    }

3.5 控制层

控制层很简单,写两个接口,一个处理注册,一个处理邮箱验证,统一返回结果:

import jakarta.mail.MessagingException;import org.springframework.web.bind.annotation.*;@RestController@RequestMapping("/api/user")public class UserController {    private final UserService userService;    public UserController(UserService userService) {        this.userService = userService;    }    /**     * 用户注册接口     * @param email 注册邮箱     * @param password 注册密码     * @return 统一返回结果     */    @PostMapping("/register")    public Result register(@RequestParam String email, @RequestParam String password) {        try {            userService.registerAndSendValidateEmail(email, password);            return Result.success("注册成功,验证邮件已发送,请查收(记得看垃圾邮件箱)");        } catch (MessagingException e) {            return Result.error("注册失败,邮件发送异常,请重试");        } catch (RuntimeException e) {            return Result.error(e.getMessage());        }    }    /**     * 邮箱验证接口(用户点击链接后,前端调用此接口)     * @param token 验证token     * @return 统一返回结果     */    @GetMapping("/validate-email")    public Result validateEmail(@RequestParam String token) {        try {            userService.validateEmail(token);            return Result.success("邮箱验证成功,账号已激活,可正常登录");        } catch (RuntimeException e) {            return Result.error(e.getMessage());        }    }}    

在附上简化版本的统一返回结果类:

import lombok.Data;@Data// 统一返回结果类public class Result {    private Integer code; // 0-成功,1-失败    private String message;    private Object data;       public static Result success(String message) {        Result result = new Result();        result.setCode(0);        result.setMessage(message);        return result;    }    public static Result error(String message) {        Result result = new Result();        result.setCode(1);        result.setMessage(message);        return result;    }}

四、测试验证

测试步骤如下:

  1. 调用POST接口 http://localhost:8080/api/user/register,传入邮箱和密码;

  1. 查收验证邮件(含垃圾邮件箱),点击验证链接;

  1. 用Postman调用GET接口 http://localhost:8080/api/user/validate-email?token=xxx(替换链接中的token);

  1. 查看数据库sys_user表,emailValidated为1、token及过期时间为空,即验证成功。

补充一下sys_user表的建表SQL(MySQL8+):

CREATE TABLE `sys_user` (  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',  `email` varchar(100) NOT NULL COMMENT '用户邮箱(唯一)',  `password` varchar(100) NOT NULL COMMENT '用户密码(加密存储)',  `email_validated` int NOT NULL DEFAULT '0' COMMENT '邮箱验证状态:0-未验证,1-已验证',  `validate_token` varchar(100) DEFAULT NULL COMMENT '邮箱验证token',  `token_expire_time` datetime DEFAULT NULL COMMENT 'token过期时间',  `create_time` datetime NOT NULL COMMENT '创建时间',  PRIMARY KEY (`id`),  UNIQUE KEY `uk_email` (`email`) COMMENT '邮箱唯一约束') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';    

五、常见问题排查

问题1:邮件发送报“Authentication failed”异常

  • 解决方案:大概率是授权码错了,重新获取授权码,确认配置文件里的password填的是授权码,不是邮箱密码;

问题2:链接点击后提示“验证链接无效”

  • 解决方案:检查token是不是和数据库里的validate_token一致,有没有多空格;MyBatis-Plus的话,检查查询条件,别把字段名和数据库列名搞混了;

问题3:163邮箱发送失败

  • 解决方案:把host改成smtp.163.com,port改成25,授权码获取方式和QQ邮箱一样,开启SMTP服务就行;

六、优化建议

上面的代码,能满足基础的邮箱验证需求,实际开发中,还可以做一些优化,贴合生产环境,提升用户体验和安全性,我列几个常用的:

  1. 限流控制:给注册接口和验证接口加限流,比如用Redis实现,避免恶意请求刷接口,导致服务器压力过大;
  2. 邮件重试:邮件发送失败很常见(比如网络波动),加个重试机制,比如用Spring的Retry注解,重试3次,减少发送失败的概率;
  3. 日志记录:在邮件发送、验证的关键步骤加日志(用SLF4J),后面排查问题的时候,能快速定位问题所在;
  4. 多邮箱支持:封装一下邮箱配置,支持QQ、163、企业邮箱,通过配置文件切换,不用改代码,更灵活。

七、总结

其实邮箱验证的核心逻辑一点都不复杂,就是生成唯一token、拼接链接、发邮件、校验token这几步,关键在于细节处理——比如token的唯一性、过期时间的计算、事务管理。

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

最新文章

热门文章

本栏目文章