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"
# JWT 密钥(修改为随机字符串)
JWT_SECRET="your-super-secret-jwt-key-change-this-in-production"
# JWT 密钥(务必修改为随机字符串)
JWT_SECRET="change-me-to-a-random-64-char-string"
# JWT 过期时间
JWT_EXPIRES_IN="7d"
# 数据加密密钥32字节修改)
# 数据加密密钥32字节务必修改)
ENCRYPTION_KEY="0123456789abcdef0123456789abcdef"
# 服务端口
@@ -16,5 +16,8 @@ PORT=3001
# 环境
NODE_ENV="development"
# VIP 卡密批次密钥(用于生成卡密签名)
BATCH_SECRET="your-batch-secret-key"
# CORS 允许的来源 (逗号分隔)
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

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

View File

@@ -4,15 +4,17 @@ import crypto from 'crypto';
const prisma = new PrismaClient();
/**
* 生成 VIP 卡密
* 生成 VIP 卡密 (原始 + 哈希)
*/
function generateCardKey(days: number): string {
function generateCardKey(days: number): { raw: string; hash: string } {
const prefix = `VIP${days}`;
const segments = [];
for (let i = 0; i < 4; i++) {
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() {
@@ -22,79 +24,59 @@ async function main() {
// 创建分类
// ============================================
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_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' },
];
for (const cat of categories) {
await prisma.category.create({
data: cat,
}).catch(() => {}); // 忽略重复错误
await prisma.category.create({ data: cat }).catch(() => {});
}
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 = [
{ type: 'MONTH', days: 30, count: 100 },
{ type: 'QUARTER', days: 90, count: 50 },
{ 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;
for (const config of cardConfigs) {
const cards = [];
console.log(`\n📦 ${config.type} 卡密 (${config.count} 张):`);
for (let i = 0; i < config.count; i++) {
cards.push({
cardKey: generateCardKey(config.days),
cardType: config.type,
days: config.days,
});
const { raw, hash } = generateCardKey(config.days);
cards.push({ cardKey: hash, cardType: config.type, days: config.days });
console.log(` ${raw}`);
}
await prisma.vipCard.createMany({ data: cards, skipDuplicates: true });
totalCards += config.count;
console.log(`✅ 生成 ${config.type} 卡密 ${config.count}`);
}
console.log(`\n🎉 种子数据初始化完成!`);
console.log(` ⚠️ 以上卡密仅显示一次,请妥善保存`);
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,
}));
// CORS
// CORS — 仅允许白名单来源
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',') || [
'http://localhost:5173',
'http://localhost:3000',
'app://.',
'file://',
];
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,
}));
@@ -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) => {
if (process.env.NODE_ENV !== 'development') {
return res.status(403).json({ success: false, message: '仅开发环境可用' });
const secret = process.env.ADMIN_SECRET;
if (!secret || req.headers['x-admin-secret'] !== secret) {
return res.status(403).json({ success: false, message: '禁止访问' });
}
try {
const { username, isVip } = req.body;

View File

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

View File

@@ -13,7 +13,10 @@ const prisma = new PrismaClient();
// ============================================
const registerSchema = z.object({
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(),
installId: z.string().optional(),
deviceHash: z.string().optional(),
@@ -22,34 +25,34 @@ const registerSchema = z.object({
router.post('/register', async (req: Request, res: Response) => {
try {
const body = registerSchema.parse(req.body);
// 检查用户名是否已存在
const existingUsername = await prisma.user.findUnique({
where: { username: body.username },
});
if (existingUsername) {
return res.status(400).json({
success: false,
message: '用户名已被使用',
});
}
// 检查邮箱是否已存在
const existingEmail = await prisma.user.findUnique({
where: { email: body.email },
});
if (existingEmail) {
return res.status(400).json({
success: false,
message: '邮箱已被注册',
});
}
// 密码哈希
const passwordHash = await bcrypt.hash(body.password, 10);
// 创建用户
const user = await prisma.user.create({
data: {
@@ -60,7 +63,7 @@ router.post('/register', async (req: Request, res: Response) => {
deviceHash: body.deviceHash,
},
});
// 生成 Token
const token = generateToken({
userId: user.id,
@@ -68,8 +71,9 @@ router.post('/register', async (req: Request, res: Response) => {
email: user.email,
isVip: user.isVip,
vipLevel: user.vipLevel,
tokenVersion: user.tokenVersion,
});
// 记录日志
await prisma.userLog.create({
data: {
@@ -80,7 +84,7 @@ router.post('/register', async (req: Request, res: Response) => {
ipAddress: req.ip,
},
});
res.json({
success: true,
token,
@@ -102,7 +106,7 @@ router.post('/register', async (req: Request, res: Response) => {
errors: error.errors,
});
}
console.error('Register error:', error);
res.status(500).json({ success: false, message: '注册失败' });
}
@@ -170,8 +174,9 @@ router.post('/login', async (req: Request, res: Response) => {
email: user.email,
isVip: user.isVip,
vipLevel: user.vipLevel,
tokenVersion: user.tokenVersion,
});
// 记录日志
await prisma.userLog.create({
data: {
@@ -182,7 +187,7 @@ router.post('/login', async (req: Request, res: Response) => {
ipAddress: req.ip,
},
});
res.json({
success: true,
token,
@@ -204,12 +209,28 @@ router.post('/login', async (req: Request, res: Response) => {
errors: error.errors,
});
}
console.error('Login error:', error);
res.status(500).json({ success: false, message: '登录失败' });
}
});
// ============================================
// 登出 (使当前 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 过期)
// ============================================

View File

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

View File

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