普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月4日首页

node全栈系列(七)-增加验证码登录

作者 小胖霞
2025年12月4日 14:26

这是一个非常实用的功能,能有效防止暴力破解密码。

我们将采用 svg-captcha 库来生成验证码。这是一种不需要安装复杂图形库(如 Python 的 pillow 或 C++ 的 gd)的轻量级方案,生成的 SVG 图片在前端渲染非常清晰且速度快。

实现逻辑如下:

  1. 生成:前端请求验证码 -> 后端生成一个随机字符 + 一个唯一ID (UUID) -> 后端把 {UUID: 验证码} 存到内存中 -> 把 UUID 和 SVG 图片返给前端。
  2. 验证:前端登录时,把 账号 + 密码 + 验证码 + 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>

总结变化点

  1. 后端

    • 引入 svg-captcha 库。
    • 新增 /captcha 接口,生成 SVG 并在内存(captchaStore Map)中记录 uuid -> code 的映射。
    • 修改 /login 接口,在验证账号前,先拿 uuid 去内存查,对比前端传来的 code。
  2. 前端

    • UI 上增加了输入框和 v-html 来显示 SVG。
    • 进入页面或点击图片时,调用 /captcha 接口,保存 uuid,渲染 img。
    • 登录时把 code 和 uuid 一起发给后端。

这样你就拥有了一个安全、轻量且体验流畅的图形验证码功能了!

❌
❌