宁德市浔绾网

滑块验证完整实现部署流程(前端 + 后端 + Nginx 集成)

2026-04-02 07:32:02 浏览次数:1
详细信息

一、系统架构设计

用户浏览器 → Nginx(反向代理/静态资源) → 后端API → Redis/MySQL

二、前端实现

1. 项目结构

frontend/
├── src/
│   ├── components/
│   │   └── SliderCaptcha.vue  # 滑块组件
│   ├── assets/
│   │   └── images/            # 验证码图片资源
│   └── utils/
│       └── request.js          # API请求封装
├── public/
└── package.json

2. 滑块组件 (SliderCaptcha.vue)

<template>
  <div class="slider-captcha">
    <!-- 背景图 -->
    <div class="captcha-container">
      <div class="background-image" :style="bgStyle"></div>
      <!-- 缺口图 -->
      <div class="slider-image" :style="sliderStyle"></div>
    </div>

    <!-- 滑块轨道 -->
    <div class="slider-track" @mousedown="startDrag" ref="track">
      <div class="slider-thumb" :style="{ left: sliderPosition + 'px' }">
        <div class="thumb-icon">→</div>
      </div>
      <div class="track-text">{{ isVerified ? '验证成功' : '向右滑动完成验证' }}</div>
    </div>

    <!-- 刷新按钮 -->
    <button @click="refresh" class="refresh-btn">刷新验证码</button>
  </div>
</template>

<script>
export default {
  name: 'SliderCaptcha',
  props: {
    onVerify: {
      type: Function,
      default: () => {}
    }
  },
  data() {
    return {
      captchaId: '',
      bgImage: '',
      sliderImage: '',
      sliderWidth: 0,
      sliderHeight: 0,
      bgWidth: 0,
      bgHeight: 0,
      sliderPosition: 0,
      isDragging: false,
      isVerified: false,
      startX: 0,
      startLeft: 0,
      targetX: 0
    };
  },
  computed: {
    bgStyle() {
      return {
        backgroundImage: `url(${this.bgImage})`,
        width: `${this.bgWidth}px`,
        height: `${this.bgHeight}px`
      };
    },
    sliderStyle() {
      return {
        backgroundImage: `url(${this.sliderImage})`,
        left: `${this.targetX}px`,
        width: `${this.sliderWidth}px`,
        height: `${this.sliderHeight}px`
      };
    }
  },
  mounted() {
    this.initCaptcha();
    document.addEventListener('mousemove', this.onDrag);
    document.addEventListener('mouseup', this.stopDrag);
  },
  beforeDestroy() {
    document.removeEventListener('mousemove', this.onDrag);
    document.removeEventListener('mouseup', this.stopDrag);
  },
  methods: {
    async initCaptcha() {
      try {
        const response = await this.$http.get('/api/captcha/generate');
        const { captchaId, bgImage, sliderImage, targetX, bgWidth, bgHeight, sliderWidth, sliderHeight } = response.data;

        this.captchaId = captchaId;
        this.bgImage = bgImage;
        this.sliderImage = sliderImage;
        this.targetX = targetX;
        this.bgWidth = bgWidth;
        this.bgHeight = bgHeight;
        this.sliderWidth = sliderWidth;
        this.sliderHeight = sliderHeight;
        this.isVerified = false;
        this.sliderPosition = 0;
      } catch (error) {
        console.error('初始化验证码失败:', error);
      }
    },

    startDrag(event) {
      if (this.isVerified) return;

      this.isDragging = true;
      this.startX = event.clientX;
      this.startLeft = this.sliderPosition;
    },

    onDrag(event) {
      if (!this.isDragging) return;

      const track = this.$refs.track;
      const maxPosition = track.offsetWidth - 40; // 滑块宽度
      const deltaX = event.clientX - this.startX;
      let newPosition = this.startLeft + deltaX;

      // 限制滑块范围
      newPosition = Math.max(0, Math.min(newPosition, maxPosition));
      this.sliderPosition = newPosition;
    },

    async stopDrag() {
      if (!this.isDragging) return;
      this.isDragging = false;

      // 验证位置是否匹配(允许5px误差)
      const trackWidth = this.$refs.track.offsetWidth;
      const targetPercentage = (this.targetX / this.bgWidth) * 100;
      const currentPercentage = (this.sliderPosition / trackWidth) * 100;

      if (Math.abs(currentPercentage - targetPercentage) < 3) {
        await this.verifyCaptcha();
      } else {
        // 验证失败,滑块回弹
        this.sliderPosition = 0;
      }
    },

    async verifyCaptcha() {
      try {
        const response = await this.$http.post('/api/captcha/verify', {
          captchaId: this.captchaId,
          sliderPosition: this.sliderPosition
        });

        if (response.data.success) {
          this.isVerified = true;
          this.$emit('verified', response.data.token);
        }
      } catch (error) {
        console.error('验证失败:', error);
        this.sliderPosition = 0;
      }
    },

    refresh() {
      this.initCaptcha();
    }
  }
};
</script>

<style scoped>
.slider-captcha {
  width: 350px;
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}

.captcha-container {
  position: relative;
  margin-bottom: 20px;
  overflow: hidden;
  border-radius: 4px;
  border: 1px solid #ddd;
}

.background-image {
  background-size: cover;
  background-position: center;
}

.slider-image {
  position: absolute;
  top: 0;
  background-size: cover;
}

.slider-track {
  position: relative;
  height: 40px;
  background: #f5f5f5;
  border-radius: 20px;
  border: 1px solid #ddd;
  cursor: pointer;
}

.slider-thumb {
  position: absolute;
  top: 0;
  left: 0;
  width: 40px;
  height: 40px;
  background: #409eff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  z-index: 2;
}

.thumb-icon {
  font-size: 18px;
}

.track-text {
  position: absolute;
  width: 100%;
  text-align: center;
  line-height: 40px;
  color: #666;
  user-select: none;
}

.refresh-btn {
  margin-top: 15px;
  padding: 8px 16px;
  background: #f5f5f5;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: pointer;
}

.refresh-btn:hover {
  background: #e8e8e8;
}
</style>

三、后端实现 (Node.js + Express)

1. 项目结构

backend/
├── src/
│   ├── controllers/
│   │   └── captcha.controller.js
│   ├── services/
│   │   └── captcha.service.js
│   ├── middleware/
│   │   └── captcha.middleware.js
│   ├── utils/
│   │   ├── image.generator.js
│   │   └── redis.client.js
│   └── app.js
├── public/
│   └── captcha-images/  # 存储验证码图片
├── config/
├── package.json

2. 主要依赖

{
  "dependencies": {
    "express": "^4.18.2",
    "redis": "^4.6.8",
    "sharp": "^0.32.6",
    "uuid": "^9.0.0",
    "canvas": "^2.11.2",
    "cors": "^2.8.5",
    "express-rate-limit": "^6.10.0"
  }
}

3. 验证码服务 (captcha.service.js)

const sharp = require('sharp');
const { createCanvas } = require('canvas');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs').promises;
const redis = require('../utils/redis.client');

class CaptchaService {
  constructor() {
    this.BG_WIDTH = 350;
    this.BG_HEIGHT = 200;
    this.SLIDER_SIZE = 50;
    this.EXPIRATION = 300; // 5分钟过期
  }

  // 生成验证码
  async generateCaptcha() {
    const captchaId = crypto.randomUUID();
    const targetX = Math.floor(Math.random() * (this.BG_WIDTH - this.SLIDER_SIZE - 20)) + 10;
    const targetY = Math.floor(Math.random() * (this.BG_HEIGHT - this.SLIDER_SIZE - 10)) + 5;

    // 生成背景图
    const bgBuffer = await this.generateBackground();
    // 生成滑块图
    const sliderBuffer = await this.generateSlider(bgBuffer, targetX, targetY);

    // 保存到Redis
    await redis.setex(`captcha:${captchaId}`, this.EXPIRATION, JSON.stringify({
      targetX,
      targetY,
      timestamp: Date.now()
    }));

    // 保存图片到文件系统(生产环境建议使用对象存储)
    const bgPath = `public/captcha-images/${captchaId}_bg.png`;
    const sliderPath = `public/captcha-images/${captchaId}_slider.png`;

    await fs.writeFile(bgPath, bgBuffer);
    await fs.writeFile(sliderPath, sliderBuffer);

    return {
      captchaId,
      bgImage: `/captcha-images/${captchaId}_bg.png`,
      sliderImage: `/captcha-images/${captchaId}_slider.png`,
      targetX,
      targetY,
      bgWidth: this.BG_WIDTH,
      bgHeight: this.BG_HEIGHT,
      sliderWidth: this.SLIDER_SIZE,
      sliderHeight: this.SLIDER_SIZE
    };
  }

  // 生成背景图
  async generateBackground() {
    const canvas = createCanvas(this.BG_WIDTH, this.BG_HEIGHT);
    const ctx = canvas.getContext('2d');

    // 随机背景色
    const hue = Math.floor(Math.random() * 360);
    ctx.fillStyle = `hsl(${hue}, 70%, 85%)`;
    ctx.fillRect(0, 0, this.BG_WIDTH, this.BG_HEIGHT);

    // 添加干扰元素
    for (let i = 0; i < 30; i++) {
      ctx.beginPath();
      ctx.arc(
        Math.random() * this.BG_WIDTH,
        Math.random() * this.BG_HEIGHT,
        Math.random() * 3,
        0,
        Math.PI * 2
      );
      ctx.fillStyle = `rgba(255,255,255,${Math.random() * 0.5})`;
      ctx.fill();
    }

    return canvas.toBuffer('image/png');
  }

  // 生成滑块
  async generateSlider(bgBuffer, targetX, targetY) {
    // 从背景图中裁剪出滑块区域
    const sliderBuffer = await sharp(bgBuffer)
      .extract({
        left: targetX,
        top: targetY,
        width: this.SLIDER_SIZE,
        height: this.SLIDER_SIZE
      })
      .toBuffer();

    // 添加边框效果
    return sharp(sliderBuffer)
      .composite([
        {
          input: Buffer.from([0, 0, 0, 100]),
          raw: { width: 1, height: this.SLIDER_SIZE, channels: 4 },
          top: 0,
          left: 0
        },
        {
          input: Buffer.from([255, 255, 255, 100]),
          raw: { width: 1, height: this.SLIDER_SIZE, channels: 4 },
          top: 0,
          left: this.SLIDER_SIZE - 1
        }
      ])
      .png()
      .toBuffer();
  }

  // 验证滑块位置
  async verifyCaptcha(captchaId, sliderPosition) {
    const data = await redis.get(`captcha:${captchaId}`);

    if (!data) {
      throw new Error('验证码已过期或不存在');
    }

    const { targetX, timestamp } = JSON.parse(data);

    // 防止重放攻击
    if (Date.now() - timestamp > this.EXPIRATION * 1000) {
      await redis.del(`captcha:${captchaId}`);
      throw new Error('验证码已过期');
    }

    // 计算误差(允许5%的误差)
    const trackWidth = 300; // 前端轨道宽度
    const targetPercentage = (targetX / this.BG_WIDTH) * 100;
    const currentPercentage = (sliderPosition / trackWidth) * 100;
    const tolerance = 5; // 5% 容忍度

    const isValid = Math.abs(currentPercentage - targetPercentage) < tolerance;

    if (isValid) {
      // 验证成功后删除验证码记录
      await redis.del(`captcha:${captchaId}`);

      // 生成验证令牌
      const token = crypto.randomBytes(32).toString('hex');
      await redis.setex(`captcha_token:${token}`, 600, 'valid'); // 10分钟有效

      return { success: true, token };
    }

    // 失败计数
    const failCount = await redis.incr(`captcha_fail:${captchaId}`);
    if (failCount >= 5) {
      await redis.del(`captcha:${captchaId}`);
      await redis.del(`captcha_fail:${captchaId}`);
    }

    return { success: false };
  }
}

module.exports = new CaptchaService();

4. 控制器 (captcha.controller.js)

const captchaService = require('../services/captcha.service');

class CaptchaController {
  // 生成验证码
  async generate(req, res) {
    try {
      const captchaData = await captchaService.generateCaptcha();
      res.json({
        code: 200,
        data: captchaData,
        message: '验证码生成成功'
      });
    } catch (error) {
      res.status(500).json({
        code: 500,
        message: '验证码生成失败'
      });
    }
  }

  // 验证验证码
  async verify(req, res) {
    try {
      const { captchaId, sliderPosition } = req.body;

      if (!captchaId || typeof sliderPosition !== 'number') {
        return res.status(400).json({
          code: 400,
          message: '参数错误'
        });
      }

      const result = await captchaService.verifyCaptcha(captchaId, sliderPosition);

      if (result.success) {
        res.json({
          code: 200,
          data: { token: result.token },
          message: '验证成功'
        });
      } else {
        res.status(400).json({
          code: 400,
          message: '验证失败'
        });
      }
    } catch (error) {
      res.status(400).json({
        code: 400,
        message: error.message
      });
    }
  }

  // 验证令牌中间件
  async validateToken(req, res, next) {
    try {
      const token = req.headers['x-captcha-token'] || req.body.captchaToken;

      if (!token) {
        return res.status(403).json({
          code: 403,
          message: '需要验证码验证'
        });
      }

      const redis = require('../utils/redis.client');
      const isValid = await redis.get(`captcha_token:${token}`);

      if (isValid !== 'valid') {
        return res.status(403).json({
          code: 403,
          message: '验证码无效或已过期'
        });
      }

      // 验证通过后删除令牌,防止重复使用
      await redis.del(`captcha_token:${token}`);

      next();
    } catch (error) {
      res.status(500).json({
        code: 500,
        message: '服务器错误'
      });
    }
  }
}

module.exports = new CaptchaController();

5. Redis配置 (redis.client.js)

const { createClient } = require('redis');

class RedisClient {
  constructor() {
    this.client = createClient({
      url: process.env.REDIS_URL || 'redis://localhost:6379',
      password: process.env.REDIS_PASSWORD || ''
    });

    this.client.on('error', (err) => {
      console.error('Redis Client Error:', err);
    });

    this.client.on('connect', () => {
      console.log('Redis connected successfully');
    });

    this.connect();
  }

  async connect() {
    await this.client.connect();
  }

  async setex(key, seconds, value) {
    return await this.client.setEx(key, seconds, value);
  }

  async get(key) {
    return await this.client.get(key);
  }

  async del(key) {
    return await this.client.del(key);
  }

  async incr(key) {
    return await this.client.incr(key);
  }
}

module.exports = new RedisClient();

6. 主应用 (app.js)

const express = require('express');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const captchaController = require('./controllers/captcha.controller');
const path = require('path');

const app = express();

// 安全限制
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 100 // 每个IP限制100个请求
});

// 中间件
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use('/captcha-images', express.static(path.join(__dirname, '../public/captcha-images')));

// 应用限流
app.use('/api/', limiter);

// 路由
app.get('/api/captcha/generate', captchaController.generate);
app.post('/api/captcha/verify', captchaController.verify);

// 受保护的路由示例
app.post('/api/protected/action', 
  captchaController.validateToken,
  (req, res) => {
    res.json({ message: '操作成功' });
  }
);

// 错误处理
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: '服务器内部错误' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`服务器运行在端口 ${PORT}`);
});

四、Nginx配置

1. 主配置文件

# /etc/nginx/nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/json;

    # 上传文件大小限制
    client_max_body_size 20M;

    # 安全头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    include /etc/nginx/conf.d/*.conf;
}

2. 站点配置

# /etc/nginx/conf.d/slider-captcha.conf

upstream backend {
    server 127.0.0.1:3000;
    # 可以添加更多后端服务器做负载均衡
    # server 127.0.0.1:3001;
    keepalive 32;
}

server {
    listen 80;
    server_name your-domain.com;  # 替换为你的域名
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your-domain.com;  # 替换为你的域名

    # SSL证书配置
    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # 静态文件服务(前端)
    location / {
        root /var/www/slider-captcha/frontend/dist;  # Vue/React构建后的目录
        try_files $uri $uri/ /index.html;

        # 缓存控制
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }

    # 验证码图片访问
    location /captcha-images {
        alias /var/www/slider-captcha/backend/public/captcha-images;

        # 设置缓存(验证码图片应该短时间内有效)
        expires 5m;
        add_header Cache-Control "public, max-age=300";

        # 防止目录列表
        autoindex off;

        # 安全限制
        location ~ \.php$ {
            deny all;
        }
    }

    # API代理
    location /api/ {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;

        # 超时设置
        proxy_connect_timeout 30s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;

        # 限制请求大小
        client_max_body_size 10M;

        # 安全头
        add_header X-Content-Type-Options nosniff;
        add_header X-Frame-Options DENY;
        add_header X-XSS-Protection "1; mode=block";
    }

    # 阻止敏感文件访问
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    location ~ /\.(env|git|svn) {
        deny all;
        access_log off;
        log_not_found off;
    }

    # 限流配置
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    location /api/captcha/generate {
        limit_req zone=api burst=5 nodelay;
        proxy_pass http://backend/api/captcha/generate;
    }

    location /api/captcha/verify {
        limit_req zone=api burst=3 nodelay;
        proxy_pass http://backend/api/captcha/verify;
    }
}

五、部署脚本

1. 安装脚本 (setup.sh)

#!/bin/bash

# 滑块验证系统部署脚本

set -e

echo "开始部署滑块验证系统..."

# 1. 安装系统依赖
echo "安装系统依赖..."
sudo apt-get update
sudo apt-get install -y nginx redis-server nodejs npm certbot python3-certbot-nginx

# 2. 创建项目目录
echo "创建项目目录..."
sudo mkdir -p /var/www/slider-captcha/{frontend,backend,logs}
sudo chown -R $USER:$USER /var/www/slider-captcha

# 3. 部署前端
echo "部署前端..."
cd /var/www/slider-captcha/frontend
# 这里假设你已经构建了前端项目
# npm install
# npm run build

# 4. 部署后端
echo "部署后端..."
cd /var/www/slider-captcha/backend
npm install

# 5. 创建服务文件
echo "创建系统服务..."

# 后端服务
sudo tee /etc/systemd/system/slider-backend.service << EOF
[Unit]
Description=Slider Captcha Backend Service
After=network.target redis.service

[Service]
Type=simple
User=$USER
WorkingDirectory=/var/www/slider-captcha/backend
ExecStart=/usr/bin/node src/app.js
Restart=on-failure
Environment=NODE_ENV=production
Environment=REDIS_URL=redis://localhost:6379
Environment=PORT=3000

[Install]
WantedBy=multi-user.target
EOF

# 6. 配置Nginx
echo "配置Nginx..."
sudo cp slider-captcha.conf /etc/nginx/conf.d/
sudo nginx -t

# 7. 申请SSL证书(如果需要)
read -p "是否需要申请SSL证书? (y/n): " ssl_choice
if [[ $ssl_choice == "y" || $ssl_choice == "Y" ]]; then
    read -p "请输入域名: " domain_name
    sudo certbot --nginx -d $domain_name
fi

# 8. 启动服务
echo "启动服务..."
sudo systemctl daemon-reload
sudo systemctl enable slider-backend
sudo systemctl start slider-backend
sudo systemctl restart nginx

echo "部署完成!"

2. 环境变量配置

# /var/www/slider-captcha/backend/.env

NODE_ENV=production
PORT=3000
REDIS_URL=redis://localhost:6379
REDIS_PASSWORD=your_redis_password
SESSION_SECRET=your_session_secret
CAPTCHA_EXPIRE=300
MAX_FAIL_ATTEMPTS=5

六、安全增强措施

1. 防机器请求

// 在captcha.service.js中添加
async checkRequestFrequency(ip) {
  const key = `req_freq:${ip}`;
  const count = await redis.incr(key);

  if (count === 1) {
    await redis.expire(key, 60); // 1分钟窗口
  }

  return count > 100; // 每分钟超过100次请求视为异常
}

2. 添加验证码难度调整

// 根据失败次数调整难度
getDifficultyLevel(failCount) {
  if (failCount > 10) return 'hard';
  if (failCount > 5) return 'medium';
  return 'easy';
}

// 根据难度生成不同的验证码
async generateWithDifficulty(difficulty) {
  const configs = {
    easy: { bgWidth: 300, sliderSize: 60, tolerance: 8 },
    medium: { bgWidth: 350, sliderSize: 50, tolerance: 5 },
    hard: { bgWidth: 400, sliderSize: 40, tolerance: 3 }
  };

  const config = configs[difficulty];
  // 使用配置生成验证码
}

七、监控和维护

1. 日志监控脚本

#!/bin/bash
# monitor.sh

LOG_FILE="/var/log/slider-captcha/monitor.log"
ERROR_THRESHOLD=10
REQUEST_THRESHOLD=1000

# 监控错误率
check_errors() {
    local error_count=$(sudo journalctl -u slider-backend --since "1 hour ago" | grep -c "ERROR")

    if [ $error_count -gt $ERROR_THRESHOLD ]; then
        echo "$(date): 错误率过高: $error_count errors/hour" >> $LOG_FILE
        # 可以添加邮件或短信通知
    fi
}

# 监控请求量
check_requests() {
    local request_count=$(tail -1000 /var/log/nginx/access.log | wc -l)

    if [ $request_count -gt $REQUEST_THRESHOLD ]; then
        echo "$(date): 请求量异常: $request_count requests/last-1000-logs" >> $LOG_FILE
    fi
}

# 清理过期图片
cleanup_images() {
    find /var/www/slider-captcha/backend/public/captcha-images -name "*.png" -mmin +10 -delete
}

# 执行监控
check_errors
check_requests
cleanup_images

八、使用示例

1. 前端使用

<template>
  <div>
    <form @submit.prevent="submitForm">
      <SliderCaptcha @verified="onVerified" ref="captcha" />

      <!-- 其他表单字段 -->
      <input type="email" v-model="email" placeholder="邮箱" />
      <input type="password" v-model="password" placeholder="密码" />

      <button type="submit" :disabled="!isCaptchaVerified">提交</button>
    </form>
  </div>
</template>

<script>
import SliderCaptcha from '@/components/SliderCaptcha.vue';

export default {
  components: { SliderCaptcha },
  data() {
    return {
      email: '',
      password: '',
      captchaToken: '',
      isCaptchaVerified: false
    };
  },
  methods: {
    onVerified(token) {
      this.captchaToken = token;
      this.isCaptchaVerified = true;
    },

    async submitForm() {
      try {
        const response = await this.$http.post('/api/login', {
          email: this.email,
          password: this.password,
          captchaToken: this.captchaToken
        });

        // 处理登录成功
      } catch (error) {
        // 验证失败,刷新验证码
        this.$refs.captcha.refresh();
        this.isCaptchaVerified = false;
      }
    }
  }
};
</script>

2. 后端验证中间件使用

const express = require('express');
const router = express.Router();
const captchaController = require('../controllers/captcha.controller');

// 需要滑块验证的路由
router.post('/login', 
  captchaController.validateToken,
  (req, res) => {
    // 处理登录逻辑
  }
);

router.post('/register',
  captchaController.validateToken,
  (req, res) => {
    // 处理注册逻辑
  }
);

部署步骤总结

环境准备

部署后端

部署前端

配置Nginx

启动服务

验证部署

这个实现方案包含了完整的前后端代码、Nginx配置和安全防护措施,可以根据实际需求进行调整和优化。

相关推荐