登录验证码发送间隔限制的设计与实现
引言
在小项目设计登录模块时,一个常见的思路是账号与邮箱/手机号绑定,通过邮件/短信验证码的方式登录/注册。相比于传统的口令式注册/登录有以下优点:
- 用户不需要记住密码:只要邮箱/手机号还能用,就可以登录。
- 系统不需要考虑太多安全问题:数据库不存储敏感信息,安全性由邮箱/电信运营商保障。
- 登录、注册合一:简化开发。
发送邮件/短信验证码一般都要通过云厂商的服务来实现,如果有人恶意刷接口就会产生大量费用。而且登录验证码本身也是一段时间内有效的,不需要频繁调用。所以我们需要对发送验证码的接口做调用时间间隔的限制。
本文用 vue3 + element plus + springboot 的技术栈举例。重点在分享思路,在使用其他技术栈的情况下也可以套用。
后端
目的:对于同一个邮箱来说,一分钟内最多发送一次验证码。
思路:使用 Caffeine 库的 Cache 类,过期策略是写后 1 分钟过期。key 存放邮箱,value 存一个 new Object()(我们可以称之为 timer)。发送验证码之前先检查 cache 里邮箱对应的 timer 是否还在,如果还在说明 1 分钟内发过验证码,拒绝重复发送;如果不在就可以发。
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
private final Cache<String, String> loginCodeCache = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build();
private final Cache<String, Object> sendLoginCodeTimers = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build();
public Result sendLoginCode(String email) { AtomicBoolean isDuplicated = new AtomicBoolean(true); sendLoginCodeTimers.asMap().computeIfAbsent(email, targetEmail -> { String code = RandomStringUtils.randomNumeric(6); String content = getLoginEmailContent(code); MailUtil.send(targetEmail, "登录验证码", content, true); log.info("向{}发送了登录验证码: {}", targetEmail, code); loginCodeCache.put(targetEmail, code); isDuplicated.set(false); return new Object(); }); return isDuplicated.get() ? Result.error("发送过于频繁") : Result.success(); }
|
利用了 Cache 的线程安全的特性,不必手动写同步锁。
这样,后端层面就限制了发送间隔,不过前端用户难以感知到 1 分钟的间隔有多久,所以我们最好在前端实现一个 1 分钟的倒计时。并且在期间禁用发送按钮,避免用户连续点击发起重复的请求。
前端
为了突出重点,省略了无关的组件、样式和代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <template> <el-form :model="loginForm"> <el-form-item prop="email"> <el-input v-model="loginForm.email" placeholder="邮箱"> <template #append> <el-button @click="sendLoginCode" v-if="allowSendEmail"> 发送验证码 </el-button> <el-button v-else disabled> {{ remainSeconds + '秒后重试' }} </el-button> </template> </el-input> </el-form-item> <el-form-item prop="code"> <el-input v-model="loginForm.code" placeholder="验证码"></el-input> </el-form-item> </template>
<script setup lang="ts"> import { reactive, ref } from 'vue'; import type { FormInstance } from "element-plus"; import { ElMessage } from "element-plus"; import axiosInstance from "@/axios";
const loginForm = reactive({ email: '', code: '', });
// 60s 内不允许重复点击 const INTERVAL = 60 // 按钮是否允许点击 const allowSendEmail = ref(true) // 倒计时剩余秒数 const remainSeconds = ref(0)
function sendLoginCode() { // 一开始就要禁用按钮,避免等待服务器响应期间重复点击 allowSendEmail.value = false remainSeconds.value = INTERVAL axiosInstance.post('/user/login/code?email=' + loginForm.email) .then(resp => { if (resp.data.code == 2000) { ElMessage.success('发送成功,请注意查收') } }) .finally(() => { // 无论成功与否, 都开启 60s 冷却倒计时 let timer = setInterval(() => { if (remainSeconds.value > 0) { remainSeconds.value-- } else { clearInterval(timer) // 倒计时结束,解除按钮禁用 allowSendEmail.value = true } }, 1000); }) } </script>
<style scoped></style>
|
效果
点击前:

点击后:

总结
本文介绍了登录验证码发送间隔限制在后端和前端的设计与实现。希望可以为有类似需求的朋友节省时间。