一、系统架构设计
用户浏览器 → 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) => {
// 处理注册逻辑
}
);
部署步骤总结
环境准备
- 安装 Node.js、Nginx、Redis
- 配置防火墙规则
部署后端
- 复制后端代码到服务器
- 安装依赖:
npm install
- 配置环境变量
- 启动服务:
pm2 start app.js
部署前端
- 构建前端项目:
npm run build
- 复制构建文件到Nginx目录
配置Nginx
- 复制提供的Nginx配置
- 修改域名和路径
- 配置SSL证书
启动服务
- 重启Nginx:
sudo systemctl restart nginx
- 启动Redis:
sudo systemctl start redis
验证部署
- 访问网站查看验证码是否正常显示
- 测试验证码功能
- 监控错误日志
这个实现方案包含了完整的前后端代码、Nginx配置和安全防护措施,可以根据实际需求进行调整和优化。