☰
基于SpringBoot+Vue的注册邮件激活功能实战解析
2026/10/9 1:48:15 网站建设 项目流程

简介:本资源是一个基于Vue.js与SpringBoot全栈整合的用户邮箱激活系统实战项目,面向Web开发初学者及全栈入门者,解决现代Web应用中注册环节的身份核验与安全激活问题。压缩包共63个文件,涵盖17个JavaScript前端逻辑文件、10个Java后端核心类(含Controller、Service、Entity及邮件配置)、5个Vue单文件组件、2个YML配置文件(含SMTP邮件服务参数)、1个SQL建表脚本及配套HTML/CSS/图片等资源,整体体积仅675KB,结构精简、开箱即用。已有493人学习下载,适合快速理解前后端分离架构下邮箱验证的完整链路。读者可直接运行获得注册表单提交、唯一激活码生成、Thymeleaf动态邮件模板渲染、带时效性的激活链接校验及前后端错误反馈等完整功能模块,代码注释清晰,目录层次分明(含login-demo前端工程与demo后端模块),便于对照学习与二次开发。 “注册+邮件激活”这套流程,基本上是所有带账号体系的Web项目都绕不开的标配功能。这几天刚好有个朋友问我要一套能直接跑的Vue+SpringBoot注册发送邮件激活项目,我就把之前写过的代码重新整理翻了出来。这个项目不大,但麻雀虽小五脏俱全:前端Vue负责注册表单和互动提示,后端SpringBoot负责用户落库、生成激活码、调用SMTP发邮件,用户点击邮件里的链接完成账号激活,整套流程闭环跑通。对于正在做课程设计、毕业设计,或者刚接触前后端分离想练手完整业务闭环的同学来说,这个项目非常适合拿来当模板,看懂之后自己动手改改就能用。

下面我会把这套实现从设计到落地的完整思路拆开讲,包括后端怎么写、前端怎么配合、激活链接怎么设计、邮件发送踩过哪些坑,以及联调测试时最容易翻车的几个点。内容偏实战,代码也是可以直接抄作业的程度。

1. 这个项目到底做了什么,以及为什么值得自己写一遍

先花几百字把项目全貌说清楚,避免你下载完代码之后一头雾水不知道该从哪看起。

1.1 一句话说清楚系统流程

整个系统的核心流程其实很简单,可以拆成五步:

  1. 用户在前端Vue注册页填写用户名、邮箱、密码,点击注册按钮。
  2. 前端把表单数据通过POST请求提交给SpringBoot后端接口。
  3. 后端校验参数后,将用户信息写入数据库,此时账号状态是“未激活”。
  4. 后端生成一个唯一的激活码(通常是一串UUID),拼接成激活链接,通过JavaMailSender发送到用户邮箱。
  5. 用户点击邮件里的激活链接,携带激活码访问激活接口,后端校验激活码正确且未过期,把账号状态改为“已激活”,整个注册流程结束。

这套流程看着简单,但每一步都有值得琢磨的细节。比如激活码为什么要用UUID而不是自增ID,激活链接为什么要带过期时间而不是永久有效,这些都是面试官爱问、实际项目也容易踩坑的地方。

1.2 为什么注册环节非要加邮件激活

很多新手会觉得邮件激活多此一举,用户注册完直接登录不香吗?但只要你做过面向真实用户的系统,就会明白这一步有多重要。

第一,验证邮箱真实性。垃圾注册、批量刷号是每个平台都头疼的问题。如果注册不需要验证邮箱,脚本可以伪造几千个假账号产生大量脏数据。邮件激活等于让用户证明“这个邮箱确实是我的”,极大地提高了批量注册的成本。

第二,为后续找回密码等功能铺路。账号密码忘了要通过邮箱找回,如果邮箱本身就是假的,整个流程就失效了。注册时先完成邮箱验证,后面的密码找回、安全通知、营销邮件才能建立在可靠的基础上。

第三,避免无效账号占用资源。未激活的账号可以定期清理,减少数据库垃圾数据。很多系统也会限制未激活账号的登录权限,引导用户先去完成激活。

所以不要觉得邮件激活是多余的,这是生产环境的刚需,也是判断一个开发人员有没有真正做过上线项目的一个小标尺。

1.3 技术栈选型的实际理由

这个项目选择Vue+SpringBoot,本身就是当下Java后端开发圈子最主流的组合,没有之一。SpringBoot负责提供RESTful API和处理邮件逻辑,Vue负责页面展示和交互体验,两者通过JSON数据交互。

选这套组合有三个现实理由:

  • 找工作、做毕设认可度高。目前市面上Java后端岗位的技术栈,绝大多数是SpringBoot + Vue这套组合,你把这套流程吃透,面试时聊到注册登录、邮件发送、前后端联调都有真实案例可以讲。
  • SpringBoot对邮件的支持非常省心。spring-boot-starter-mail封装了JavaMail的全部底层细节,你只需要配置文件里写上SMTP服务器地址、账号、授权码,代码里注入一个JavaMailSender就能发邮件,不需要自己写Socket通信。
  • Vue做表单交互开发效率高。注册页的校验、按钮loading状态、成功失败提示,用Vue的双向绑定和指令系统写起来非常顺手,比传统JSP+JQuery时代体验好太多。

2. 后端SpringBoot核心逻辑拆解

后端是整个项目的核心,我按照“用户表设计 → 邮件服务 → 注册接口 → 激活接口”的顺序来讲,每个环节都给出关键代码和设计理由。

2.1 用户表和实体类怎么设计

先看数据库表结构。我没有用复杂的权限表,只围绕注册激活这个核心需求设计了一张user表:

CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)', `email` varchar(100) NOT NULL COMMENT '邮箱', `status` tinyint NOT NULL DEFAULT '0' COMMENT '账号状态 0-未激活 1-已激活', `activate_code` varchar(64) DEFAULT NULL COMMENT '激活码', `activate_code_expire_time` datetime DEFAULT NULL COMMENT '激活码过期时间', `create_time` datetime DEFAULT NULL COMMENT '注册时间', `activate_time` datetime DEFAULT NULL COMMENT '激活时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), UNIQUE KEY `uk_email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个字段的设计意图我说明一下:

  • status字段是整个激活逻辑的状态机。0表示未激活,1表示已激活。激活成功就是一次简单的UPDATE。
  • activate_code字段存激活码,这个值必须是全局唯一且不可预测的,我用UUID.randomUUID().toString().replace("-", "")来生成,长度32位,碰撞概率可以忽略不计。
  • activate_code_expire_time字段是激活码过期时间。如果不设置过期时间,用户几个月后拿着老邮件来点激活,会把已经重新注册的同名邮箱覆盖,造成安全问题。
  • email字段加唯一索引,防止同一个邮箱被重复注册。

对应的User实体类可以用Lombok简化:

@Data @TableName("user") public class User { private Integer id; private String username; private String password; private String email; private Integer status; private String activateCode; private Date activateCodeExpireTime; private Date createTime; private Date activateTime; }

这里用的@TableName是MyBatis-Plus的注解。如果你习惯用MyBatis原生或JPA,逻辑是一样的,字段对上就行。我建议新手直接上MyBatis-Plus,避免写大量XML映射,把注意力集中在业务流程上。

2.2 邮件服务配置与授权码获取

SpringBoot发邮件只需要在application.yml里配置SMTP信息。以163邮箱为例:

spring: mail: host: smtp.163.com port: 465 username: your_account@163.com password: your_auth_code protocol: smtps default-encoding: UTF-8 properties: mail: smtp: ssl: enable: true auth: true starttls: enable: true

这里最容易踩的坑是password不是邮箱登录密码,而是邮箱的SMTP授权码。163、QQ邮箱都需要在网页端设置里开启SMTP服务,然后生成一个授权码。QQ邮箱是登录qq邮箱 → 设置 → 账号 → 开启POP3/SMTP服务,会得到一个16位授权码。网易邮箱同理。

我特别说明一下为什么推荐用465端口+SMTPS协议。很多教程用587端口走STARTTLS,但在某些服务器环境下,公司网络会拦截25端口或587端口的连接,导致超时。465端口是SSL直接加密的,兼容性更好。所以在配置里我显式写了protocol: smtps和mail.smtp.ssl.enable=true。

邮件发送服务类的核心代码:

@Service public class MailService { @Resource private JavaMailSender mailSender; @Value("${spring.mail.username}") private String from; public void sendActivateMail(String to, String activateUrl) { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom(from); message.setTo(to); message.setSubject("【XX系统】账号激活通知"); message.setText("亲爱的用户:\n\n" + "您正在注册XX系统账号,请点击以下链接完成激活:\n" + activateUrl + "\n\n" + "如果链接无法点击,请复制到浏览器地址栏访问。\n" + "链接有效期为24小时,请及时激活。\n\n" + "如非本人操作,请忽略此邮件。"); mailSender.send(message); } }

这是最简版本的SimpleMailMessage,纯文本邮件。生产环境建议用MimeMessageHelper配合HTML模板,可以把激活链接做成按钮样式,体验更好。比如用Thymeleaf模板引擎渲染邮件内容,这部分代码我放在后面扩展章节讲。

2.3 注册接口:写库、生成激活码、发邮件

注册接口是整个后端的重头戏,逻辑顺序一定要理清楚:先校验参数、再查重、然后生成激活码、保存用户、最后发送邮件。

@PostMapping("/register") public Result register(@RequestBody RegisterDTO dto) { // 1. 参数校验 if (StringUtils.isBlank(dto.getUsername()) || StringUtils.isBlank(dto.getEmail()) || StringUtils.isBlank(dto.getPassword())) { return Result.error("用户名、邮箱、密码不能为空"); } // 2. 用户名和邮箱查重 if (userService.getByUsername(dto.getUsername()) != null) { return Result.error("用户名已被注册"); } if (userService.getByEmail(dto.getEmail()) != null) { return Result.error("该邮箱已被注册"); } // 3. 生成激活码和过期时间 String activateCode = UUID.randomUUID().toString().replace("-", ""); Date expireTime = new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000); // 4. 构建用户对象,密码加密存储 User user = new User(); user.setUsername(dto.getUsername()); user.setPassword(new BCryptPasswordEncoder().encode(dto.getPassword())); user.setEmail(dto.getEmail()); user.setStatus(0); // 默认未激活 user.setActivateCode(activateCode); user.setActivateCodeExpireTime(expireTime); user.setCreateTime(new Date()); userService.save(user); // 5. 拼接激活链接并发送邮件 String activateUrl = "http://localhost:8080/api/activate?code=" + activateCode; mailService.sendActivateMail(dto.getEmail(), activateUrl); return Result.success("注册成功,请查收激活邮件"); }

这里的细节我要重点展开讲:

密码加密为什么用BCrypt。绝对不能明文存密码,这是底线。BCrypt是不可逆哈希,而且每次加密结果都不一样,内置盐值处理,即使两个用户密码相同,密文也不同,能在一定程度上抵抗彩虹表攻击。Spring Security里自带BCryptPasswordEncoder,单独引入spring-security-crypto这个依赖也可以独立使用。

先保存用户再发邮件,还是先发邮件再保存?我的做法是先保存用户,再调用mailService发邮件。如果邮件发送失败,通过try-catch捕获后可以返回“注册失败”或“注册成功但邮件发送失败”的提示。但要注意,如果先保存用户再发邮件时抛异常,会导致用户表里多了一条未激活的脏数据。实际项目里更稳妥的方案是:邮件发送失败时,删除刚保存的用户,或者把发送失败的情况记录到日志表里稍后重试。教学项目我保留了最简逻辑,但你心里要清楚生产环境不能这么简单。

激活链接拼接的域名写死还是动态获取?我这里为了方便返回的是localhost,但真实项目里这个地址应该是可配置的,比如写到application.yml里:

app: activate-base-url: http://your-domain.com:8080

然后在代码里通过@Value注入,拼接成完整的链接。否则部署到服务器上之后,链接还是指向localhost,用户点击之后访问的是他自己的电脑,必然失败。

2.4 激活接口:校验、更新、统一响应

激活接口是最简单但最容易写错的地方。我见过很多人只校验激活码是否存在,不校验过期时间、不校验账号当前状态,结果出现各种逻辑漏洞。

@GetMapping("/activate") public Result activate(@RequestParam("code") String code) { // 1. 根据激活码查询用户 User user = userService.getByActivateCode(code); if (user == null) { return Result.error("激活链接无效"); } // 2. 校验是否已激活 if (user.getStatus() == 1) { return Result.success("账号已激活,请直接登录"); } // 3. 校验是否过期 if (user.getActivateCodeExpireTime() == null || user.getActivateCodeExpireTime().before(new Date())) { return Result.error("激活链接已过期,请重新发送激活邮件"); } // 4. 更新状态 user.setStatus(1); user.setActivateTime(new Date()); user.setActivateCode(null); // 激活后清空激活码,防止重复激活 user.setActivateCodeExpireTime(null); userService.updateById(user); return Result.success("激活成功,请前往登录"); }

激活后清空激活码和过期时间这个细节很重要。如果不清空,同一个链接可以反复被访问,而且每次都会执行一次UPDATE,虽然影响不大,但逻辑上不够严谨。清空之后,用户再点同一条链接,第一步查询就查不到,返回“激活链接无效”,语义也合理。

另外,这个接口我用的是GET请求,因为用户点邮件链接就是一次浏览器GET请求,你不可能让用户在地址栏里构造一个POST请求。这一点是邮件激活场景的特殊性,和平时写接口要求POST的规范不一样。

还有一点容易被忽略:如果激活链接指向后端接口,激活成功后应该返回一个HTML页面或重定向到前端页面,而不是返回JSON。因为用户是在浏览器地址栏直接访问的,返回JSON会让用户看到一串不友好的文本。后面讲前端配合的时候我会给出两种处理方式的取舍。

3. 前端Vue页面与交互设计

后端接口写完了,接下来看前端Vue这边怎么配合。前端的主要工作是注册页面、请求封装、激活结果提示,以及和激活链接的衔接方式。

3.1 注册页面的表单校验

我用Vue3 + Vite + Element Plus来搭建的注册页面,这个组合目前用得最多。表单部分直接上代码:

<template> <div class="register-container"> <el-card class="register-card"> <h2>用户注册</h2> <el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-width="80px" > <el-form-item label="用户名" prop="username"> <el-input v-model="registerForm.username" placeholder="请输入用户名" /> </el-form-item> <el-form-item label="邮箱" prop="email"> <el-input v-model="registerForm.email" placeholder="请输入邮箱" /> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="registerForm.password" type="password" show-password placeholder="请输入密码" /> </el-form-item> <el-form-item label="确认密码" prop="confirmPassword"> <el-input v-model="registerForm.confirmPassword" type="password" show-password placeholder="请再次输入密码" /> </el-form-item> <el-form-item> <el-button type="primary" :loading="submitting" @click="handleRegister" > 注册 </el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup> import { reactive, ref } from 'vue' import { ElMessage } from 'element-plus' import request from '@/utils/request' const registerFormRef = ref() const submitting = ref(false) const registerForm = reactive({ username: '', email: '', password: '', confirmPassword: '' }) // 自定义确认密码校验 const validateConfirmPassword = (rule, value, callback) => { if (value !== registerForm.password) { callback(new Error('两次输入的密码不一致')) } else { callback() } } const registerRules = { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 3, max: 20, message: '用户名长度3-20个字符', trigger: 'blur' } ], email: [ { required: true, message: '请输入邮箱', trigger: 'blur' }, { type: 'email', message: '邮箱格式不正确', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 6, message: '密码长度不能少于6位', trigger: 'blur' } ], confirmPassword: [ { required: true, message: '请再次输入密码', trigger: 'blur' }, { validator: validateConfirmPassword, trigger: 'blur' } ] } const handleRegister = () => { registerFormRef.value.validate(async (valid) => { if (!valid) return submitting.value = true try { const res = await request.post('/api/register', { username: registerForm.username, email: registerForm.email, password: registerForm.password }) if (res.code === 200) { ElMessage.success('注册成功,激活邮件已发送,请查收') } else { ElMessage.error(res.message) } } catch (err) { ElMessage.error('网络异常,请稍后重试') } finally { submitting.value = false } }) } </script>

表单校验这块我建议做到前端先拦一遍,后端再拦一遍,不要怕麻烦。前端校验是为了用户体验,用户输入不符合规则时立即提示,不用等提交到后端;后端校验是为了安全,防止有人绕过前端直接调接口。

按钮的loading属性也很关键。用户点击注册后,后端处理需要一些时间(尤其是发邮件),如果不加loading,用户会以为没点上,疯狂点击,导致提交多条重复数据。设置了:loading="submitting"之后,按钮会转圈且不可重复点击,这是最基础的防重复提交手段。

3.2 激活链接的跳转设计(前后端分离怎么处理)

这是前后端分离架构下最需要思考的一个点。激活链接点击后,到底该指向谁?

方案一:链接指向后端激活接口,后端激活成功后重定向到前端结果页。

比如激活链接是http://localhost:8080/api/activate?code=xxx,后端处理完后,返回302重定向到http://localhost:5173/activate-result?status=success。这个方案的好处是用户点击链接后,后端在服务端直接完成激活逻辑,不用前端做任何事;缺点是重定向地址需要动态拼接前端的地址,如果前端部署域名变了,链接就失效了。

方案二:链接指向前端激活页面(携带code参数),前端页面调后端接口激活。

激活链接是http://localhost:5173/activate?code=xxx,用户在浏览器打开的是前端Vue页面,这个页面在onMounted里拿到URL上的code,然后调用/api/activate?code=xxx,根据后端返回结果展示不同的提示页。这个方案更符合前后端分离的直觉,前端可以做出更美观的激活结果页,但需要前端多写一个页面。

我在这个项目里用的是方案一,但代码里也预留了方案二的实现接口。如果只是一次性激活,方案一代码量最少;如果希望激活成功后引导用户去设置密码、完善资料,方案二体验更好。你自己取舍。

顺带补充一下,Vue Router里配置一个激活结果页,通过路由钩子或query参数来展示结果,代码大致是这样:

<template> <div class="activate-result"> <el-result :icon="resultStatus" :title="resultTitle" :sub-title="resultSubTitle" > <template #extra> <el-button type="primary" @click="goLogin">去登录</el-button> </template> </el-result> </div> </template> <script setup> import { computed } from 'vue' import { useRoute, useRouter } from 'vue-router' const route = useRoute() const router = useRouter() const status = route.query.status const resultStatus = computed(() => (status === 'success' ? 'success' : 'error')) const resultTitle = computed(() => (status === 'success' ? '激活成功' : '激活失败')) const resultSubTitle = computed(() => status === 'success' ? '您的账号已完成激活,可以登录使用了' : '激活链接无效或已过期,请重新注册或联系管理员' ) const goLogin = () => { router.push('/login') } </script>

3.3 前端请求封装与跨域处理

前端调用后端接口时,我用axios统一封装了一个request实例,这样不用每个页面都重复写baseURL和拦截器:

import axios from 'axios' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080', timeout: 10000 }) request.interceptors.response.use( (response) => { return response.data }, (error) => { return Promise.reject(error) } ) export default request

开发环境下Vite默认端口是5173,后端SpringBoot默认端口是8080,跨域是必然的。处理跨域有两种常见方案,我推荐在后端做全局CORS配置,因为前端部署到Nginx之后,用反向代理也能解决,但本地开发时后端允许跨域最直接:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

allowedOriginPatterns使用*的时候,如果你不是前后端分离的敏感场景,allowCredentials设置为true会报错。这里我一般建议如果不需要携带Cookie就不开allowCredentials,或者明确指定前端的来源地址。

4. 联调、踩坑与问题排查实录

这一章是我最想说的。代码本身不难,真正让新手心态爆炸的都是那些不起眼的小问题。我把实际开发中高频出现的问题整理成清单,按症状、原因、解决方案三个维度来讲。

4.1 邮件发送失败的常见原因

邮件发不出去,报错信息五花八门,但根因基本都是以下几类:

第一类:javax.mail.AuthenticationFailedException,认证失败。

原因几乎都是授权码填错了,或者账号密码格式不对。SpringBoot的spring.mail.username和spring.mail.password必须和SMTP服务商要求一致。QQ邮箱和163邮箱都要求使用授权码,而不是网页登录密码。如果你用公司内部邮箱,可能还需要配置额外的SSL参数。遇到认证失败,先检查配置文件的空格和特殊字符,YAML文件里冒号后面必须有空格,密码里有特殊字符建议加引号。

第二类:java.net.ConnectException,连接超时。

原因一般是服务器网络不允许访问外网SMTP端口。开发机直接跑没问题,上了云服务器就超时,这是最常见的场景。云服务商出于安全考虑,默认封禁25端口,你要么在安全组规则里放通465或587端口,要么改用服务商的企业邮箱SMTP(比如阿里云邮件推送、SendGrid这类服务)。这里要说的直接一点:如果你在云服务器上部署,25端口基本别想用,直接用465省事。

第三类:535 Error:authentication failed。

这是QQ邮箱很经典的报错。QQ邮箱的SMTP服务要求发件人地址必须和认证账号一致,而且需要先确保网页端已经开启了SMTP服务。另外,QQ邮箱还会拒绝发送包含明显垃圾邮件特征的邮件,比如大量链接、敏感词,所以激活邮件正文不要写得太像广告。

第四类:邮件发送成功但进垃圾箱。

这不是报错,但同样让人头大。如果你使用个人邮箱的SMTP发送激活邮件,由于缺乏SPF、DKIM等域名认证记录,邮件很容易被收件方的垃圾邮件过滤机制拦截。解决办法是正文不要出现“免费”“领奖”等敏感词,发件人昵称设置成系统名称而非纯数字,域名邮箱一定要配置SPF记录。如果是个人开发项目,最好在正文里加一句“如未收到邮件请检查垃圾箱”的提示,这是最务实的手段。

4.2 激活链接失效的几种情况

激活链接点了之后提示无效或过期,这个问题我排查过很多次,总结成下面的速查表:

症状可能原因解决方案
提示“激活链接无效”激活码被清空或修改确认用户是否已经激活过,激活后激活码会被置空
提示“激活链接无效”数据库没有该记录检查激活码是否被URL编码/解码导致不一致
提示“激活链接已过期”超过24小时有效期重新发送激活邮件,或完善重新发送接口
点击链接报404前端路由没配置对应页面检查Vue Router的activate页面路径

URL编码问题很隐蔽。如果激活码本身只包含字母和数字(UUID去横线后就是这种情况),一般不会出现编码问题。但如果你生成的激活码带特殊字符,邮件的链接拼接时没有URLEncode,点击之后后端拿到的参数可能就不对了。建议激活码统一用UUID去掉横线的纯字母数字格式,规避整个坑。

另一个隐蔽问题出现在Linux服务器上。有些开发者在Windows本地用UUID生成激活码,数据库里存的是小写,到了Linux环境某个版本的JDK生成的是大写,导致查不到记录。这个问题不常见,但只要出现一次就够你查半天。解决方案是在生成激活码之后统一转成小写,或者查询时大小写不敏感。

4.3 跨域和CORS配置的坑

前端报错“Access to XMLHttpRequest at 'http://localhost:8080/api/register' from origin 'http://localhost:5173' has been blocked by CORS policy”,说明后端跨域配置没生效。

排查步骤:

  1. 先确认请求是否真的发出去了。F12打开浏览器Network面板,如果请求状态是CORS error,说明OPTIONS预检请求失败。
  2. 确认SpringBoot的CorsConfig是否被扫描到。配置类放在启动类同包或子包下才能被@ComponentScan扫描到,放在其他包忘了加@MapperScan或@ComponentScan就会静默失效。
  3. 确认是否有拦截器拦截了OPTIONS请求。如果你的项目里配置了登录拦截器或JWT拦截器,注意放行OPTIONS请求,因为浏览器跨域会先发一个OPTIONS预检请求,被拦截后CORS配置再正确也会失败。

SpringBoot的拦截器放行OPTIONS写法:

@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } // 其他拦截逻辑... return true; }

4.4 测试中的其他细节

重复注册同一个邮箱。我在user表上加了email唯一索引,这是数据库层面的兜底。但注意,如果你的注册接口是先查重再插入,两个线程同时注册同一个邮箱时,查重都通过了,最终插入时后插入的那个会抛DuplicateKeyException。我的建议是:先用查重做业务层校验,再用唯一索引做最后兜底,同时在接口层捕获DuplicateKeyException返回友好提示。

未激活用户能不能登录?这里取决于业务需求。你可以规定只有status=1才能登录,也可以允许登录但功能受限。这个项目里我做的登录逻辑是校验status,未激活时提示“请先激活账号”。你可以在登录接口加同样的校验,把提示信息写清楚。

测试邮件发送的等待时间。用163或QQ邮箱发邮件,通常几秒内就能收到。如果超过几十秒没收到,先看后端日志有没有报错,再看垃圾箱。生产环境建议引入简单的邮件发送状态表或日志,方便排查。

5. 代码结构、部署上线与后续扩展建议

项目写完只是开始,部署上线和扩展同样值得讲,因为很多初学者代码本地能跑,一到服务器就抓瞎。

5.1 完整项目代码结构

后端代码结构简单说一下,方便你看项目时快速定位:

src/main/java/com/example/register/ ├── RegisterApplication.java ├── config/ │ └── CorsConfig.java ├── controller/ │ └── UserController.java ├── service/ │ ├── UserService.java │ ├── MailService.java │ └── impl/ │ └── UserServiceImpl.java ├── entity/ │ └── User.java ├── mapper/ │ └── UserMapper.java └── common/ └── Result.java

前端结构:

src/ ├── api/ │ └── user.js ├── utils/ │ └── request.js ├── router/ │ └── index.js ├── views/ │ ├── Register.vue │ ├── Login.vue │ └── ActivateResult.vue └── App.vue

新手可以对照这个结构,自己从头搭一遍,比直接看完整代码效果要好得多。不建议把这些文件放在一个zip里硬啃,先厘清职责边界再动手。

5.2 部署到服务器时要改哪些配置

本地开发通过之后,部署到服务器绝不是把jar包丢上去就完事。有几个配置我建议上线前统一检查:

后端打包。用Maven打jar包时,确保测试代码不参与打包,否则可能因为测试环境连不上数据库导致打包失败。执行mvn clean package -DskipTests。

前端构建。Vue项目打包成静态文件,产物在dist目录。你需要把dist目录里的文件通过Nginx托管,并配置代理转发API请求:

server { listen 80; server_name your-domain.com; location / { root /opt/register-front/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

try_files配置很重要,如果缺失,刷新前端页面路由时会404。这个坑我见过太多次了。

数据库连接。本地用的MySQL和服务器上的MySQL版本要保持一致,尤其是时区设置。连接串加上serverTimezone=Asia/Shanghai,避免日期时间差8小时的问题。

激活链接地址。激活链接里的base URL不能再用localhost,改成你的服务器公网地址或域名,否则用户点击邮件里的链接会指向他自己的机器。

5.3 可以扩展的方向

如果这个项目对你来说已经不够过瘾了,往下延伸的思路很多:

  • 邮件模板HTML化。用Thymeleaf或FreeMarker渲染邮件内容,把激活链接做成按钮样式,大幅提升用户体验和邮件打开率。
  • 重新发送激活邮件接口。激活码过期后,提供输入邮箱、重新发送激活邮件的接口。这里要注意防刷,同一个邮箱一天最多发几次。
  • 异步发送邮件。当前代码里注册接口是同步调用邮件服务,如果SMTP服务器响应慢,用户要等很久。可以用Spring Boot的@Async注解把发邮件动作异步化,注册接口快速返回,邮件后台慢慢发。
  • 引入Redis存激活码。激活码不一定要存数据库,可以存Redis并设置过期时间,配合TTL自动删除。不过教学项目里存数据库更直观,方便你观察数据变化。
  • 对接更多邮箱服务商。支持QQ、163、Gmail,甚至阿里云邮件推送,在配置层面做多租户邮件通道。

这些扩展方向你随便选一个做深,都能写在简历上作为项目亮点。

写在最后的实操心得

最后分享一点自己的真实感受。这类注册激活项目,代码量不大,但它把后端开发最常见的几个环节都串起来了:数据库设计、接口编写、第三方服务集成(邮件)、前后端联调、部署上线。做一遍这个流程,你对SpringBoot开发的理解会从“会写接口”上升到“能把一个功能完整落地”。

我在实际做这个项目的过程中,感受最深的一点是:很多问题不是代码写错,而是环境配置和细节逻辑没想清楚。比如授权码不是密码、激活链接要处理过期、跨域要处理OPTIONS预检,这些内容不会写在你背的八股文里,但实际开发遇到的概率极高。

如果你照着这个项目做了一遍还是遇到问题,不要急着怀疑代码,先看日志。后端的控制台日志、前端的Network面板,这两样东西能解决90%的联调问题。邮件收不到就去看SMTP的异常堆栈,接口调不通就去看是不是跨域、是不是路径写错、是不是参数名不一致,按这个思路排查,基本不存在解决不了的问题。

这套代码目前只是最基础的版本,但结构干净、流程完整,拿来理解“注册+邮件激活”这条链路足够了。后面你有需要,可以在这个基础上继续加功能,比如Spring Security做认证授权、JWT做登录态管理等。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询