文件
shanghaipeiwan/server/tests/identity-hardening.spec.ts
xcbyy 2599dce315 feat(security): 三端闭环加固 + 微信提现打款/订阅消息落地
后端: 身份与归属鉴权收口; 资金安全(结算闸门/支付防篡改/报名费真实扣款/赠金/结算唯一入账/提现打款); 数据泄露与隐私; 登录与密钥安全(默认密钥闸门/登录限流); 可观测性; 启动自检.

微信: 商家转账到零钱提现打款闭环(含回调/查单/失败退回佣金); 订阅消息真实下发闭环与后台模板字段对齐.

后台: 财务/微信支付配置读取失败锁定; 看板/待办/走势去假数据; 打款与测试推送文案修正.

小程序: 请求层错误透传与登录过期提示; 多页失败反馈与乐观回滚; 帮助中心FAQ页面化.

测试: 新增 40+ 回归护栏与真实 MySQL 冒烟(75 套件/599 用例全绿).
2026-10-17 18:00:09 +08:00

153 行
7.4 KiB
TypeScript

// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
import { signUserToken, verifyUserToken } from '../src/core/security/user-token';
/**
* 第四批高危项回归闸门:客户端伪造身份的资金写接口收口 / 订单支付状态机收口 / 静默吞错可观测化
* 以「源码契约断言 + 行为断言」锁定边界,防止后续重构悄然后退。
*/
describe('第四批安全加固闸门 (用户会话身份 / 订单支付 / 上车申请越权 / 静默吞错)', () => {
const rootDir = path.resolve(__dirname, '../../');
const serverRepoPath = path.join(rootDir, 'server/src/db/repository.ts');
const serverIndexPath = path.join(rootDir, 'server/src/index.ts');
const userTokenPath = path.join(rootDir, 'server/src/core/security/user-token.ts');
const requestPath = path.join(rootDir, 'pwuniapp/common/request.js');
const loginPath = path.join(rootDir, 'pwuniapp/pages/auth/login.vue');
const indexCode = fs.readFileSync(serverIndexPath, 'utf-8');
const repoCode = fs.readFileSync(serverRepoPath, 'utf-8');
const tokenCode = fs.readFileSync(userTokenPath, 'utf-8');
describe('1. 用户会话 token 基础设施 (HMAC 签名,防止伪造他人 userId)', () => {
it('必须提供签名/验签与安全取用用户身份的能力', () => {
expect(indexCode).toContain('const USER_TOKEN_SECRET');
expect(indexCode).toContain('function signUserToken(');
expect(indexCode).toContain('function verifyUserToken(');
expect(indexCode).toContain('function resolveSecureUserId(');
expect(indexCode).toContain('function resolveRequestUserId(');
});
it('token 验签必须使用恒定时间比较且校验有效期', () => {
expect(tokenCode).toContain('crypto.timingSafeEqual');
expect(tokenCode).toContain('ttlMs');
expect(indexCode).toContain('USER_TOKEN_TTL_MS');
});
it('签名 token 可被正确验签并解析出用户ID', () => {
const SECRET = 'unit-test-secret';
const TTL = 7 * 24 * 3600 * 1000;
const token = signUserToken(8888, SECRET, TTL);
expect(verifyUserToken(token, SECRET, TTL)).toBe(8888);
});
it('篡改 payload/签名或使用错误密钥的 token 必须验签失败', () => {
const SECRET = 'unit-test-secret';
const TTL = 7 * 24 * 3600 * 1000;
const token = signUserToken(1001, SECRET, TTL);
const [payload, sig] = token.split('.');
const forgedPayload = Buffer.from(JSON.stringify({ uid: 1, ts: Date.now() })).toString('base64url');
expect(verifyUserToken(`${forgedPayload}.${sig}`, SECRET, TTL)).toBeNull();
expect(verifyUserToken(`${payload}.deadbeef`, SECRET, TTL)).toBeNull();
expect(verifyUserToken(token, 'wrong-secret', TTL)).toBeNull();
expect(verifyUserToken('not-a-token', SECRET, TTL)).toBeNull();
});
it('过期 token 必须验签失败', () => {
const SECRET = 'unit-test-secret';
const TTL = 1000;
const issuedAt = Date.now() - 10_000;
const token = signUserToken(2002, SECRET, TTL, issuedAt);
expect(verifyUserToken(token, SECRET, TTL, issuedAt + 500)).toBe(2002);
expect(verifyUserToken(token, SECRET, TTL, issuedAt + 10_000)).toBeNull();
});
it('生产环境缺失有效 token 时必须拒绝敏感操作 (fail closed)', () => {
const start = indexCode.indexOf('function resolveSecureUserId(');
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 900);
expect(block).toContain('IS_PRODUCTION');
expect(block).toContain('401');
expect(block).toContain('无权以他人身份');
});
it('微信手机号登录必须签发真实签名 token,杜绝假 token', () => {
expect(indexCode).toContain('token: signUserToken(user.id)');
expect(indexCode).toContain('userToken: signUserToken(user.id)');
expect(indexCode).not.toContain('token: `token_${user.id}_${Date.now()}`');
});
});
describe('2. 订单支付状态机收口', () => {
it('非 LIVE 沙箱入账必须受 ALLOW_SANDBOX_PAY 生产闸门约束', () => {
const start = indexCode.indexOf("app.post('/api/orders/:id/pay'");
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 3400);
expect(block).toContain('if (!ALLOW_SANDBOX_PAY)');
expect(block).toContain('禁止直接变更订单支付状态');
});
it('沙箱仅允许推进到合法已支付状态,严禁客户端任意指定终态', () => {
const start = indexCode.indexOf("app.post('/api/orders/:id/pay'");
const block = indexCode.slice(start, start + 3400);
expect(block).toContain("const allowedSandboxStatuses = ['履约中', '待接单']");
expect(block).toContain('allowedSandboxStatuses.includes(requested)');
});
});
describe('3. 提现接口以服务端会话身份为准', () => {
it('提现用户身份必须经 resolveSecureUserId 校验,禁止直接信任路径参数', () => {
const start = indexCode.indexOf("app.post('/api/users/:id/withdraw'");
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 900);
expect(block).toContain("resolveSecureUserId(req, pathUserId, '申请提现')");
expect(block).not.toContain('const userId = Number(req.params.id);');
});
});
describe('4. 上车申请仅动态发布者本人可处置', () => {
it('处置路由必须校验申请归属并区分管理员', () => {
const start = indexCode.indexOf("app.post('/api/post-applies/:id/handle'");
expect(start).toBeGreaterThan(-1);
const block = indexCode.slice(start, start + 1500);
expect(block).toContain('repo.getPostApplyById(applyId)');
expect(block).toContain('isVerifiedAdminRequest(req)');
expect(block).toContain('仅动态发布者本人可处置该上车申请');
expect(block).toContain('SELECT user_id FROM posts WHERE id = ?');
});
it('handlePostApply 必须仅允许待处理申请被处置一次', () => {
const start = repoCode.indexOf('async handlePostApply(');
expect(start).toBeGreaterThan(-1);
const block = repoCode.slice(start, start + 900);
expect(block).toContain("WHERE id = ? AND status = 'PENDING'");
expect(block).toContain('已处置');
});
});
describe('5. 资金/结算静默吞错可观测化', () => {
it('服务端必须提供统一错误日志并用于支付与结算关键路径', () => {
expect(indexCode).toContain('function logServerError(');
expect(indexCode).toContain("logServerError('confirmWechatPayment.query'");
expect(indexCode).toContain("logServerError('resolveWechatPayRuntimeConfig'");
expect(repoCode).toContain('function logServerError(');
expect(repoCode).toContain("logServerError('handleArbitration.settle'");
});
});
describe('6. 小程序端会话 token 存储与透传', () => {
it('统一请求层必须携带 Authorization Bearer 用户会话 token', () => {
const code = fs.readFileSync(requestPath, 'utf-8');
expect(code).toContain("uni.getStorageSync('userToken')");
expect(code).toContain('Authorization: `Bearer ${sessionToken}`');
});
it('微信手机号登录成功后必须落地 userToken', () => {
const code = fs.readFileSync(loginPath, 'utf-8');
expect(code).toContain("res.userToken || res.token");
expect(code).toContain("uni.setStorageSync('userToken', sessionToken)");
});
});
});