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;

}
相关推荐
你怎么知道我是队长11 分钟前
C语言---文件读写
java·c语言·开发语言
wszy18091 小时前
外部链接跳转:从 App 打开浏览器的正确姿势
java·javascript·react native·react.js·harmonyos
期待のcode1 小时前
认识Java虚拟机
java·开发语言·jvm
raining_peidx1 小时前
xxljob源码
java·开发语言
肥猪猪爸1 小时前
双重检查锁(DCL)与 volatile 的关键作用
java·开发语言·单例模式
yaoxin5211231 小时前
289. Java Stream API - 从字符串的字符创建 Stream
java·开发语言
浮游本尊1 小时前
Java学习第35天 - 分布式系统深入与大数据处理
java
2301_780669862 小时前
Set集合、HashSet集合的底层原理
java
你曾经是少年2 小时前
Java 关键字
java
海南java第二人2 小时前
SpringBoot启动流程深度解析:从入口到容器就绪的完整机制
java·开发语言