springboot - 邮箱验证码登录

参考文章:邮箱验证码登录的简易实现

1. 引入依赖

XML 复制代码
<!-- 1.邮箱依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
    <version>2.7.2</version>
</dependency>
<!-- 2.模板引擎 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- 3.validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 4.HuTool -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.5</version>
</dependency>
<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 说明
    • **2.模板引擎**是用于美化发送的邮件样式
    • 本邮件验证登录以qq邮箱作为发送箱,因此需要引入qq邮箱官网的依赖支持**3.validation**

2. 配置

XML 复制代码
spring:
  mail:
    host: smtp.qq.com 
    username: # 用来发送邮件的邮箱账号
    password: # 授权码
    default-encoding: UTF-8
  redis:
    database: 1
    host: 127.0.0.1
    port: 6379
    password: # 密码
  • 说明
    • 需要登录自己的邮箱账号,在设置-账户-POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务那开启 POP3/SMTP 服务,同时获取授权码
    • 引入 redis 数据库是为了保存用户的验证码和校验通过后的状态

3. 使用示例

a. util,用于生成验证码
  • 文件名:VerificationCodeUtil.java
  • 存放位置:src/main/java/com/yu/cloudpicturebackend/utils/VerificationCodeUtil.java
java 复制代码
package com.yu.cloudpicturebackend.utils;

import java.util.Random;

public class VerificationCodeUtil {
    /**
     * 随机生成验证码
     *
     * @param length 长度为 4 位或者 6 位
     * @return
     */
    public static Integer getVerificationCode(int length) {
        Integer code = null;
        if (length == 4) {
            code = new Random().nextInt(9999);//生成随机数,最大为9999
            if (code < 1000) {
                code = code + 1000;//保证随机数为4位数字
            }
        } else if (length == 6) {
            code = new Random().nextInt(999999);//生成随机数,最大为999999
            if (code < 100000) {
                code = code + 100000;//保证随机数为6位数字
            }
        } else {
            throw new RuntimeException("只能生成4位或6位数字验证码");
        }
        return code;
    }

    /**
     * 随机生成指定长度字符串验证码
     *
     * @param length 长度
     * @return
     */
    public static String generateValidateCode4String(int length) {
        Random rdm = new Random();
        String hash = Integer.toHexString(rdm.nextInt());
        return hash.substring(0, length);
    }
}
b. controller层
  • 文件名:AuthController.java
  • 存放位置:src/main/java/com/yu/cloudpicturebackend/controller/AuthController.java
java 复制代码
package com.yu.cloudpicturebackend.controller;

import cn.hutool.core.util.ObjUtil;
import com.yu.cloudpicturebackend.common.ResultUtils;
import com.yu.cloudpicturebackend.exception.BaseResponse;
import com.yu.cloudpicturebackend.exception.BusinessException;
import com.yu.cloudpicturebackend.exception.ErrorCode;
import com.yu.cloudpicturebackend.model.dto.SendEmailRequest;
import com.yu.cloudpicturebackend.model.dto.VerifyEmailRequest;
import com.yu.cloudpicturebackend.service.AuthService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/auth")
@Api(tags = "授权接口")
public class AuthController {

    @Autowired
    private AuthService authService;

    @PostMapping("/email/send")
    @ApiOperation("发送邮箱验证码")
    public BaseResponse<String> sendEmail(@RequestBody SendEmailRequest sendEmailRequest) {
        if (ObjUtil.isEmpty(sendEmailRequest)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        authService.handleSend(sendEmailRequest);
        return ResultUtils.success("验证码发送成功");
    }

    @PostMapping("/email/verify")
    @ApiOperation("校验邮箱验证码")
    public BaseResponse<String> verifyCode(@RequestBody VerifyEmailRequest verifyEmailRequest) {
        if (ObjUtil.isEmpty(verifyEmailRequest)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // 校验邮箱验证码
        authService.verifyCode(verifyEmailRequest);
        return ResultUtils.success("校验成功");
    }

}
c. service层
  • 文件名:AuthService.java
  • 存放位置:src/main/java/com/yu/cloudpicturebackend/service/AuthService.java
1. 发送邮箱验证码
java 复制代码
package com.yu.cloudpicturebackend.service;

import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.mail.Mail;
import cn.hutool.extra.mail.MailAccount;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import com.yu.cloudpicturebackend.exception.BusinessException;
import com.yu.cloudpicturebackend.exception.ErrorCode;
import com.yu.cloudpicturebackend.model.dto.SendEmailRequest;
import com.yu.cloudpicturebackend.model.dto.VerifyEmailRequest;
import com.yu.cloudpicturebackend.model.entity.EmailRo;
import com.yu.cloudpicturebackend.utils.VerificationCodeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Service
@Slf4j
public class AuthService {

    //获取配置文件的信息
    @Value("${spring.mail.host}")
    private String host;
    @Value("${spring.mail.username}")
    private String username;
    @Value("${spring.mail.password}")
    private String password;

    // springboot 整合 redis
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public final String SEND_KEY_PRE = "email:send:";
    public final String STATUS_KEY_PRE = "email:verified:";

    /**
     * 处理发送邮件验证码逻辑
     *
     * @param sendEmailRequest
     */
    public void handleSend(SendEmailRequest sendEmailRequest) {
        String email = sendEmailRequest.getEmail();
        // 校验邮箱格式
        isValidateEmail(email);
        if (StrUtil.isBlank(email)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "邮箱不能为空");
        }
        // 获取发送邮箱验证码的 HTML 模板
        TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("templates",
                TemplateConfig.ResourceMode.CLASSPATH));
        Template template = engine.getTemplate("email-code.ftl");
        // 获取验证码
        String code = VerificationCodeUtil.getVerificationCode(6).toString();

        sendEmail(new EmailRo(email, "邮箱验证码登录", template.render(Dict.create().set("code", code)), code));
    }

    /**
     * 发送邮箱验证码
     *
     * @param emailRo
     */
    public void sendEmail(EmailRo emailRo) {
        // 设置
        MailAccount account = new MailAccount();
        account.setHost(host);
        // 设置发送人信息
        account.setFrom("鱼籽云图库" + "<" + username + ">");
        // 设置发送人名称
        account.setUser(username);
        // 设置发送授权码
        account.setPass(password);
        account.setAuth(true);
        // ssl方式发送
        account.setSslEnable(true);
        // 使用安全连接
        account.setStarttlsEnable(true);

        // 发送邮件
        try {
            Mail.create(account)
                    .setTos(emailRo.getEmail())
                    .setTitle(emailRo.getSubject())
                    .setContent(emailRo.getContent())
                    .setHtml(true)
                    // 关闭session
                    .setUseGlobalSession(false)
                    .send();
            // 将验证码保存到 redis
            stringRedisTemplate.opsForValue().set(SEND_KEY_PRE + emailRo.getEmail(), emailRo.getCode(), 120, TimeUnit.SECONDS);
        } catch (Exception e) {
            log.error(e.toString());
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "邮件发送失败");
        }
    }


    /**
     * 校验邮箱格式
     *
     * @param email
     * @return
     */
    public void isValidateEmail(String email) {
        // 只允许字母、数字和 _+&*- 在本地部分
        // 要求域名部分有2-7个字母的顶级域名
        // 更简单但可能排除一些有效的邮箱格式
        String regex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(email);
        if (!matcher.matches()) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "邮箱格式不正确");
        }
    }

}
2. 校验邮箱验证码
java 复制代码
/**
 * 校验邮件验证码
 *
 * @param verifyEmailRequest
 * @return
 */
 public void verifyCode(VerifyEmailRequest verifyEmailRequest) {
     String email = verifyEmailRequest.getEmail();
     String code = verifyEmailRequest.getCode();
     if (StrUtil.isBlank(code)) {
         throw new BusinessException(ErrorCode.PARAMS_ERROR, "验证码不能为空");
     }
     // 校验邮箱格式
     isValidateEmail(email);
     // 从 Redis 获取存储的验证码
     String storedCode = stringRedisTemplate.opsForValue().get(email);
     if (storedCode == null || !code.equals(storedCode)) {
         throw new BusinessException(ErrorCode.PARAMS_ERROR, "验证码错误");
     }
     // 校验通过,设置验证状态(有效期 10 分钟)
     String statusKey = STATUS_KEY_PRE + verifyEmailRequest.getEmail();
     stringRedisTemplate.opsForValue().set(statusKey, "true", 10, TimeUnit.MINUTES);
 }
d. 模板文件
  • 文件名:email-code.ftl
  • 存放位置:src/main/resources/templates/email-code.ftl
html 复制代码
<!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"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>邮箱验证码</title>
    <style type="text/css">
        /* 重置样式 */
        body, p, div {
            margin: 0;
            padding: 0;
        }

        body {
            width: 100% !important;
            -webkit-text-size-adjust: 100%;
            -ms-text-size-adjust: 100%;
            margin: 0;
            padding: 0;
            font-family: 'Microsoft YaHei', SimSun, Arial, sans-serif;
            line-height: 1.6;
            color: #333333;
            background-color: #f6f9fc;
        }

        /* 外层容器 */
        .email-container {
            max-width: 600px;
            margin: 0 auto;
            background-color: #ffffff;
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
        }

        /* 头部样式 */
        .email-header {
            background: linear-gradient(135deg, #4A6EE0 0%, #6A11CB 100%);
            color: white;
            padding: 25px 30px;
            text-align: center;
        }

        .email-header h1 {
            font-size: 24px;
            margin: 0;
            font-weight: 600;
        }

        /* 内容区域 */
        .email-content {
            padding: 30px;
        }

        .greeting {
            font-size: 16px;
            margin-bottom: 20px;
            color: #555;
        }

        .instruction {
            font-size: 15px;
            line-height: 1.7;
            margin-bottom: 25px;
            color: #666;
        }

        /* 验证码样式 */
        .verification-code {
            background: #f8f9fa;
            border-left: 4px solid #4A6EE0;
            padding: 20px;
            margin: 25px 0;
            text-align: center;
            border-radius: 0 6px 6px 0;
        }

        .code-text {
            font-size: 48px;
            font-weight: 700;
            letter-spacing: 5px;
            color: #4A6EE0;
            margin: 10px 0;
            font-family: 'Courier New', monospace;
        }

        .code-expiry {
            font-size: 14px;
            color: #888;
            margin-top: 10px;
        }

        /* 提示信息 */
        .tips {
            background-color: #f0f7ff;
            border-radius: 6px;
            padding: 15px;
            margin: 25px 0;
            font-size: 14px;
            color: #555;
            border-left: 3px solid #4A6EE0;
        }

        .tips p {
            margin: 5px 0;
        }

        /* 底部样式 */
        .email-footer {
            background-color: #f8f9fa;
            padding: 20px 30px;
            text-align: center;
            font-size: 13px;
            color: #888;
            border-top: 1px solid #eaeaea;
        }

        .company-info {
            margin-bottom: 10px;
        }

        .support-info {
            margin-top: 10px;
            font-size: 12px;
        }

        /* 响应式设计 */
        @media only screen and (max-width: 600px) {
            .email-container {
                width: 100% !important;
                border-radius: 0;
            }

            .email-content, .email-header, .email-footer {
                padding: 20px !important;
            }

            .code-text {
                font-size: 28px !important;
                letter-spacing: 3px !important;
            }
        }
    </style>
</head>
<body>
<center>
    <table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="#f6f9fc">
        <tr>
            <td align="center" valign="top">
                <table class="email-container" border="0" cellpadding="0" cellspacing="0" width="600">
                    <!-- 头部 -->
                    <tr>
                        <td class="email-header">
                            <h1>邮箱验证</h1>
                        </td>
                    </tr>

                    <!-- 内容区域 -->
                    <tr>
                        <td class="email-content">
                            <p class="greeting">尊敬的用户,您好:</p>

                            <p class="instruction">您正在申请邮箱验证,为了确保是您本人操作,请在验证页面输入以下验证码:</p>

                            <div class="verification-code">
                                <div class="code-text">${code}</div>
                                <p class="code-expiry">验证码有效期为15分钟,请尽快使用</p>
                            </div>

                            <div class="tips">
                                <p><strong>温馨提示:</strong></p>
                                <p>• 请勿将验证码告知他人,工作人员不会向您索要验证码</p>
                                <p>• 如非本人操作,请忽略此邮件</p>
                                <p>• 如有疑问,请联系我们的客服团队</p>
                            </div>

                            <p class="instruction">感谢您使用我们的服务!</p>
                        </td>
                    </tr>

                    <!-- 底部 -->
                    <tr>
                        <td class="email-footer">
                            <div class="company-info">鱼籽科技有限公司</div>
                            <div>此为系统邮件,请勿直接回复</div>
                            <div class="support-info">如需帮助,请联系客服邮箱:1319764245@qq.com</div>
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
    </table>
</center>
</body>
</html>
e. dto
  • 文件名:EmailRequest.java
  • 存放位置:src/main/java/com/yu/cloudpicturebackend/model/dto/EmailRequest.java
java 复制代码
package com.yu.cloudpicturebackend.model.dto;

import lombok.Data;

import java.io.Serializable;

@Data
public class EmailRequest implements Serializable {

    private static final long serialVersionUID = 3662164451473225507L;

    /**
     * 邮箱
     */
    private String email;

}
f. model
  • 文件名:EmailRo
  • 存放位置:src/main/java/com/yu/cloudpicturebackend/model/entity/EmailRo.java
java 复制代码
package com.yu.cloudpicturebackend.model.entity;

import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 邮件请求对象
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmailRo implements Serializable {

    private static final long serialVersionUID = -8620593378368489081L;

    /**
     * 收件人邮箱地址
     */
    private String email;

    /**
     * 邮件主题
     */
    private String subject;

    /**
     * 邮件内容
     */
    private String content;

    /**
     * 验证码
     */
    private Integer code;

}
相关推荐
xcl09254 小时前
北京24小时自助健身房系统软件开发实战:从架构到部署全流程指南
java·spring boot·架构
一 乐5 小时前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长6 小时前
w214基于jsp知道特产网
java·intellij-idea
步行cgn6 小时前
Spring 注解使用详解
java·spring
一条小小yu6 小时前
Spring IoC的理解
java·后端·spring
落魄实习生6 小时前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈7 小时前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
自强的小白8 小时前
核心功能(Service接口)
java·mybatis
乌暮9 小时前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习