✨ 添加登录验证码功能 - 增强系统安全性
## 新增功能 - 密码输错2次后自动显示验证码 - 4位数字验证码,点击可刷新 - 验证码有效期5分钟 - 基于IP和用户名双重防护 - 前台和后台登录均支持 ## 后端改动 - 新增验证码生成API: GET /api/captcha - 修改登录API支持验证码验证 - 添加session管理验证码 - 增强RateLimiter防爆破机制 ## 前端改动 - 登录表单添加验证码输入框(条件显示) - 验证码图片展示和刷新功能 - 自动触发验证码显示逻辑 ## 依赖更新 - 新增: svg-captcha (验证码生成) - 新增: express-session (session管理) ## 文档 - CAPTCHA_FEATURE.md - 详细功能文档 - CAPTCHA_README.md - 快速开始指南 - test_captcha.sh - 自动化测试脚本 - 更新说明_验证码功能.txt - 中文说明 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
280
CAPTCHA_FEATURE.md
Normal file
280
CAPTCHA_FEATURE.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# 登录验证码功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
本次更新为"玩玩云"云存储系统添加了登录验证码功能,提高了系统的安全性。该功能会在用户输错密码一定次数后自动显示验证码,要求用户输入验证码才能继续尝试登录。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 智能验证码触发
|
||||
- **自动触发**:当用户输错密码2次后,系统会自动显示验证码输入框
|
||||
- **适用范围**:前台用户登录和后台管理员登录均适用
|
||||
- **双重保护**:基于IP地址和用户名两个维度进行失败次数统计
|
||||
|
||||
### 2. 验证码特点
|
||||
- **纯数字验证码**:4位数字,易于识别和输入
|
||||
- **彩色显示**:验证码图片使用彩色显示,提高可读性
|
||||
- **点击刷新**:点击验证码图片即可刷新获取新的验证码
|
||||
- **有效期限**:验证码有效期为5分钟,过期后需要刷新
|
||||
- **安全存储**:验证码存储在服务器端session中,防止客户端篡改
|
||||
|
||||
### 3. 防爆破机制
|
||||
- **失败限制**:15分钟内失败5次将被封锁30分钟
|
||||
- **渐进式保护**:
|
||||
- 第1-2次失败:仅提示密码错误
|
||||
- 第3-5次失败:显示验证码要求输入
|
||||
- 第5次失败:封锁IP和用户名30分钟
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 后端改动
|
||||
|
||||
#### 1. 新增依赖
|
||||
- `svg-captcha`: 用于生成SVG格式的验证码图片
|
||||
- `express-session`: 用于管理session存储验证码
|
||||
|
||||
#### 2. 新增API端点
|
||||
```
|
||||
GET /api/captcha
|
||||
```
|
||||
- 功能:生成并返回SVG格式的验证码图片
|
||||
- 返回:SVG图片数据
|
||||
- Session存储:验证码文本和生成时间
|
||||
|
||||
#### 3. 修改登录API
|
||||
```
|
||||
POST /api/login
|
||||
```
|
||||
新增参数:
|
||||
- `captcha` (可选): 验证码输入值
|
||||
|
||||
验证逻辑:
|
||||
1. 检查IP和用户名的失败次数
|
||||
2. 如果失败次数 >= 2,则要求提供验证码
|
||||
3. 验证验证码的有效性(是否存在、是否过期、是否正确)
|
||||
4. 验证码错误返回 `needCaptcha: true`
|
||||
|
||||
#### 4. RateLimiter增强
|
||||
- 新增 `getFailureCount()` 方法:获取指定key的失败次数
|
||||
- `recordFailure()` 返回值新增 `needCaptcha` 字段
|
||||
|
||||
### 前端改动
|
||||
|
||||
#### 1. 数据字段
|
||||
新增:
|
||||
```javascript
|
||||
showCaptcha: false, // 是否显示验证码
|
||||
captchaUrl: '', // 验证码图片URL
|
||||
loginForm.captcha: '' // 验证码输入值
|
||||
```
|
||||
|
||||
#### 2. UI组件
|
||||
在登录表单中添加:
|
||||
- 验证码输入框(条件显示)
|
||||
- 验证码图片显示区域
|
||||
- 点击刷新提示文字
|
||||
|
||||
#### 3. 逻辑方法
|
||||
新增 `refreshCaptcha()` 方法:
|
||||
```javascript
|
||||
refreshCaptcha() {
|
||||
this.captchaUrl = `${this.apiBase}/api/captcha?t=${Date.now()}`;
|
||||
}
|
||||
```
|
||||
|
||||
修改 `handleLogin()` 方法:
|
||||
- 登录失败时检查 `response.data.needCaptcha`
|
||||
- 如果需要验证码,显示验证码并调用 `refreshCaptcha()`
|
||||
- 登录成功后隐藏验证码并清空输入
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 用户使用流程
|
||||
|
||||
1. **首次登录尝试**
|
||||
- 输入用户名和密码
|
||||
- 点击"登录"按钮
|
||||
- 如果密码错误,会提示"用户名或密码错误"
|
||||
|
||||
2. **第三次登录尝试(触发验证码)**
|
||||
- 输入用户名和密码
|
||||
- 系统自动显示验证码输入框
|
||||
- 输入图片中显示的4位数字
|
||||
- 如果看不清,点击图片刷新验证码
|
||||
- 点击"登录"按钮
|
||||
|
||||
3. **验证码验证**
|
||||
- 如果验证码错误,会提示"验证码错误",验证码会自动刷新
|
||||
- 如果验证码过期,会提示"验证码已过期,请刷新验证码"
|
||||
- 验证码正确且密码正确,登录成功
|
||||
|
||||
4. **账号封锁**
|
||||
- 如果连续失败5次,账号将被封锁30分钟
|
||||
- 封锁期间尝试登录会提示"账号已被封禁"
|
||||
|
||||
### 管理员说明
|
||||
|
||||
管理员登录时同样受到验证码保护,流程与普通用户完全一致。
|
||||
|
||||
## 配置说明
|
||||
|
||||
### Session配置
|
||||
|
||||
在 `backend/server.js` 中配置session:
|
||||
|
||||
```javascript
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'your-session-secret-change-in-production',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.COOKIE_SECURE === 'true',
|
||||
httpOnly: true,
|
||||
maxAge: 10 * 60 * 1000 // 10分钟
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
建议在 `.env` 文件中设置:
|
||||
```
|
||||
SESSION_SECRET=你的session密钥
|
||||
```
|
||||
|
||||
### 验证码参数
|
||||
|
||||
在 `backend/server.js` 的验证码生成代码中可调整:
|
||||
|
||||
```javascript
|
||||
const captcha = svgCaptcha.create({
|
||||
size: 4, // 验证码长度(4位数字)
|
||||
noise: 2, // 干扰线条数
|
||||
color: true, // 使用彩色
|
||||
background: '#f0f0f0', // 背景色
|
||||
width: 120, // 宽度
|
||||
height: 40, // 高度
|
||||
fontSize: 50, // 字体大小
|
||||
charPreset: '0123456789' // 只使用数字
|
||||
});
|
||||
```
|
||||
|
||||
### 防爆破参数
|
||||
|
||||
在 `backend/server.js` 中配置RateLimiter:
|
||||
|
||||
```javascript
|
||||
const loginLimiter = new RateLimiter({
|
||||
maxAttempts: 5, // 最大失败次数
|
||||
windowMs: 15 * 60 * 1000, // 统计窗口(15分钟)
|
||||
blockDuration: 30 * 60 * 1000 // 封锁时长(30分钟)
|
||||
});
|
||||
```
|
||||
|
||||
**验证码触发阈值**在登录逻辑中设置:
|
||||
```javascript
|
||||
const needCaptcha = ipFailures >= 2 || usernameFailures >= 2;
|
||||
```
|
||||
可以修改 `>= 2` 来调整触发次数。
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **设置SESSION_SECRET**
|
||||
- 在生产环境中务必设置强随机的SESSION_SECRET
|
||||
- 使用 `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` 生成
|
||||
|
||||
2. **启用HTTPS**
|
||||
- 在生产环境中设置 `COOKIE_SECURE=true`
|
||||
- 确保使用HTTPS协议
|
||||
|
||||
3. **定期审计**
|
||||
- 定期检查登录失败日志
|
||||
- 关注异常的登录尝试
|
||||
|
||||
4. **调整参数**
|
||||
- 根据实际使用情况调整失败次数阈值
|
||||
- 根据用户反馈调整验证码难度
|
||||
|
||||
## 测试方法
|
||||
|
||||
### 测试验证码显示
|
||||
|
||||
1. 启动服务器:`cd backend && node server.js`
|
||||
2. 访问登录页面
|
||||
3. 使用错误的用户名或密码登录2次
|
||||
4. 第3次尝试时应该看到验证码输入框
|
||||
|
||||
### 测试验证码API
|
||||
|
||||
```bash
|
||||
curl "http://localhost:40001/api/captcha" > test.svg
|
||||
```
|
||||
|
||||
查看生成的 test.svg 文件,应该显示一个4位数字的验证码。
|
||||
|
||||
### 测试登录流程
|
||||
|
||||
```bash
|
||||
# 第一次失败(无验证码)
|
||||
curl -X POST http://localhost:40001/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"test","password":"wrong"}'
|
||||
|
||||
# 第二次失败(无验证码)
|
||||
curl -X POST http://localhost:40001/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"test","password":"wrong"}'
|
||||
|
||||
# 第三次失败(需要验证码)
|
||||
curl -X POST http://localhost:40001/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"test","password":"wrong"}'
|
||||
# 返回: {"success":false,"message":"请输入验证码","needCaptcha":true}
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题1:验证码不显示
|
||||
|
||||
**可能原因**:
|
||||
- Session未正确配置
|
||||
- 前端未正确接收 `needCaptcha` 标志
|
||||
|
||||
**解决方法**:
|
||||
- 检查浏览器控制台是否有错误
|
||||
- 检查后端日志是否有session相关错误
|
||||
- 确认 `express-session` 依赖已安装
|
||||
|
||||
### 问题2:验证码一直提示错误
|
||||
|
||||
**可能原因**:
|
||||
- Session未持久化
|
||||
- 验证码大小写不匹配
|
||||
|
||||
**解决方法**:
|
||||
- 验证码已统一转换为小写进行比较
|
||||
- 检查浏览器是否禁用了Cookie
|
||||
|
||||
### 问题3:验证码图片不加载
|
||||
|
||||
**可能原因**:
|
||||
- CORS配置问题
|
||||
- API路径错误
|
||||
|
||||
**解决方法**:
|
||||
- 检查 `ALLOWED_ORIGINS` 环境变量配置
|
||||
- 确认API基础路径配置正确
|
||||
|
||||
## 更新日志
|
||||
|
||||
**版本:1.1.0**
|
||||
- 新增登录验证码功能
|
||||
- 密码错误2次后自动显示验证码
|
||||
- 支持点击刷新验证码
|
||||
- 验证码有效期5分钟
|
||||
- 前台和后台登录均支持
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请查看:
|
||||
- 项目README.md
|
||||
- GitHub Issues
|
||||
- 后端日志文件
|
||||
173
CAPTCHA_README.md
Normal file
173
CAPTCHA_README.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# 登录验证码功能 - 快速开始
|
||||
|
||||
## 功能说明
|
||||
|
||||
本次更新为"玩玩云"添加了登录验证码功能,提高系统安全性:
|
||||
|
||||
✅ **自动触发**:密码输错2次后自动显示验证码
|
||||
✅ **智能保护**:基于IP和用户名双重维度防护
|
||||
✅ **易于使用**:点击图片即可刷新验证码
|
||||
✅ **前后通用**:前台用户和后台管理员登录均适用
|
||||
|
||||
## 安装依赖
|
||||
|
||||
已为后端安装以下依赖:
|
||||
```bash
|
||||
cd backend
|
||||
npm install svg-captcha express-session
|
||||
```
|
||||
|
||||
## 快速启动
|
||||
|
||||
### 1. 启动后端服务
|
||||
```bash
|
||||
cd backend
|
||||
node server.js
|
||||
```
|
||||
|
||||
### 2. 访问登录页面
|
||||
打开浏览器访问:`http://localhost:40001`
|
||||
|
||||
### 3. 测试验证码功能
|
||||
|
||||
**方法1:浏览器手动测试**
|
||||
1. 输入任意用户名和错误密码
|
||||
2. 点击"登录"按钮2次
|
||||
3. 第3次尝试时会自动显示验证码输入框
|
||||
4. 输入验证码后继续登录
|
||||
|
||||
**方法2:使用测试脚本**
|
||||
```bash
|
||||
# 确保后端服务已启动
|
||||
./test_captcha.sh
|
||||
```
|
||||
|
||||
## 使用截图流程
|
||||
|
||||
### 第1-2次登录失败
|
||||

|
||||
- 显示用户名和密码输入框
|
||||
- 提示"用户名或密码错误"
|
||||
|
||||
### 第3次登录失败(触发验证码)
|
||||

|
||||
- 自动显示验证码输入框
|
||||
- 显示4位数字验证码图片
|
||||
- 可点击图片刷新验证码
|
||||
|
||||
### 验证码验证
|
||||

|
||||
- 输入图片中的4位数字
|
||||
- 点击"登录"继续
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 环境变量配置(可选)
|
||||
|
||||
在 `backend/.env` 文件中添加:
|
||||
|
||||
```env
|
||||
# Session密钥(建议生产环境修改)
|
||||
SESSION_SECRET=your-session-secret-here
|
||||
|
||||
# Cookie安全(HTTPS环境启用)
|
||||
COOKIE_SECURE=false
|
||||
```
|
||||
|
||||
### 调整验证码触发次数
|
||||
|
||||
编辑 `backend/server.js`,找到以下代码:
|
||||
|
||||
```javascript
|
||||
// 第683行附近
|
||||
const needCaptcha = ipFailures >= 2 || usernameFailures >= 2;
|
||||
```
|
||||
|
||||
修改 `>= 2` 为你想要的次数(如 `>= 3` 表示第4次才需要验证码)。
|
||||
|
||||
### 调整防爆破参数
|
||||
|
||||
编辑 `backend/server.js`,找到以下代码:
|
||||
|
||||
```javascript
|
||||
// 第394行附近
|
||||
const loginLimiter = new RateLimiter({
|
||||
maxAttempts: 5, // 最大失败次数
|
||||
windowMs: 15 * 60 * 1000, // 15分钟
|
||||
blockDuration: 30 * 60 * 1000 // 封锁30分钟
|
||||
});
|
||||
```
|
||||
|
||||
## 文件修改清单
|
||||
|
||||
### 后端文件
|
||||
- ✅ `backend/server.js` - 添加验证码生成API和登录验证逻辑
|
||||
- ✅ `backend/package.json` - 添加验证码依赖
|
||||
|
||||
### 前端文件
|
||||
- ✅ `frontend/app.html` - 添加验证码输入框和图片显示
|
||||
- ✅ `frontend/app.js` - 添加验证码逻辑和刷新方法
|
||||
|
||||
### 新增文件
|
||||
- ✅ `CAPTCHA_FEATURE.md` - 详细功能说明文档
|
||||
- ✅ `CAPTCHA_README.md` - 快速开始指南
|
||||
- ✅ `test_captcha.sh` - 自动化测试脚本
|
||||
|
||||
## 功能特点
|
||||
|
||||
### 验证码特性
|
||||
- **纯数字**:只使用0-9数字,易于识别
|
||||
- **4位长度**:平衡安全性和用户体验
|
||||
- **彩色显示**:提高可读性
|
||||
- **点击刷新**:用户体验友好
|
||||
- **5分钟有效期**:防止验证码被重复使用
|
||||
|
||||
### 安全机制
|
||||
- **渐进式保护**:
|
||||
- 1-2次失败:仅密码验证
|
||||
- 3-5次失败:要求验证码
|
||||
- 5次失败:封锁30分钟
|
||||
- **双重维度**:同时基于IP和用户名统计
|
||||
- **Session存储**:验证码存储在服务器端,防止篡改
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 验证码不显示
|
||||
**检查项**:
|
||||
1. 确认后端服务已启动
|
||||
2. 检查浏览器控制台是否有错误
|
||||
3. 确认已输错密码至少2次
|
||||
|
||||
### 验证码一直错误
|
||||
**检查项**:
|
||||
1. 确认输入的是图片中的数字
|
||||
2. 刷新验证码重新尝试
|
||||
3. 检查浏览器是否禁用Cookie
|
||||
|
||||
### API返回错误
|
||||
**检查项**:
|
||||
1. 查看后端日志:`tail -f backend/logs/error.log`
|
||||
2. 确认依赖已安装:`cd backend && npm list svg-captcha express-session`
|
||||
3. 重启后端服务
|
||||
|
||||
## 测试清单
|
||||
|
||||
- [ ] 第1次登录失败不显示验证码
|
||||
- [ ] 第2次登录失败不显示验证码
|
||||
- [ ] 第3次登录失败显示验证码
|
||||
- [ ] 验证码图片可以正常加载
|
||||
- [ ] 点击验证码图片可以刷新
|
||||
- [ ] 输入正确验证码和正确密码可以登录成功
|
||||
- [ ] 输入错误验证码提示"验证码错误"
|
||||
- [ ] 管理员登录也受到验证码保护
|
||||
- [ ] 登录成功后验证码自动隐藏
|
||||
|
||||
## 技术支持
|
||||
|
||||
详细技术文档:[CAPTCHA_FEATURE.md](./CAPTCHA_FEATURE.md)
|
||||
|
||||
如有问题,请:
|
||||
1. 查看后端日志
|
||||
2. 检查浏览器控制台
|
||||
3. 运行测试脚本 `./test_captcha.sh`
|
||||
4. 查看详细文档
|
||||
109
backend/package-lock.json
generated
109
backend/package-lock.json
generated
@@ -12,15 +12,17 @@
|
||||
"archiver": "^7.0.1",
|
||||
"basic-ftp": "^5.0.4",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.18.2",
|
||||
"express-validator": "^7.3.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.2",
|
||||
"ssh2-sftp-client": "^12.0.1"
|
||||
"ssh2-sftp-client": "^12.0.1",
|
||||
"svg-captcha": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
@@ -352,17 +354,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "12.4.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
|
||||
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
|
||||
"version": "11.10.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
|
||||
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x || 22.x || 23.x || 24.x"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
@@ -1105,6 +1104,40 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
|
||||
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.7",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-headers": "~1.1.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "~2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express-validator": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.0.tgz",
|
||||
@@ -1976,6 +2009,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -1985,6 +2027,18 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/opentype.js": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.7.3.tgz",
|
||||
"integrity": "sha512-Veui5vl2bLonFJ/SjX/WRWJT3SncgiZNnKUyahmXCc2sa1xXW15u3R/3TN5+JFiP7RsjK5ER4HA5eWaEmV9deA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tiny-inflate": "^1.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"ot": "bin/ot"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -2130,6 +2184,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -2676,6 +2739,18 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-captcha": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/svg-captcha/-/svg-captcha-1.4.0.tgz",
|
||||
"integrity": "sha512-/fkkhavXPE57zRRCjNqAP3txRCSncpMx3NnNZL7iEoyAtYwUjPhJxW6FQTQPG5UPEmCrbFoXS10C3YdJlW7PDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"opentype.js": "^0.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
@@ -2713,6 +2788,12 @@
|
||||
"b4a": "^1.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -2782,6 +2863,18 @@
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"random-bytes": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.18.2",
|
||||
"express-validator": "^7.3.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.2",
|
||||
"ssh2-sftp-client": "^12.0.1"
|
||||
"ssh2-sftp-client": "^12.0.1",
|
||||
"svg-captcha": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
|
||||
@@ -4,6 +4,8 @@ require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const session = require('express-session');
|
||||
const svgCaptcha = require('svg-captcha');
|
||||
const SftpClient = require('ssh2-sftp-client');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
@@ -69,6 +71,18 @@ app.use(cors(corsOptions));
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
|
||||
// Session配置(用于验证码)
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'your-session-secret-change-in-production',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.COOKIE_SECURE === 'true',
|
||||
httpOnly: true,
|
||||
maxAge: 10 * 60 * 1000 // 10分钟
|
||||
}
|
||||
}));
|
||||
|
||||
// 安全响应头中间件
|
||||
app.use((req, res, next) => {
|
||||
// 防止点击劫持
|
||||
@@ -300,7 +314,8 @@ class RateLimiter {
|
||||
blocked: true,
|
||||
remainingAttempts: 0,
|
||||
resetTime: blockInfo.expiresAt,
|
||||
waitMinutes: Math.ceil((blockInfo.expiresAt - now) / 60000)
|
||||
waitMinutes: Math.ceil((blockInfo.expiresAt - now) / 60000),
|
||||
needCaptcha: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -329,7 +344,8 @@ class RateLimiter {
|
||||
blocked: true,
|
||||
remainingAttempts: 0,
|
||||
resetTime: blockExpiresAt,
|
||||
waitMinutes: Math.ceil(this.blockDuration / 60000)
|
||||
waitMinutes: Math.ceil(this.blockDuration / 60000),
|
||||
needCaptcha: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -337,10 +353,20 @@ class RateLimiter {
|
||||
blocked: false,
|
||||
remainingAttempts: this.maxAttempts - attemptInfo.count,
|
||||
resetTime: attemptInfo.windowEnd,
|
||||
waitMinutes: 0
|
||||
waitMinutes: 0,
|
||||
needCaptcha: attemptInfo.count >= 2 // 失败2次后需要验证码
|
||||
};
|
||||
}
|
||||
|
||||
// 获取失败次数
|
||||
getFailureCount(key) {
|
||||
const attemptInfo = this.attempts.get(key);
|
||||
if (!attemptInfo || Date.now() > attemptInfo.windowEnd) {
|
||||
return 0;
|
||||
}
|
||||
return attemptInfo.count;
|
||||
}
|
||||
|
||||
// 记录成功(清除失败记录)
|
||||
recordSuccess(key) {
|
||||
this.attempts.delete(key);
|
||||
@@ -568,6 +594,35 @@ app.get('/api/health', (req, res) => {
|
||||
res.json({ success: true, message: 'Server is running' });
|
||||
});
|
||||
|
||||
// 生成验证码API
|
||||
app.get('/api/captcha', (req, res) => {
|
||||
try {
|
||||
const captcha = svgCaptcha.create({
|
||||
size: 4, // 验证码长度
|
||||
noise: 2, // 干扰线条数
|
||||
color: true, // 使用彩色
|
||||
background: '#f0f0f0', // 背景色
|
||||
width: 120,
|
||||
height: 40,
|
||||
fontSize: 50,
|
||||
charPreset: '0123456789' // 只使用数字
|
||||
});
|
||||
|
||||
// 将验证码文本存储在session中
|
||||
req.session.captcha = captcha.text.toLowerCase();
|
||||
req.session.captchaTime = Date.now();
|
||||
|
||||
res.type('svg');
|
||||
res.send(captcha.data);
|
||||
} catch (error) {
|
||||
console.error('生成验证码失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '生成验证码失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 用户注册(简化版)
|
||||
app.post('/api/register',
|
||||
[
|
||||
@@ -641,18 +696,74 @@ app.post('/api/login',
|
||||
});
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
const { username, password, captcha } = req.body;
|
||||
|
||||
try {
|
||||
// 检查是否需要验证码
|
||||
const ipKey = req.rateLimitKeys?.ipKey;
|
||||
const usernameKey = req.rateLimitKeys?.usernameKey;
|
||||
const ipFailures = ipKey ? loginLimiter.getFailureCount(ipKey) : 0;
|
||||
const usernameFailures = usernameKey ? loginLimiter.getFailureCount(usernameKey) : 0;
|
||||
const needCaptcha = ipFailures >= 2 || usernameFailures >= 2;
|
||||
|
||||
// 如果需要验证码,则验证验证码
|
||||
if (needCaptcha) {
|
||||
if (!captcha) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请输入验证码',
|
||||
needCaptcha: true
|
||||
});
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
const sessionCaptcha = req.session.captcha;
|
||||
const captchaTime = req.session.captchaTime;
|
||||
|
||||
if (!sessionCaptcha || !captchaTime) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码已过期,请刷新验证码',
|
||||
needCaptcha: true
|
||||
});
|
||||
}
|
||||
|
||||
// 验证码有效期5分钟
|
||||
if (Date.now() - captchaTime > 5 * 60 * 1000) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码已过期,请刷新验证码',
|
||||
needCaptcha: true
|
||||
});
|
||||
}
|
||||
|
||||
if (captcha.toLowerCase() !== sessionCaptcha) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码错误',
|
||||
needCaptcha: true
|
||||
});
|
||||
}
|
||||
|
||||
// 验证通过后清除session中的验证码
|
||||
delete req.session.captcha;
|
||||
delete req.session.captchaTime;
|
||||
}
|
||||
|
||||
const user = UserDB.findByUsername(username);
|
||||
|
||||
if (!user) {
|
||||
// 记录失败尝试
|
||||
if (req.rateLimitKeys) {
|
||||
loginLimiter.recordFailure(req.rateLimitKeys.ipKey);
|
||||
const result = loginLimiter.recordFailure(req.rateLimitKeys.ipKey);
|
||||
if (req.rateLimitKeys.usernameKey) {
|
||||
loginLimiter.recordFailure(req.rateLimitKeys.usernameKey);
|
||||
}
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误',
|
||||
needCaptcha: result.needCaptcha
|
||||
});
|
||||
}
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
@@ -670,10 +781,15 @@ app.post('/api/login',
|
||||
if (!UserDB.verifyPassword(password, user.password)) {
|
||||
// 记录失败尝试
|
||||
if (req.rateLimitKeys) {
|
||||
loginLimiter.recordFailure(req.rateLimitKeys.ipKey);
|
||||
const result = loginLimiter.recordFailure(req.rateLimitKeys.ipKey);
|
||||
if (req.rateLimitKeys.usernameKey) {
|
||||
loginLimiter.recordFailure(req.rateLimitKeys.usernameKey);
|
||||
}
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误',
|
||||
needCaptcha: result.needCaptcha
|
||||
});
|
||||
}
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
|
||||
@@ -672,6 +672,16 @@
|
||||
<label class="form-label">密码</label>
|
||||
<input type="password" class="form-input" v-model="loginForm.password" required>
|
||||
</div>
|
||||
<div v-if="showCaptcha" class="form-group">
|
||||
<label class="form-label">验证码</label>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<input type="text" class="form-input" v-model="loginForm.captcha" required style="flex: 1;" placeholder="请输入验证码">
|
||||
<div style="cursor: pointer; border: 1px solid #ddd; border-radius: 4px; padding: 5px; background: #f0f0f0;" @click="refreshCaptcha">
|
||||
<img :src="captchaUrl" alt="验证码" style="display: block; width: 120px; height: 40px;" />
|
||||
</div>
|
||||
</div>
|
||||
<small style="color: #666; font-size: 12px;">点击图片刷新验证码</small>
|
||||
</div>
|
||||
<div style="text-align: right; margin-bottom: 15px;">
|
||||
<a @click="showForgotPasswordModal = true" style="color: #667eea; cursor: pointer; font-size: 14px; text-decoration: none;">
|
||||
忘记密码?
|
||||
|
||||
@@ -22,7 +22,8 @@ createApp({
|
||||
// 表单数据
|
||||
loginForm: {
|
||||
username: '',
|
||||
password: ''
|
||||
password: '',
|
||||
captcha: ''
|
||||
},
|
||||
registerForm: {
|
||||
username: '',
|
||||
@@ -30,6 +31,10 @@ createApp({
|
||||
password: ''
|
||||
},
|
||||
|
||||
// 验证码相关
|
||||
showCaptcha: false,
|
||||
captchaUrl: '',
|
||||
|
||||
// SFTP配置表单
|
||||
ftpConfigForm: {
|
||||
ftp_host: '',
|
||||
@@ -321,6 +326,10 @@ handleDragLeave(e) {
|
||||
this.user = response.data.user;
|
||||
this.isLoggedIn = true;
|
||||
|
||||
// 登录成功后隐藏验证码并清空验证码输入
|
||||
this.showCaptcha = false;
|
||||
this.loginForm.captcha = '';
|
||||
|
||||
// 保存token到localStorage
|
||||
localStorage.setItem('token', this.token);
|
||||
localStorage.setItem('user', JSON.stringify(this.user));
|
||||
@@ -378,7 +387,18 @@ handleDragLeave(e) {
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorMessage = error.response?.data?.message || '登录失败';
|
||||
|
||||
// 检查是否需要显示验证码
|
||||
if (error.response?.data?.needCaptcha) {
|
||||
this.showCaptcha = true;
|
||||
this.refreshCaptcha();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 刷新验证码
|
||||
refreshCaptcha() {
|
||||
this.captchaUrl = `${this.apiBase}/api/captcha?t=${Date.now()}`;
|
||||
},
|
||||
|
||||
async handleRegister() {
|
||||
|
||||
90
test_captcha.sh
Executable file
90
test_captcha.sh
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 登录验证码功能测试脚本
|
||||
|
||||
echo "================================"
|
||||
echo "登录验证码功能测试"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
BASE_URL="http://localhost:40001"
|
||||
|
||||
echo "1. 测试验证码API..."
|
||||
response=$(curl -s -w "\n%{http_code}" "$BASE_URL/api/captcha")
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
if [ "$http_code" = "200" ]; then
|
||||
echo "✓ 验证码API正常 (HTTP $http_code)"
|
||||
else
|
||||
echo "✗ 验证码API异常 (HTTP $http_code)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "2. 测试第一次登录失败(不需要验证码)..."
|
||||
response=$(curl -s -X POST "$BASE_URL/api/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"test","password":"wrong"}' \
|
||||
-c cookies.txt)
|
||||
echo "$response" | jq -r '.message'
|
||||
needCaptcha=$(echo "$response" | jq -r '.needCaptcha // false')
|
||||
if [ "$needCaptcha" = "false" ]; then
|
||||
echo "✓ 第一次失败不需要验证码"
|
||||
else
|
||||
echo "⚠ 第一次失败就需要验证码(可能之前已有失败记录)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "3. 测试第二次登录失败(不需要验证码)..."
|
||||
response=$(curl -s -X POST "$BASE_URL/api/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"test","password":"wrong"}' \
|
||||
-b cookies.txt -c cookies.txt)
|
||||
echo "$response" | jq -r '.message'
|
||||
needCaptcha=$(echo "$response" | jq -r '.needCaptcha // false')
|
||||
if [ "$needCaptcha" = "false" ]; then
|
||||
echo "✓ 第二次失败不需要验证码"
|
||||
else
|
||||
echo "⚠ 第二次失败就需要验证码(可能之前已有失败记录)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "4. 测试第三次登录失败(应该需要验证码)..."
|
||||
response=$(curl -s -X POST "$BASE_URL/api/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"test","password":"wrong"}' \
|
||||
-b cookies.txt -c cookies.txt)
|
||||
echo "$response" | jq -r '.message'
|
||||
needCaptcha=$(echo "$response" | jq -r '.needCaptcha // false')
|
||||
if [ "$needCaptcha" = "true" ]; then
|
||||
echo "✓ 第三次失败需要验证码"
|
||||
else
|
||||
echo "✗ 第三次失败应该需要验证码"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "5. 测试不提供验证码时登录..."
|
||||
response=$(curl -s -X POST "$BASE_URL/api/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin123"}' \
|
||||
-b cookies.txt -c cookies.txt)
|
||||
message=$(echo "$response" | jq -r '.message')
|
||||
echo "$message"
|
||||
if [[ "$message" == *"验证码"* ]]; then
|
||||
echo "✓ 正确要求输入验证码"
|
||||
else
|
||||
echo "⚠ 未要求验证码(用户可能不存在或之前没有失败记录)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 清理
|
||||
rm -f cookies.txt
|
||||
|
||||
echo "================================"
|
||||
echo "测试完成"
|
||||
echo "================================"
|
||||
echo ""
|
||||
echo "注意事项:"
|
||||
echo "1. 确保后端服务已启动 (node backend/server.js)"
|
||||
echo "2. 测试用户'test'可能不存在,这是正常的"
|
||||
echo "3. 如果要完整测试,请使用浏览器手动测试"
|
||||
echo "4. 防爆破机制会在失败5次后封锁30分钟"
|
||||
echo ""
|
||||
171
更新说明_验证码功能.txt
Normal file
171
更新说明_验证码功能.txt
Normal file
@@ -0,0 +1,171 @@
|
||||
====================================
|
||||
玩玩云 - 登录验证码功能更新说明
|
||||
====================================
|
||||
|
||||
更新时间:2025年11月21日
|
||||
版本:v1.1.0
|
||||
|
||||
一、功能概述
|
||||
----------------------------------
|
||||
|
||||
为"玩玩云"云存储系统添加了登录验证码功能,显著提升了系统的安全性。
|
||||
|
||||
核心特性:
|
||||
✅ 密码输错2次后自动显示验证码
|
||||
✅ 前台和后台登录均支持
|
||||
✅ 验证码可点击刷新
|
||||
✅ 基于IP和用户名双重防护
|
||||
✅ 失败5次封锁30分钟
|
||||
|
||||
二、使用方法
|
||||
----------------------------------
|
||||
|
||||
1. 正常登录流程:
|
||||
- 输入用户名和密码
|
||||
- 点击"登录"按钮
|
||||
- 如果密码正确,直接登录成功
|
||||
|
||||
2. 触发验证码流程:
|
||||
- 第1次密码错误:提示"用户名或密码错误"
|
||||
- 第2次密码错误:提示"用户名或密码错误"
|
||||
- 第3次尝试:自动显示验证码输入框
|
||||
- 输入验证码和密码
|
||||
- 点击"登录"按钮
|
||||
|
||||
3. 验证码操作:
|
||||
- 看不清?点击图片刷新验证码
|
||||
- 验证码是4位纯数字
|
||||
- 验证码有效期5分钟
|
||||
|
||||
三、技术实现
|
||||
----------------------------------
|
||||
|
||||
后端改动:
|
||||
1. 安装依赖:svg-captcha, express-session
|
||||
2. 新增API:GET /api/captcha (生成验证码)
|
||||
3. 修改API:POST /api/login (验证码验证)
|
||||
4. 增强防爆破机制
|
||||
|
||||
前端改动:
|
||||
1. 添加验证码显示区域
|
||||
2. 添加验证码输入框
|
||||
3. 实现自动触发逻辑
|
||||
4. 支持点击刷新
|
||||
|
||||
四、文件清单
|
||||
----------------------------------
|
||||
|
||||
修改的文件:
|
||||
backend/server.js - 后端主文件
|
||||
backend/package.json - 依赖配置
|
||||
frontend/app.html - 前端页面
|
||||
frontend/app.js - 前端逻辑
|
||||
|
||||
新增的文件:
|
||||
CAPTCHA_FEATURE.md - 详细功能文档
|
||||
CAPTCHA_README.md - 快速开始指南
|
||||
test_captcha.sh - 测试脚本
|
||||
更新说明_验证码功能.txt - 本文件
|
||||
|
||||
五、启动说明
|
||||
----------------------------------
|
||||
|
||||
1. 安装依赖(如果尚未安装):
|
||||
cd backend
|
||||
npm install
|
||||
|
||||
2. 启动后端服务:
|
||||
cd backend
|
||||
node server.js
|
||||
|
||||
3. 访问系统:
|
||||
打开浏览器访问 http://localhost:40001
|
||||
|
||||
六、测试方法
|
||||
----------------------------------
|
||||
|
||||
方法1:浏览器手动测试
|
||||
1. 访问登录页面
|
||||
2. 输入任意用户名和错误密码
|
||||
3. 连续点击"登录"按钮2次
|
||||
4. 第3次尝试时应该看到验证码
|
||||
|
||||
方法2:自动化测试脚本
|
||||
./test_captcha.sh
|
||||
|
||||
七、配置调整(可选)
|
||||
----------------------------------
|
||||
|
||||
1. 修改验证码触发次数:
|
||||
编辑 backend/server.js 第683行附近
|
||||
const needCaptcha = ipFailures >= 2 || usernameFailures >= 2;
|
||||
修改 >= 2 为你想要的次数
|
||||
|
||||
2. 修改防爆破参数:
|
||||
编辑 backend/server.js 第394行附近
|
||||
const loginLimiter = new RateLimiter({
|
||||
maxAttempts: 5, // 最大失败次数
|
||||
windowMs: 15 * 60 * 1000, // 15分钟
|
||||
blockDuration: 30 * 60 * 1000 // 封锁30分钟
|
||||
});
|
||||
|
||||
3. 设置Session密钥(生产环境推荐):
|
||||
编辑 backend/.env 文件
|
||||
SESSION_SECRET=your-random-secret-key
|
||||
|
||||
八、安全建议
|
||||
----------------------------------
|
||||
|
||||
✅ 已实现的安全措施:
|
||||
- 验证码采用SVG格式,防止OCR识别
|
||||
- 验证码存储在服务器端Session
|
||||
- 验证码有5分钟有效期
|
||||
- 基于IP和用户名双重防护
|
||||
- 失败5次自动封锁30分钟
|
||||
|
||||
🔒 建议的额外措施:
|
||||
- 在生产环境设置强随机SESSION_SECRET
|
||||
- 启用HTTPS并设置COOKIE_SECURE=true
|
||||
- 定期检查登录日志
|
||||
- 考虑添加登录通知功能
|
||||
|
||||
九、故障排查
|
||||
----------------------------------
|
||||
|
||||
问题1:验证码不显示
|
||||
解决:
|
||||
- 确认后端服务已启动
|
||||
- 检查浏览器控制台错误
|
||||
- 确认已输错密码至少2次
|
||||
|
||||
问题2:验证码一直错误
|
||||
解决:
|
||||
- 刷新验证码重试
|
||||
- 检查浏览器是否禁用Cookie
|
||||
- 清除浏览器缓存
|
||||
|
||||
问题3:验证码API 500错误
|
||||
解决:
|
||||
- 检查依赖是否安装:npm list svg-captcha express-session
|
||||
- 重新安装:npm install
|
||||
- 查看后端日志
|
||||
|
||||
十、技术支持
|
||||
----------------------------------
|
||||
|
||||
详细文档:
|
||||
- CAPTCHA_FEATURE.md - 完整功能说明和技术文档
|
||||
- CAPTCHA_README.md - 快速入门指南
|
||||
|
||||
测试工具:
|
||||
- test_captcha.sh - 自动化测试脚本
|
||||
|
||||
遇到问题:
|
||||
1. 查看后端日志
|
||||
2. 检查浏览器控制台
|
||||
3. 运行测试脚本
|
||||
4. 查看详细文档
|
||||
|
||||
====================================
|
||||
更新完成!享受更安全的云存储体验!
|
||||
====================================
|
||||
Reference in New Issue
Block a user