AI摘要

本文详细介绍了如何在Spring Boot应用中实现邮箱验证码注册和登录功能。包括功能概述、技术栈、实现步骤、邮件服务配置、创建邮件服务、验证码生成和存储、控制器实现、服务层实现、邮件模板、前端实现以及最佳实践。通过这些步骤,可以为用户提供安全、便捷的注册和登录方式。

在现代Web应用中,邮箱验证码注册和登录是非常常见的功能,不仅增强了账号安全性,也简化了用户的注册和登录流程。本文将详细介绍如何在Spring Boot应用中实现邮箱验证码注册和登录功能。

1. 功能概述

我们将实现以下功能:

  • 邮箱+验证码注册
  • 邮箱+验证码登录
  • 邮箱+密码登录(传统登录方式)

这些功能涵盖了用户注册和登录的主要场景,为用户提供了灵活的选择。

2. 技术栈

  • Spring Boot 3.2.0
  • Spring Security
  • Spring Mail
  • Thymeleaf (邮件模板)
  • MyBatis Flex
  • JWT (认证)

3. 实现步骤

3.1 邮件服务配置

首先,我们需要配置邮件服务。在application.properties文件中添加:

# 邮件服务配置
spring.mail.host=smtp.qq.com
spring.mail.port=587
spring.mail.username=your-email@qq.com
spring.mail.password=your-authorization-code
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

对于QQ邮箱,密码不是邮箱密码,而是授权码,需要在QQ邮箱设置中获取。

3.2 创建邮件服务

创建EmailService接口和实现类:

public interface EmailService {
    boolean sendVerificationEmail(String to, String subject, String code, String type);
}
@Service
public class EmailServiceImpl implements EmailService {

    private static final Logger logger = LoggerFactory.getLogger(EmailServiceImpl.class);

    private final JavaMailSender mailSender;
    private final TemplateEngine templateEngine;

    @Value("${spring.mail.username}")
    private String from;

    public EmailServiceImpl(JavaMailSender mailSender, TemplateEngine templateEngine) {
        this.mailSender = mailSender;
        this.templateEngine = templateEngine;
    }

    @Override
    public boolean sendVerificationEmail(String to, String subject, String code, String type) {
        try {
            // 准备Thymeleaf模板上下文
            Context context = new Context();
            context.setVariable("code", code);
            context.setVariable("type", type);
            context.setVariable("time", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

            // 处理模板生成HTML内容
            String htmlContent = templateEngine.process("email/verification-code", context);

            // 创建MIME消息
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(htmlContent, true);

            // 发送邮件
            mailSender.send(message);
            logger.info("邮件发送成功:{}", to);
            return true;
        } catch (Exception e) {
            logger.error("邮件发送失败", e);
            return false;
        }
    }
}

3.3 验证码生成和存储

为了管理验证码,我们需要创建生成验证码的方法和存储验证码的机制:

// 在UserServiceImpl中
private final Map<String, String> emailCodeCache = new HashMap<>();

private String generateRandomCode(int length) {
    StringBuilder sb = new StringBuilder();
    Random random = new Random();
    for (int i = 0; i < length; i++) {
        sb.append(random.nextInt(10));
    }
    return sb.toString();
}

@Override
public boolean sendEmailCode(String email, String type) {
    try {
        // 生成6位随机验证码
        String code = generateRandomCode(6);

        // 将验证码存入缓存
        emailCodeCache.put(email + ":" + type, code);

        // 验证码类型对应的邮件主题
        String subject;
        switch (type) {
            case "login":
                subject = "登录验证码";
                break;
            case "register":
                subject = "注册验证码";
                break;
            case "reset":
                subject = "重置密码验证码";
                break;
            default:
                subject = "验证码";
        }

        // 使用邮件服务发送验证码
        return emailService.sendVerificationEmail(email, subject, code, type);
    } catch (Exception e) {
        logger.error("发送邮箱验证码失败", e);
        return false;
    }
}

@Override
public String getEmailCode(String email, String type) {
    return emailCodeCache.get(email + ":" + type);
}

@Override
public void removeEmailCode(String email, String type) {
    emailCodeCache.remove(email + ":" + type);
}

在生产环境中,应该使用Redis等分布式缓存来存储验证码,并设置过期时间。

3.4 控制器实现

接下来,实现处理注册和登录请求的控制器方法:

发送邮箱验证码

@PostMapping("/send/email")
public ResponseEntity<?> sendEmailCode(
        @RequestParam @NotBlank @Email String email,
        @RequestParam @NotBlank String type) {
    logger.info("发送邮箱验证码,邮箱: {}, 类型: {}", email, type);
    boolean result = userService.sendEmailCode(email, type);

    if (result) {
        return ResponseEntity.ok().body(Map.of("code", 200, "message", "验证码发送成功"));
    } else {
        return ResponseEntity.badRequest().body(Map.of("code", 400, "message", "验证码发送失败"));
    }
}

邮箱+验证码注册

@PostMapping("/register")
public ResponseEntity<?> register(
        @RequestBody(required = false) Map<String, String> registerRequest,
        @RequestParam(required = false) String email,
        @RequestParam(required = false) String password,
        @RequestParam(required = false) String code,
        @RequestParam(required = false) String nickname) {

    // 优先使用URL参数,如果没有则使用请求体
    if (registerRequest != null) {
        if (email == null) email = registerRequest.get("email");
        if (password == null) password = registerRequest.get("password");
        if (code == null) code = registerRequest.get("code");
        if (nickname == null) nickname = registerRequest.get("nickname");
    }

    if (email == null || password == null || code == null) {
        return ResponseEntity.badRequest().body(Map.of("code", 400, "message", "邮箱、密码和验证码不能为空"));
    }

    logger.info("用户一步式注册,邮箱: {}", email);

    // 验证验证码
    String cachedCode = userService.getEmailCode(email, "register");
    if (cachedCode == null || !cachedCode.equals(code)) {
        return ResponseEntity.status(400).body(Map.of("code", 400, "message", "验证码错误或已过期"));
    }

    // 创建用户
    User user = new User();
    user.setEmail(email);
    user.setUsername(email); // 使用邮箱作为用户名
    user.setPassword(password); // 未加密的密码,在service层会加密
    user.setNickname(nickname != null ? nickname : "用户" + email.substring(0, email.indexOf("@")));

    // 调用服务层方法创建用户
    Map<String, Object> registerResult = userService.register(user, true);
    int registerCode = (int) registerResult.get("code");

    // 验证码验证成功后,删除缓存中的验证码
    if (registerCode == 200) {
        userService.removeEmailCode(email, "register");
    }

    return ResponseEntity.status(registerCode == 200 ? 200 : 400).body(registerResult);
}

邮箱+验证码登录

@PostMapping("/login/email")
public ResponseEntity<?> loginByEmail(
        @RequestParam @NotBlank @Email String email,
        @RequestParam @NotBlank String code) {
    logger.info("邮箱验证码登录,邮箱: {}", email);
    Map<String, Object> result = userService.emailLogin(email, code);

    int code2 = (int) result.get("code");
    return ResponseEntity.status(code2 == 200 ? 200 : 400).body(result);
}

3.5 服务层实现

在服务层,实现用户注册和登录的业务逻辑:

邮箱验证码登录

@Override
public Map<String, Object> emailLogin(String email, String code) {
    Map<String, Object> result = new HashMap<>();

    // 验证验证码
    String cachedCode = getEmailCode(email, "login");
    if (cachedCode == null || !cachedCode.equals(code)) {
        result.put("code", 400);
        result.put("message", "验证码错误或已过期");
        return result;
    }

    // 清除验证码缓存
    removeEmailCode(email, "login");

    // 根据邮箱查询用户
    User user = getOneOrNull(QueryWrapper.create().where("email = ?", email));

    if (user == null) {
        result.put("code", 400);
        result.put("message", "用户不存在,请先注册");
        return result;
    }

    // 检查用户状态
    if (user.getStatus() != 1) {
        result.put("code", 403);
        result.put("message", "账号已被禁用");
        return result;
    }

    // 更新登录信息
    user.setLastLoginTime(LocalDateTime.now());
    updateById(user);

    // 生成token
    String token = jwtUtil.generateToken(user.getUsername());

    // 准备返回数据
    Map<String, Object> userData = new HashMap<>();
    userData.put("id", user.getId());
    userData.put("username", user.getUsername());
    userData.put("nickname", user.getNickname());
    userData.put("avatar", user.getAvatar());
    userData.put("email", user.getEmail());

    result.put("code", 200);
    result.put("message", "登录成功");
    result.put("data", new HashMap<String, Object>() {{
        put("token", token);
        put("user", userData);
    }});

    return result;
}

3.6 邮件模板

创建verification-code.html邮件模板(放在resources/templates/email目录下):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>验证码</title>
    <style>
        body {
            font-family: 'Arial', sans-serif;
            line-height: 1.6;
            color: #333;
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
        }
        .container {
            border: 1px solid #e6e6e6;
            border-radius: 5px;
            padding: 20px;
            background-color: #f9f9f9;
        }
        .header {
            text-align: center;
            margin-bottom: 20px;
        }
        .code {
            text-align: center;
            font-size: 24px;
            font-weight: bold;
            letter-spacing: 5px;
            color: #ff6600;
            margin: 20px 0;
            padding: 10px;
            background-color: #fff;
            border: 1px dashed #ccc;
            border-radius: 5px;
        }
        .footer {
            margin-top: 30px;
            font-size: 12px;
            color: #999;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h2>宠物商店 - 验证码</h2>
        </div>
        <p>尊敬的用户:</p>
        <p>您正在进行<span th:text="${type == 'register'} ? '注册' : (${type == 'login'} ? '登录' : '重置密码')"></span>操作,您的验证码为:</p>
        <div class="code" th:text="${code}">123456</div>
        <p>验证码有效期为5分钟,请勿将验证码泄露给他人。</p>
        <p>如非本人操作,请忽略此邮件。</p>
        <div class="footer">
            <p>宠物商店团队</p>
            <p>发送时间:<span th:text="${time}">2023-01-01 12:00:00</span></p>
        </div>
    </div>
</body>
</html>

4. 前端实现

在前端,我们需要创建对应的API调用和页面:

4.1 API层

// 用户相关接口
export const userApi = {
    // 邮箱验证码注册
    register: (data) => {
        return request({
            url: '/user/register',
            method: 'post',
            data
        })
    },
    // 邮箱验证码登录
    emailLogin: (data) => {
        return request({
            url: '/user/login/email',
            method: 'post',
            data
        })
    },
    // 发送邮箱验证码
    sendEmailCode: (email, type) => {
        return request({
            url: '/user/send/email',
            method: 'post',
            params: {
                email,
                type // login-登录, register-注册, reset-重置密码
            }
        })
    }
}

4.2 注册页面

<template>
  <div class="form-container">
    <div class="form-item">
      <label class="label">邮箱</label>
      <input type="text" class="input" placeholder="请输入邮箱" v-model="form.email" />
    </div>

    <div class="form-item code-group">
      <label class="label">验证码</label>
      <div class="code-input-wrapper">
        <input type="text" class="input code-input" placeholder="请输入验证码" v-model="form.code" />
        <button class="code-btn" :disabled="countdown > 0" @click="sendCode">
          {{ countdown > 0 ? `${countdown}s后重发` : '获取验证码' }}
        </button>
      </div>
    </div>

    <div class="form-item">
      <label class="label">密码</label>
      <input type="password" class="input" placeholder="请输入密码" v-model="form.password" />
    </div>

    <div class="form-item">
      <label class="label">昵称</label>
      <input type="text" class="input" placeholder="请输入昵称" v-model="form.nickname" />
    </div>

    <button class="submit-btn" @click="register">注册</button>
  </div>
</template>

<script>
import { userApi } from '@/api'

export default {
  data() {
    return {
      form: {
        email: '',
        code: '',
        password: '',
        nickname: ''
      },
      countdown: 0
    }
  },
  methods: {
    sendCode() {
      // 验证邮箱格式
      if (!this.form.email || !/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(this.form.email)) {
        this.$message.error('请输入正确的邮箱地址')
        return
      }

      userApi.sendEmailCode(this.form.email, 'register').then(res => {
        if (res.code === 200) {
          this.$message.success('验证码发送成功')
          this.countdown = 60
          const timer = setInterval(() => {
            this.countdown--
            if (this.countdown <= 0) {
              clearInterval(timer)
            }
          }, 1000)
        } else {
          this.$message.error(res.message || '验证码发送失败')
        }
      })
    },
    register() {
      // 表单验证
      if (!this.form.email) {
        this.$message.error('请输入邮箱')
        return
      }
      if (!this.form.code) {
        this.$message.error('请输入验证码')
        return
      }
      if (!this.form.password) {
        this.$message.error('请输入密码')
        return
      }

      userApi.register(this.form).then(res => {
        if (res.code === 200) {
          this.$message.success('注册成功')
          // 跳转到登录页
          this.$router.push('/login')
        } else {
          this.$message.error(res.message || '注册失败')
        }
      }).catch(err => {
        this.$message.error('注册失败,请稍后再试')
      })
    }
  }
}
</script>

4.3 登录页面

<template>
  <div class="login-container">
    <div class="login-tabs">
      <div :class="['tab-item', loginType === 'password' ? 'active' : '']" @click="loginType = 'password'">密码登录</div>
      <div :class="['tab-item', loginType === 'code' ? 'active' : '']" @click="loginType = 'code'">验证码登录</div>
    </div>

    <!-- 密码登录 -->
    <div v-if="loginType === 'password'" class="form-content">
      <div class="form-item">
        <label class="label">邮箱</label>
        <input type="text" class="input" placeholder="请输入邮箱" v-model="passwordForm.username" />
      </div>

      <div class="form-item">
        <label class="label">密码</label>
        <input type="password" class="input" placeholder="请输入密码" v-model="passwordForm.password" />
      </div>

      <button class="submit-btn" @click="loginByPassword">登录</button>
    </div>

    <!-- 验证码登录 -->
    <div v-if="loginType === 'code'" class="form-content">
      <div class="form-item">
        <label class="label">邮箱</label>
        <input type="text" class="input" placeholder="请输入邮箱" v-model="codeForm.email" />
      </div>

      <div class="form-item code-group">
        <label class="label">验证码</label>
        <div class="code-input-wrapper">
          <input type="text" class="input code-input" placeholder="请输入验证码" v-model="codeForm.code" />
          <button class="code-btn" :disabled="countdown > 0" @click="sendCode">
            {{ countdown > 0 ? `${countdown}s后重发` : '获取验证码' }}
          </button>
        </div>
      </div>

      <button class="submit-btn" @click="loginByCode">登录</button>
    </div>
  </div>
</template>

<script>
import { userApi } from '@/api'

export default {
  data() {
    return {
      loginType: 'password',
      passwordForm: {
        username: '',
        password: ''
      },
      codeForm: {
        email: '',
        code: ''
      },
      countdown: 0
    }
  },
  methods: {
    sendCode() {
      if (!this.codeForm.email || !/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(this.codeForm.email)) {
        this.$message.error('请输入正确的邮箱地址')
        return
      }

      userApi.sendEmailCode(this.codeForm.email, 'login').then(res => {
        if (res.code === 200) {
          this.$message.success('验证码发送成功')
          this.countdown = 60
          const timer = setInterval(() => {
            this.countdown--
            if (this.countdown <= 0) {
              clearInterval(timer)
            }
          }, 1000)
        } else {
          this.$message.error(res.message || '验证码发送失败')
        }
      })
    },
    loginByPassword() {
      if (!this.passwordForm.username) {
        this.$message.error('请输入邮箱')
        return
      }
      if (!this.passwordForm.password) {
        this.$message.error('请输入密码')
        return
      }

      userApi.login(this.passwordForm).then(res => {
        if (res.code === 200) {
          // 保存token
          localStorage.setItem('token', res.data.token)
          // 保存用户信息
          localStorage.setItem('user', JSON.stringify(res.data.user))
          this.$message.success('登录成功')
          // 跳转到首页
          this.$router.push('/')
        } else {
          this.$message.error(res.message || '登录失败')
        }
      }).catch(err => {
        this.$message.error('登录失败,请稍后再试')
      })
    },
    loginByCode() {
      if (!this.codeForm.email) {
        this.$message.error('请输入邮箱')
        return
      }
      if (!this.codeForm.code) {
        this.$message.error('请输入验证码')
        return
      }

      userApi.emailLogin(this.codeForm).then(res => {
        if (res.code === 200) {
          // 保存token
          localStorage.setItem('token', res.data.token)
          // 保存用户信息
          localStorage.setItem('user', JSON.stringify(res.data.user))
          this.$message.success('登录成功')
          // 跳转到首页
          this.$router.push('/')
        } else {
          this.$message.error(res.message || '登录失败')
        }
      }).catch(err => {
        this.$message.error('登录失败,请稍后再试')
      })
    }
  }
}
</script>

5. 最佳实践

在实现邮箱验证码注册登录功能时,有以下最佳实践:

  1. 验证码安全性

    • 设置验证码过期时间(通常5-10分钟)
    • 限制验证码尝试次数,防止暴力破解
    • 使用Redis存储验证码,方便设置过期时间并在分布式环境中共享
  2. 邮件模板设计

    • 使用响应式设计,适配各种设备
    • 邮件内容简洁明了,突出验证码
    • 提供品牌识别元素(Logo、颜色等)
  3. 性能优化

    • 邮件发送应异步处理,不阻塞主线程
    • 考虑使用消息队列处理邮件发送任务
    • 邮件服务配置连接池,优化性能
  4. 安全防护

    • 防止短时间内重复发送验证码
    • 验证邮箱格式
    • 实现IP限制,防止恶意请求

6. 总结

邮箱验证码注册登录功能是现代Web应用的标准功能,通过本文的实现,我们可以为用户提供一种安全、便捷的注册和登录方式。

在实际应用中,可以根据具体需求扩展和优化这些功能,例如:

  • 添加邮箱绑定和解绑功能
  • 实现邮箱地址变更功能
  • 支持通过邮箱重置密码
  • 结合第三方登录(如QQ、微信等)

希望本文对你实现邮箱验证码注册登录功能有所帮助!

如果觉得我的文章对你有用,请随意赞赏
END
本文作者:
文章标题:Springboot 邮箱验证码注册登录实现
本文地址:https://blog.ybyq.wang/archives/646.html
版权说明:若无注明,本文皆Xuan's blog原创,转载请保留文章出处。