基于同济大学校园场景的九宫格图片验证码服务,采用无状态 JWT 方案,支持 Vercel 部署。
- 验证题型:随机出现"选择同济校内"或"选择非同济校内"两种题型
- 无状态设计:基于 JWT 的无状态验证,无需服务端存储
- Serverless 部署:适配 Vercel 等 Serverless 平台
- 服务:提供完整的 API 接口和演示页面
- 后端验证:提供
/api/siteverify供后端服务核验通行证
- 提示:
选择下列在同济校内的有: - 规则:只选择
public/assets/tongji/中的图片才算通过
- 提示:
选择下列不在同济校内的有: - 规则:只选择
public/assets/other/中的图片才算通过
TongjiCaptcha/
├── api/ # Vercel Serverless Functions
│ ├── captcha.ts # 生成验证码挑战
│ ├── verify.ts # 验证用户答案
│ └── siteverify.ts # 后端核验通行证
├── src/ # 前端源码
│ ├── index.ts # 主入口
│ ├── data.ts # 数据处理逻辑
│ ├── types.ts # TypeScript 类型定义
│ ├── tongjiAssets.ts # 同济资源管理
│ ├── assets/ # 静态资源
│ └── character/ # 字符资源
├── public/ # 公共资源
│ └── assets/
│ ├── tongji/ # 同济校内图片(必需)
│ └── other/ # 非同济图片(必需)
├── package.json
├── tsconfig.json
├── vercel.json # Vercel 部署配置
└── .env.example # 环境变量示例
- Node.js >= 18
- npm 或 pnpm
npm install
# 或
pnpm install复制 .env.example 为 .env 并配置:
cp .env.example .env编辑 .env 文件:
PORT=3001 # 本地开发端口
JWT_SECRET=your-secret-key # JWT 密钥(生产环境必须设置)在 public/assets/ 目录下创建两个文件夹:
tongji/:放置同济校内场景图片other/:放置非同济场景图片
支持的图片格式:.jpg、.jpeg、.png、.webp
npm run dev访问 http://localhost:3001 查看演示页面。
npm run build
npm startGET /api/captcha
响应示例:
{
"puzzle_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"questionType": "TONGJI_IN",
"prompt": "选择下列在同济校内的有:",
"images": [
"/assets/tongji/1.jpg",
"/assets/tongji/2.jpg",
"/assets/other/1.jpg",
"... 共9张"
]
}字段说明:
puzzle_token:加密的题目令牌,包含答案信息questionType:题型类型TONGJI_IN:选择同济校内场景TONGJI_NOT_IN:选择非同济场景
prompt:提示文本images:9 张图片的 URL 数组
POST /api/verify
请求体:
{
"puzzle_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"selected_indices": [0, 3, 5]
}成功响应:
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"duration": 1234
}失败响应:
{
"success": false,
"message": "验证失败"
}字段说明:
success:验证是否成功token:通行证令牌(验证成功时返回)duration:验证耗时(毫秒)
POST /api/siteverify
请求体:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}响应:
{
"success": true
}说明: 供后端服务调用,验证用户提交的通行证是否有效。
- 注册 Vercel 账号
- 安装 Vercel CLI:
npm i -g vercel
在 Vercel 项目设置中添加环境变量:
JWT_SECRET=your-production-secret-key
vercel或通过 GitHub 集成自动部署。
在 Vercel 项目设置中配置自定义域名。
<!DOCTYPE html>
<html>
<head>
<title>验证码示例</title>
</head>
<body>
<div id="captcha-container"></div>
<button onclick="verifyCaptcha()">提交</button>
<script>
let puzzleToken = '';
let selectedIndices = [];
// 加载验证码
async function loadCaptcha() {
const res = await fetch('/api/captcha');
const data = await res.json();
puzzleToken = data.puzzle_token;
// 渲染九宫格
const container = document.getElementById('captcha-container');
container.innerHTML = `<p>${data.prompt}</p>`;
data.images.forEach((img, index) => {
const imgEl = document.createElement('img');
imgEl.src = img;
imgEl.onclick = () => toggleSelect(index);
container.appendChild(imgEl);
});
}
function toggleSelect(index) {
const idx = selectedIndices.indexOf(index);
if (idx > -1) {
selectedIndices.splice(idx, 1);
} else {
selectedIndices.push(index);
}
}
// 验证
async function verifyCaptcha() {
const res = await fetch('/api/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
puzzle_token: puzzleToken,
selected_indices: selectedIndices
})
});
const data = await res.json();
if (data.success) {
console.log('验证成功,通行证:', data.token);
// 将 token 提交给后端
} else {
alert('验证失败,请重试');
loadCaptcha();
}
}
loadCaptcha();
</script>
</body>
</html>import { useState, useEffect } from 'react';
function TongjiCaptcha({ onSuccess }) {
const [captcha, setCaptcha] = useState(null);
const [selected, setSelected] = useState([]);
useEffect(() => {
loadCaptcha();
}, []);
const loadCaptcha = async () => {
const res = await fetch('/api/captcha');
const data = await res.json();
setCaptcha(data);
setSelected([]);
};
const toggleSelect = (index) => {
setSelected(prev =>
prev.includes(index)
? prev.filter(i => i !== index)
: [...prev, index]
);
};
const verify = async () => {
const res = await fetch('/api/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
puzzle_token: captcha.puzzle_token,
selected_indices: selected
})
});
const data = await res.json();
if (data.success) {
onSuccess(data.token);
} else {
alert('验证失败');
loadCaptcha();
}
};
if (!captcha) return <div>加载中...</div>;
return (
<div>
<p>{captcha.prompt}</p>
<div className="grid grid-cols-3 gap-2">
{captcha.images.map((img, index) => (
<img
key={index}
src={img}
onClick={() => toggleSelect(index)}
className={selected.includes(index) ? 'selected' : ''}
/>
))}
</div>
<button onClick={verify}>提交</button>
</div>
);
}- JWT 密钥:生产环境必须设置强随机密钥
- HTTPS:生产环境必须使用 HTTPS
- 速率限制:建议在 API 网关层添加速率限制
- 图片资源:定期更新图片库,避免被机器学习破解
A: 将图片放入 public/assets/tongji/ 或 public/assets/other/ 目录即可,服务会自动扫描。
A: 支持 .jpg、.jpeg、.png、.webp 格式。
A: 修改 api/captcha.ts 中的 prompt 字段。
A: 默认无限期有效,可在 JWT 签发时添加 exp 字段设置过期时间。
- 运行时:Node.js
- 框架:Vercel Serverless Functions
- 认证:JWT (jsonwebtoken)
- 语言:TypeScript