node全栈系列(七)-增加验证码登录
这是一个非常实用的功能,能有效防止暴力破解密码。
我们将采用 svg-captcha 库来生成验证码。这是一种不需要安装复杂图形库(如 Python 的 pillow 或 C++ 的 gd)的轻量级方案,生成的 SVG 图片在前端渲染非常清晰且速度快。
实现逻辑如下:
- 生成:前端请求验证码 -> 后端生成一个随机字符 + 一个唯一ID (UUID) -> 后端把 {UUID: 验证码} 存到内存中 -> 把 UUID 和 SVG 图片返给前端。
- 验证:前端登录时,把 账号 + 密码 + 验证码 + UUID 一起发给后端 -> 后端根据 UUID 找内存里的验证码 -> 对比是否一致。
第一步:后端安装依赖
你需要安装两个库:
- svg-captcha: 生成验证码图片。
- uuid: 生成唯一标识符(用来标记这张验证码属于谁)。
npm install svg-captcha uuid
第二步:后端代码实现 (routes/auth.js)
我们需要修改 routes/auth.js,增加获取验证码接口,并修改登录接口。
注意:为了简单起见,我们将验证码存在全局变量 Map 中(内存)。如果是生产环境集群部署,通常存在 Redis 里,但作为学习项目,用内存 Map 足够了。
routes/auth.js
import express from 'express'
import { body, validationResult } from 'express-validator'
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import svgCaptcha from 'svg-captcha' // 引入验证码库
import { v4 as uuidv4 } from 'uuid' // 引入UUID
import { pool } from '../db/mysql.js'
import config from '../config.js'
import HttpError from '../utils/HttpError.js'
const router = express.Router()
// --- 全局变量:存储验证码 ---
// Key: uuid, Value: 验证码文字
const captchaStore = new Map()
// 1. 获取图形验证码接口
// GET /auth/captcha
router.get('/captcha', (req, res, next) => {
try {
// 生成验证码
const captcha = svgCaptcha.create({
size: 4, // 4个字符
ignoreChars: '0o1i', // 排除容易混淆的字符
noise: 2, // 干扰线条数量
color: true, // 文字有颜色
background: '#fff' // 背景色
})
// 生成一个唯一标识符
const uuid = uuidv4()
// 存入内存 (转成小写方便比对)
captchaStore.set(uuid, captcha.text.toLowerCase())
// 设置过期时间:5分钟后自动删除,防止内存泄露
setTimeout(() => {
captchaStore.delete(uuid)
}, 5 * 60 * 1000)
// 返回 SVG 图片代码和 UUID
res.json({
code: 200,
message: '获取成功',
data: {
uuid: uuid,
img: captcha.data // 这是 SVG 的 XML 字符串,前端可以直接渲染
}
})
} catch (err) {
next(err)
}
})
// 2. 登录接口 (增加验证码校验)
router.post(
'/login',
[
body('username').notEmpty().withMessage('账号不能为空'),
body('password').notEmpty().withMessage('密码不能为空'),
body('code').notEmpty().withMessage('验证码不能为空'), // 新增校验
body('uuid').notEmpty().withMessage('验证码已失效,请刷新重试') // 新增校验
],
async (req, res, next) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) throw new HttpError(400, errors.array()[0].msg)
const { username, password, code, uuid } = req.body
// --- 核心修改:校验验证码 ---
const correctCode = captchaStore.get(uuid) // 从内存拿正确的码
if (!correctCode) {
throw new HttpError(400, '验证码已过期,请点击图片刷新')
}
if (correctCode !== code.toLowerCase()) {
throw new HttpError(400, '验证码错误')
}
// 校验通过后,立马删除该验证码(防止重复使用)
captchaStore.delete(uuid)
// ------------------------
// 下面是原有的登录逻辑
const [users] = await pool.query('SELECT * FROM sys_users WHERE username = ?', [username])
if (users.length === 0) throw new HttpError(400, '账号或密码错误')
const user = users[0]
if (user.status === 0) throw new HttpError(403, '账号已被停用')
const isMatch = await bcrypt.compare(password, user.password)
if (!isMatch) throw new HttpError(400, '账号或密码错误')
const payload = { userId: user.id }
const token = jwt.sign(payload, config.JWT_SECRET, { expiresIn: config.JWT_EXPIRES_IN })
res.json({
code: 200,
message: '登录成功',
data: { token }
})
} catch (err) { next(err) }
}
)
// ... info 接口保持不变 ...
export default router
第三步:前端 API 封装 (api/auth.ts)
import request from '@/utils/request'
// 登录 (现在需要多传 code 和 uuid)
export function login(data: any) {
return request({
url: '/auth/login',
method: 'post',
data
})
}
// 获取验证码
export function getCaptcha() {
return request({
url: '/auth/captcha',
method: 'get'
})
}
// 获取用户信息 (不变)
export function getUserInfo() {
return request({
url: '/auth/info',
method: 'get'
})
}
第四步:前端页面修改 (views/login/login.vue)
我们需要修改 Store 和 页面 UI。
1. 修改 Store (store/user.ts)
不需要修改 Store 的核心逻辑,因为 login action 只是透传参数。只要调用的时候传进去 {username, password, code, uuid} 即可。
2. 修改页面 UI (views/login/login.vue)
在密码框下面增加验证码输入框和图片。
<template>
<div class="login-container">
<div class="login-box">
<h2 class="title">后台管理系统</h2>
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" size="large">
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入账号"
prefix-icon="User"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
placeholder="请输入密码"
prefix-icon="Lock"
type="password"
show-password
/>
</el-form-item>
<!-- 新增:验证码区域 -->
<el-form-item prop="code">
<div class="flex w-full gap-2">
<el-input
v-model="loginForm.code"
placeholder="验证码"
prefix-icon="Key"
class="flex-1"
@keyup.enter="handleLogin"
/>
<!-- 验证码图片容器 -->
<div
class="captcha-box cursor-pointer"
v-html="captchaSvg"
@click="refreshCaptcha"
title="点击刷新"
></div>
</div>
</el-form-item>
<el-button
type="primary"
class="w-full mt-4"
:loading="loading"
@click="handleLogin"
>
登 陆
</el-button>
</el-form>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/store/modules/user'
import { ElMessage } from 'element-plus'
import { getCaptcha } from '@/api/auth' // 引入API
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const loginFormRef = ref()
const loading = ref(false)
const captchaSvg = ref('') // 存储 SVG 图片代码
const loginForm = reactive({
username: '',
password: '',
code: '', // 验证码输入值
uuid: '' // 验证码唯一ID
})
const loginRules = {
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
}
// 获取/刷新验证码
const refreshCaptcha = async () => {
try {
const res: any = await getCaptcha()
// res.data 包含 { uuid, img }
loginForm.uuid = res.data.uuid
captchaSvg.value = res.data.img
loginForm.code = '' // 刷新后清空输入框
} catch (error) {
console.error(error)
}
}
const handleLogin = () => {
loginFormRef.value.validate(async (valid: boolean) => {
if (valid) {
loading.value = true
try {
await userStore.login(loginForm)
ElMessage.success('登录成功')
const redirect = route.query.redirect as string
router.push(redirect || '/')
} catch (error) {
// 登录失败(如验证码错误),自动刷新验证码
refreshCaptcha()
} finally {
loading.value = false
}
}
})
}
// 初始化时获取验证码
onMounted(() => {
refreshCaptcha()
})
</script>
<style scoped>
.login-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-box {
width: 400px;
padding: 40px;
background: white;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.title {
text-align: center;
margin-bottom: 30px;
font-size: 24px;
font-weight: bold;
color: #333;
}
/* 验证码图片样式 */
.captcha-box {
width: 120px;
height: 40px;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
}
/* 深度选择器:控制 SVG 尺寸适应容器 */
:deep(.captcha-box svg) {
width: 100%;
height: 100%;
}
</style>
总结变化点
-
后端:
- 引入 svg-captcha 库。
- 新增 /captcha 接口,生成 SVG 并在内存(captchaStore Map)中记录 uuid -> code 的映射。
- 修改 /login 接口,在验证账号前,先拿 uuid 去内存查,对比前端传来的 code。
-
前端:
- UI 上增加了输入框和 v-html 来显示 SVG。
- 进入页面或点击图片时,调用 /captcha 接口,保存 uuid,渲染 img。
- 登录时把 code 和 uuid 一起发给后端。
这样你就拥有了一个安全、轻量且体验流畅的图形验证码功能了!