feat: production hardening - CORS whitelist, strong password, tokenVersion revoke, VIP card hashing, admin secret

This commit is contained in:
2026-05-10 22:53:32 +08:00
parent 5b23c88df9
commit 21709e5d97
9 changed files with 299 additions and 69 deletions

View File

@@ -1,13 +1,13 @@
# 数据库连接 # 数据库连接
DATABASE_URL="postgresql://postgres:password@localhost:5432/maqt?schema=public" DATABASE_URL="postgresql://postgres:password@localhost:5432/maqt?schema=public"
# JWT 密钥(修改为随机字符串) # JWT 密钥(务必修改为随机字符串)
JWT_SECRET="your-super-secret-jwt-key-change-this-in-production" JWT_SECRET="change-me-to-a-random-64-char-string"
# JWT 过期时间 # JWT 过期时间
JWT_EXPIRES_IN="7d" JWT_EXPIRES_IN="7d"
# 数据加密密钥32字节修改) # 数据加密密钥32字节务必修改)
ENCRYPTION_KEY="0123456789abcdef0123456789abcdef" ENCRYPTION_KEY="0123456789abcdef0123456789abcdef"
# 服务端口 # 服务端口
@@ -16,5 +16,8 @@ PORT=3001
# 环境 # 环境
NODE_ENV="development" NODE_ENV="development"
# VIP 卡密批次密钥(用于生成卡密签名) # CORS 允许的来源 (逗号分隔)
BATCH_SECRET="your-batch-secret-key" ALLOWED_ORIGINS="http://localhost:5173,app://.,file://"
# 管理员密钥 (用于 /api/admin 端点)
ADMIN_SECRET="change-me-to-random"

192
API_FULL_ANALYSIS.md Normal file
View File

@@ -0,0 +1,192 @@
# 码枪堂 API 全线分析报告
> 基于 Fiddler 抓包 ss3.saz原版 API+ ss4.saz自定义服务器对比
---
## 一、API 域名概览
| 域名 | 用途 |
|------|------|
| `https://maqt.top/api/*` | 主业务 API |
| `https://pop.maqt.top/api/*` | 弹窗广告 API |
| `https://update.maqt.top/*` | 更新服务 |
| `https://tuku.maqt.top/*` | 图片托管 |
| `http://100.105.17.52:3001/api/*` | 自定义后端(解包版) |
---
## 二、完整 API 端点清单
### 🔐 认证
| 方法 | 端点 | 请求体 | 当前状态 |
|------|------|--------|----------|
| POST | `/api/login` | `{"username","password","installId","deviceHash","platform","osVersion","appVersion"}` | ✅ 已实现 |
| POST | `/api/register` | 需要抓包确认 | ❌ 待实现 |
| POST | `/api/activate-vip` | 需要抓包确认 | ✅ 已实现 |
### 📊 会话 & 状态
| 方法 | 端点 | 当前状态 |
|------|------|----------|
| GET | `/api/session-status` | ✅ 已实现 |
| GET | `/api/vip-status` | ❌ 待实现 |
| GET | `/api/user/stats/{userId}` | ❌ 待实现 |
| GET | `/api/user/favorited-count/{userId}` | ❌ 待实现 |
| GET | `/api/user/limits/{userId}` | ❌ 待实现 |
| GET | `/api/user/schemes/{userId}` | ❌ 待实现 |
### 🎯 改枪方案
| 方法 | 端点 | 参数 | 当前状态 |
|------|------|------|----------|
| GET | `/api/category/{type}` | AR/SMG/SR/LMG/SG/Pistol/Launcher | ✅ 已实现 |
| GET | `/api/schemes` | `sort=hot&page=1&limit=12&source=...` | ✅ 已实现 |
| GET | `/api/schemes` | `sort=hot&page=1&limit=12&weaponCategory&weaponName` | ✅ 已实现 |
| POST | `/api/schemes/{id}/use` | 需要抓包确认 | ❌ 待实现 |
### ❤️ 收藏
| 方法 | 端点 | 当前状态 |
|------|------|----------|
| GET | `/api/favorites/count` | ✅ 已实现 |
| GET | `/api/favorites/check?schemeId={id}&source=...` | ✅ 已实现 |
### 🎯 弹出广告
| 方法 | 端点 | 当前状态 |
|------|------|----------|
| GET | `/api/popups/test_beta_01` | ❌ 待实现 |
| GET | `/api/aftersale-tutorial-popup` | ❌ 待实现 |
| GET | `/api/software-version-ad` | ❌ 待实现 |
### 🔄 更新服务
| 方法 | 端点 | 备注 |
|------|------|------|
| GET | `/latest.yml` | Electron auto-updater 配置 |
| GET | `/update-config.json` | 更新配置 |
### 🖼️ 图片托管
| 方法 | 端点 | 备注 |
|------|------|------|
| GET | `tuku.maqt.top/i/2026/03/22/*.png` | 静态图片资源 |
---
## 三、ss3 vs ss4 请求对比(关键差异)
### 原版ss3完整流程
```
1. 检查更新: GET update.maqt.top/{latest.yml,update-config.json}
2. 弹窗广告: GET pop.maqt.top/api/software-version-ad
3. 会话状态: GET maqt.top/api/session-status (高频轮询!)
4. 注册/登录: POST maqt.top/api/register
5. 获取分类: GET maqt.top/api/category/{type}
6. 获取方案: GET maqt.top/api/schemes?...
7. 收藏检查: GET maqt.top/api/favorites/check?schemeId=...
8. 激活VIP: POST maqt.top/api/activate-vip
9. VIP状态: GET maqt.top/api/vip-status
10. 用户数据: GET maqt.top/api/user/{stats,favorited-count,limits,schemes}/{id}
11. 方案使用: POST maqt.top/api/schemes/{id}/use
12. 图片加载: GET tuku.maqt.top/... (持续轮询)
```
### 自定义服务器ss4 后半段)请求序列:
```
162: GET /api/popups/test_beta_01
163: GET /api/session-status
164: GET /api/software-version-ad
165: GET /api/aftersale-tutorial-popup
166: GET /update-config.json
169: GET /api/software-version-ad
170: GET /api/popups/test_beta_01
171: GET /api/aftersale-tutorial-popup
172: GET /update-config.json
173: GET /api/category/AR
174: GET /api/schemes?sort=hot&page=1&limit=12&source=...
175: GET /api/category/SMG
176: GET /api/category/SR
177: GET /api/category/LMG
178: GET /api/category/SG
179: GET /api/category/Pistol
180: GET /api/category/Launcher
181: POST /api/login
182: GET /api/session-status
183: GET /api/favorites/count
184: GET /api/schemes?...
185: GET /api/schemes?...
186: GET /api/favorites/count
187: GET /api/session-status
190: GET /api/session-status
194-199: GET /api/session-status (轮询...)
```
### ❗ 自定义服务器缺失的端点
以下端点在原版中有调用,但在自定义服务器请求中从未出现:
- **POST `/api/register`** — 注册功能
- **POST `/api/activate-vip`** — VIP 激活(端点存在但可能未被调用)
- **GET `/api/vip-status`** — VIP 状态查询
- **GET `/api/user/stats/{id}`** — 用户统计
- **GET `/api/user/favorited-count/{id}`** — 用户收藏数
- **GET `/api/user/limits/{id}`** — 用户限制
- **GET `/api/user/schemes/{id}`** — 用户方案列表
- **POST `/api/schemes/{id}/use`** — 方案使用记录
---
## 四、重新封装开发计划
### 阶段 1API 补充(先补齐缺失端点)
**优先级 P0应用启动必需**
- [ ] `GET /api/vip-status` — 登录后立刻调用,缺了前端一直没 VIP
- [ ] `GET /api/user/stats/:id`
- [ ] `GET /api/user/favorited-count/:id`
- [ ] `GET /api/user/limits/:id`
- [ ] `GET /api/user/schemes/:id`
- [ ] `POST /api/schemes/:id/use`
**优先级 P1弹窗广告不影响核心功能**
- [ ] `GET /api/software-version-ad`
- [ ] `GET /api/popups/test_beta_01`
- [ ] `GET /api/aftersale-tutorial-popup`
- [ ] `GET /api/update-config.json`
**优先级 P2注册一次性的**
- [ ] `POST /api/register`(含 installId, deviceHash
### 阶段 2桌面端复刻Electron + React
等 API 补全后,桌面端从头写:
```
maqt-desktop/
├── electron/
│ ├── main.ts # 窗口管理
│ └── preload.ts # electronAPI
├── src/
│ ├── App.tsx
│ ├── pages/
│ │ ├── Home.tsx # 桌面首页
│ │ ├── Login.tsx
│ │ ├── Schemes.tsx # 改枪方案
│ │ ├── Filters.tsx # 画面滤镜
│ │ ├── Optimization.tsx
│ │ └── ...
│ ├── components/
│ │ ├── Dock.tsx # 底部导航
│ │ ├── DesktopIcons.tsx
│ │ └── ...
│ └── hooks/
│ ├── useAuth.ts
│ └── useApi.ts
├── native/ # 原生程序
│ ├── MaqiangTangh1.exe
│ ├── nvidiaProfileInspector.exe
│ └── tools/
└── package.json
```
### 阶段 3原生功能对接
- [ ] spawn `MaqiangTangh1.exe` 执行系统优化
- [ ] spawn `MaqiangTangXiXiOverlay.exe` 游戏内 Overlay
- [ ] 调用 PowerShell 脚本 Gamma 校准
- [ ] NVIDIA Profile Inspector 配置
- [ ] 硬件监控面板

View File

@@ -25,6 +25,9 @@ model User {
vipLevel Int @default(0) @map("vip_level") vipLevel Int @default(0) @map("vip_level")
vipExpireAt DateTime? @map("vip_expire_at") vipExpireAt DateTime? @map("vip_expire_at")
// Token 吊销 (递增版本号使旧 token 失效)
tokenVersion Int @default(0) @map("token_version")
// 统计 // 统计
schemesCount Int @default(0) @map("schemes_count") schemesCount Int @default(0) @map("schemes_count")
favoritesCount Int @default(0) @map("favorites_count") favoritesCount Int @default(0) @map("favorites_count")

View File

@@ -4,15 +4,17 @@ import crypto from 'crypto';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
/** /**
* 生成 VIP 卡密 * 生成 VIP 卡密 (原始 + 哈希)
*/ */
function generateCardKey(days: number): string { function generateCardKey(days: number): { raw: string; hash: string } {
const prefix = `VIP${days}`; const prefix = `VIP${days}`;
const segments = []; const segments = [];
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
segments.push(crypto.randomBytes(2).toString('hex').toUpperCase()); segments.push(crypto.randomBytes(2).toString('hex').toUpperCase());
} }
return `${prefix}-${segments.join('-')}`; const raw = `${prefix}-${segments.join('-')}`;
const hash = crypto.createHash('sha256').update(raw).digest('hex');
return { raw, hash };
} }
async function main() { async function main() {
@@ -22,79 +24,59 @@ async function main() {
// 创建分类 // 创建分类
// ============================================ // ============================================
const categories = [ const categories = [
// 烽火地带
{ name: '突击步枪', type: 'SCHEME' }, { name: '突击步枪', type: 'SCHEME' },
{ name: '冲锋枪', type: 'SCHEME' }, { name: '冲锋枪', type: 'SCHEME' },
{ name: '狙击步枪', type: 'SCHEME' }, { name: '狙击步枪', type: 'SCHEME' },
{ name: '轻机枪', type: 'SCHEME' }, { name: '轻机枪', type: 'SCHEME' },
{ name: '霰弹枪', type: 'SCHEME' }, { name: '霰弹枪', type: 'SCHEME' },
{ name: '手枪', type: 'SCHEME' }, { name: '手枪', type: 'SCHEME' },
{ name: '发射器', type: 'SCHEME' },
// 全面战场
{ name: '突击步枪', type: 'SCHEME_AOB' }, { name: '突击步枪', type: 'SCHEME_AOB' },
{ name: '冲锋枪', type: 'SCHEME_AOB' }, { name: '冲锋枪', type: 'SCHEME_AOB' },
{ name: '狙击步枪', type: 'SCHEME_AOB' }, { name: '狙击步枪', type: 'SCHEME_AOB' },
{ name: '轻机枪', type: 'SCHEME_AOB' }, { name: '轻机枪', type: 'SCHEME_AOB' },
{ name: '霰弹枪', type: 'SCHEME_AOB' }, { name: '霰弹枪', type: 'SCHEME_AOB' },
// 滤镜
{ name: '烽火地带', type: 'FILTER' }, { name: '烽火地带', type: 'FILTER' },
{ name: '全面战场', type: 'FILTER' }, { name: '全面战场', type: 'FILTER' },
{ name: '通用', type: 'FILTER' }, { name: '通用', type: 'FILTER' },
]; ];
for (const cat of categories) { for (const cat of categories) {
await prisma.category.create({ await prisma.category.create({ data: cat }).catch(() => {});
data: cat,
}).catch(() => {}); // 忽略重复错误
} }
console.log(`✅ 创建 ${categories.length} 个分类`); console.log(`✅ 创建 ${categories.length} 个分类`);
// ============================================ // ============================================
// 生成 VIP 卡密 // 生成 VIP 卡密 (存储哈希,输出原始值仅此一次)
// ============================================ // ============================================
const testKey = { raw: 'VIP365-0000-0000-0000-0000', hash: crypto.createHash('sha256').update('VIP365-0000-0000-0000-0000').digest('hex') };
await prisma.vipCard.create({
data: { cardKey: testKey.hash, cardType: 'YEAR', days: 365 },
}).catch(() => {});
console.log(`\n🔑 测试卡密 (仅显示一次): ${testKey.raw}`);
const cardConfigs = [ const cardConfigs = [
{ type: 'MONTH', days: 30, count: 100 }, { type: 'MONTH', days: 30, count: 100 },
{ type: 'QUARTER', days: 90, count: 50 }, { type: 'QUARTER', days: 90, count: 50 },
{ type: 'YEAR', days: 365, count: 20 }, { type: 'YEAR', days: 365, count: 20 },
]; ];
// 测试卡密 (固定值,方便开发验证)
await prisma.vipCard.create({
data: {
cardKey: 'VIP365-0000-0000-0000-0000',
cardType: 'YEAR',
days: 365,
},
}).catch(() => {});
console.log(`✅ 创建测试卡密: VIP365-0000-0000-0000-0000`);
let totalCards = 0; let totalCards = 0;
for (const config of cardConfigs) { for (const config of cardConfigs) {
const cards = []; const cards = [];
console.log(`\n📦 ${config.type} 卡密 (${config.count} 张):`);
for (let i = 0; i < config.count; i++) { for (let i = 0; i < config.count; i++) {
cards.push({ const { raw, hash } = generateCardKey(config.days);
cardKey: generateCardKey(config.days), cards.push({ cardKey: hash, cardType: config.type, days: config.days });
cardType: config.type, console.log(` ${raw}`);
days: config.days,
});
} }
await prisma.vipCard.createMany({ data: cards, skipDuplicates: true }); await prisma.vipCard.createMany({ data: cards, skipDuplicates: true });
totalCards += config.count; totalCards += config.count;
console.log(`✅ 生成 ${config.type} 卡密 ${config.count}`);
} }
console.log(`\n🎉 种子数据初始化完成!`); console.log(`\n🎉 种子数据初始化完成!`);
console.log(` ⚠️ 以上卡密仅显示一次,请妥善保存`);
console.log(` - 分类:${categories.length}`); console.log(` - 分类:${categories.length}`);
console.log(` - VIP 卡密:${totalCards}`); console.log(` - VIP 卡密:${totalCards + 1}`);
} }
main()
.catch((e) => {
console.error('❌ 种子数据初始化失败:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -27,9 +27,21 @@ app.use(helmet({
crossOriginEmbedderPolicy: false, crossOriginEmbedderPolicy: false,
})); }));
// CORS // CORS — 仅允许白名单来源
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',') || [
'http://localhost:5173',
'http://localhost:3000',
'app://.',
'file://',
];
app.use(cors({ app.use(cors({
origin: true, // 允许所有来源(开发模式) origin: (origin, callback) => {
if (!origin || ALLOWED_ORIGINS.some(o => origin.startsWith(o))) {
callback(null, true);
} else {
callback(new Error('CORS blocked'));
}
},
credentials: true, credentials: true,
})); }));
@@ -221,11 +233,11 @@ releaseDate: '2024-01-01T00:00:00.000Z'
}); });
// ============================================ // ============================================
// 开发工具端点 (仅 NODE_ENV=development) // 开发工具端点 (需 ADMIN_SECRET)
// ============================================
app.post('/api/admin/set-vip', async (req, res) => { app.post('/api/admin/set-vip', async (req, res) => {
if (process.env.NODE_ENV !== 'development') { const secret = process.env.ADMIN_SECRET;
return res.status(403).json({ success: false, message: '仅开发环境可用' }); if (!secret || req.headers['x-admin-secret'] !== secret) {
return res.status(403).json({ success: false, message: '禁止访问' });
} }
try { try {
const { username, isVip } = req.body; const { username, isVip } = req.body;

View File

@@ -44,10 +44,10 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc
}); });
} }
// 检查用户是否存在且未被删除 // 检查用户是否存在且未被删除,同时验证 tokenVersion 一致
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: payload.userId }, where: { id: payload.userId },
select: { id: true, deletedAt: true }, select: { id: true, deletedAt: true, tokenVersion: true },
}); });
if (!user || user.deletedAt) { if (!user || user.deletedAt) {
@@ -58,6 +58,15 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc
}); });
} }
// token 已被吊销 (登出或密码修改后旧 token 失效)
if (user.tokenVersion !== payload.tokenVersion) {
return res.status(401).json({
success: false,
message: '访问令牌已失效,请重新登录',
code: 'TOKEN_EXPIRED',
});
}
// 将用户信息附加到请求对象 // 将用户信息附加到请求对象
req.user = payload; req.user = payload;
next(); next();

View File

@@ -13,7 +13,10 @@ const prisma = new PrismaClient();
// ============================================ // ============================================
const registerSchema = z.object({ const registerSchema = z.object({
username: z.string().min(3).max(50), username: z.string().min(3).max(50),
password: z.string().min(6).max(100), password: z.string().min(8, '密码至少8位').max(100)
.regex(/[A-Z]/, '密码需包含大写字母')
.regex(/[a-z]/, '密码需包含小写字母')
.regex(/[0-9]/, '密码需包含数字'),
email: z.string().email(), email: z.string().email(),
installId: z.string().optional(), installId: z.string().optional(),
deviceHash: z.string().optional(), deviceHash: z.string().optional(),
@@ -68,6 +71,7 @@ router.post('/register', async (req: Request, res: Response) => {
email: user.email, email: user.email,
isVip: user.isVip, isVip: user.isVip,
vipLevel: user.vipLevel, vipLevel: user.vipLevel,
tokenVersion: user.tokenVersion,
}); });
// 记录日志 // 记录日志
@@ -170,6 +174,7 @@ router.post('/login', async (req: Request, res: Response) => {
email: user.email, email: user.email,
isVip: user.isVip, isVip: user.isVip,
vipLevel: user.vipLevel, vipLevel: user.vipLevel,
tokenVersion: user.tokenVersion,
}); });
// 记录日志 // 记录日志
@@ -210,6 +215,22 @@ router.post('/login', async (req: Request, res: Response) => {
} }
}); });
// ============================================
// 登出 (使当前 token 失效)
// ============================================
router.post('/logout', authMiddleware, async (req: Request, res: Response) => {
try {
await prisma.user.update({
where: { id: req.user!.userId },
data: { tokenVersion: { increment: 1 } },
});
res.json({ success: true, message: '已登出' });
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({ success: false, message: '登出失败' });
}
});
// ============================================ // ============================================
// 会话状态 (从数据库查询最新状态,避免 token 缓存导致 isVip 过期) // 会话状态 (从数据库查询最新状态,避免 token 缓存导致 isVip 过期)
// ============================================ // ============================================

View File

@@ -1,5 +1,6 @@
import { Router, Request, Response } from 'express'; import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import crypto from 'crypto';
import { z } from 'zod'; import { z } from 'zod';
import { authMiddleware } from '../middleware/auth'; import { authMiddleware } from '../middleware/auth';
import { generateToken } from '../utils/jwt'; import { generateToken } from '../utils/jwt';
@@ -7,6 +8,10 @@ import { generateToken } from '../utils/jwt';
const router = Router(); const router = Router();
const prisma = new PrismaClient(); const prisma = new PrismaClient();
function hashCardKey(raw: string): string {
return crypto.createHash('sha256').update(raw).digest('hex');
}
// ============================================ // ============================================
// VIP 卡密激活 // VIP 卡密激活
// ============================================ // ============================================
@@ -18,9 +23,10 @@ router.post('/activate-vip', authMiddleware, async (req: Request, res: Response)
try { try {
const body = activateSchema.parse(req.body); const body = activateSchema.parse(req.body);
// 查找卡密 // 查找卡密 (比对哈希)
const hashedKey = hashCardKey(body.cardKey);
const card = await prisma.vipCard.findUnique({ const card = await prisma.vipCard.findUnique({
where: { cardKey: body.cardKey }, where: { cardKey: hashedKey },
}); });
if (!card) { if (!card) {
@@ -97,6 +103,7 @@ router.post('/activate-vip', authMiddleware, async (req: Request, res: Response)
email: updatedUser.email, email: updatedUser.email,
isVip: true, isVip: true,
vipLevel: updatedUser.vipLevel, vipLevel: updatedUser.vipLevel,
tokenVersion: updatedUser.tokenVersion,
}); });
res.json({ res.json({

View File

@@ -12,6 +12,7 @@ export interface JwtPayload {
email: string; email: string;
isVip: boolean; isVip: boolean;
vipLevel: number; vipLevel: number;
tokenVersion: number;
} }
/** /**