- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
308 行
9.7 KiB
TypeScript
308 行
9.7 KiB
TypeScript
import {
|
||
NfcAntiTamperService,
|
||
NfcSecurityAuditLog,
|
||
} from '../../../src/modules/nfc/service/anti-tamper';
|
||
|
||
describe('Point 15: 实体 NFC 卡片动态防伪验签算法 (anti-tamper)', () => {
|
||
let service: NfcAntiTamperService;
|
||
const mockUid = '04A1B2C3D4E5F6';
|
||
const mockCardId = 1001;
|
||
const mockSecret = 'k9#mZ$8vP!xL2@qR5*wY8^uN1(bC4)eT';
|
||
|
||
beforeEach(() => {
|
||
service = new NfcAntiTamperService();
|
||
});
|
||
|
||
describe('1. generateDeviceToken (生成动态防伪验签令牌)', () => {
|
||
it('应成功生成包含时间戳与 HMAC-SHA256 签名的动态 Token', () => {
|
||
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||
expect(typeof token).toBe('string');
|
||
expect(token).toContain('.');
|
||
|
||
const [tsStr, signature] = token.split('.');
|
||
const ts = Number(tsStr);
|
||
expect(Number.isSafeInteger(ts)).toBe(true);
|
||
expect(Math.abs(Date.now() - ts)).toBeLessThan(2000);
|
||
expect(signature).toHaveLength(64); // SHA-256 16进制长度为 64
|
||
expect(/^[0-9a-f]{64}$/i.test(signature)).toBe(true);
|
||
});
|
||
|
||
it('支持显式指定 timestamp 生成 Token', () => {
|
||
const customTs = 1710000000000;
|
||
const token = service.generateDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
mockSecret,
|
||
customTs
|
||
);
|
||
const [tsStr] = token.split('.');
|
||
expect(Number(tsStr)).toBe(customTs);
|
||
});
|
||
|
||
it('入参缺失时应抛出明确的异常保护', () => {
|
||
expect(() =>
|
||
service.generateDeviceToken('', mockCardId, mockSecret)
|
||
).toThrow('缺少必要参数');
|
||
expect(() =>
|
||
service.generateDeviceToken(mockUid, 0, mockSecret)
|
||
).toThrow('缺少必要参数');
|
||
expect(() =>
|
||
service.generateDeviceToken(mockUid, -1, mockSecret)
|
||
).toThrow('缺少必要参数');
|
||
expect(() =>
|
||
service.generateDeviceToken(mockUid, mockCardId, '')
|
||
).toThrow('缺少必要参数');
|
||
});
|
||
});
|
||
|
||
describe('2. verifyDeviceToken - 合法令牌验证', () => {
|
||
it('合法未过期的令牌应验证通过', () => {
|
||
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||
const result = service.verifyDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
token,
|
||
mockSecret
|
||
);
|
||
expect(result.valid).toBe(true);
|
||
expect(result.reason).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe('3. verifyDeviceToken - 过期超时拦截', () => {
|
||
it('超过默认 5 分钟 (300,000ms) 的令牌应被拦截', () => {
|
||
const expiredTs = Date.now() - 5 * 60 * 1000 - 1000; // 5分1秒前
|
||
const token = service.generateDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
mockSecret,
|
||
expiredTs
|
||
);
|
||
|
||
const result = service.verifyDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
token,
|
||
mockSecret
|
||
);
|
||
expect(result.valid).toBe(false);
|
||
expect(result.reason).toMatch(/过期|expired/i);
|
||
});
|
||
|
||
it('支持自定义 maxAgeMs 过期时间阈值', () => {
|
||
const pastTs = Date.now() - 10 * 1000; // 10秒前
|
||
const token = service.generateDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
mockSecret,
|
||
pastTs
|
||
);
|
||
|
||
// 阈值 5 秒 -> 应当判定为过期
|
||
const resExpired = service.verifyDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
token,
|
||
mockSecret,
|
||
5000
|
||
);
|
||
expect(resExpired.valid).toBe(false);
|
||
expect(resExpired.reason).toMatch(/过期|expired/i);
|
||
|
||
// 阈值 20 秒 -> 应当判定为合法
|
||
const resValid = service.verifyDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
token,
|
||
mockSecret,
|
||
20000
|
||
);
|
||
expect(resValid.valid).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('4. verifyDeviceToken - 恶意参数篡改拦截', () => {
|
||
it('篡改 UID 时应拦截', () => {
|
||
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||
const tamperedUid = '04A1B2C3D4E5F9';
|
||
const result = service.verifyDeviceToken(
|
||
tamperedUid,
|
||
mockCardId,
|
||
token,
|
||
mockSecret
|
||
);
|
||
expect(result.valid).toBe(false);
|
||
expect(result.reason).toMatch(/签名无效|篡改|invalid signature/i);
|
||
});
|
||
|
||
it('篡改 cardId 时应拦截', () => {
|
||
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||
const tamperedCardId = 9999;
|
||
const result = service.verifyDeviceToken(
|
||
mockUid,
|
||
tamperedCardId,
|
||
token,
|
||
mockSecret
|
||
);
|
||
expect(result.valid).toBe(false);
|
||
expect(result.reason).toMatch(/签名无效|篡改|invalid signature/i);
|
||
});
|
||
|
||
it('篡改签名内容时应拦截', () => {
|
||
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||
const [ts, sig] = token.split('.');
|
||
const tamperedSig = sig.substring(0, sig.length - 1) + (sig.endsWith('0') ? '1' : '0');
|
||
const tamperedToken = `${ts}.${tamperedSig}`;
|
||
|
||
const result = service.verifyDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
tamperedToken,
|
||
mockSecret
|
||
);
|
||
expect(result.valid).toBe(false);
|
||
expect(result.reason).toMatch(/签名无效|篡改|invalid signature/i);
|
||
});
|
||
|
||
it('使用错误的密钥验证时应拦截', () => {
|
||
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||
const wrongSecret = 'different_secret_key_1234567890';
|
||
const result = service.verifyDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
token,
|
||
wrongSecret
|
||
);
|
||
expect(result.valid).toBe(false);
|
||
expect(result.reason).toMatch(/签名无效|篡改|invalid signature/i);
|
||
});
|
||
});
|
||
|
||
describe('5. verifyDeviceToken - 空参与畸形输入保护', () => {
|
||
it('传入空参数时不应崩溃,应安全返回无效原因', () => {
|
||
const validToken = service.generateDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
mockSecret
|
||
);
|
||
|
||
expect(
|
||
service.verifyDeviceToken('', mockCardId, validToken, mockSecret).valid
|
||
).toBe(false);
|
||
expect(
|
||
service.verifyDeviceToken(mockUid, 0, validToken, mockSecret).valid
|
||
).toBe(false);
|
||
expect(
|
||
service.verifyDeviceToken(mockUid, -10, validToken, mockSecret).valid
|
||
).toBe(false);
|
||
expect(
|
||
service.verifyDeviceToken(mockUid, mockCardId, '', mockSecret).valid
|
||
).toBe(false);
|
||
expect(
|
||
service.verifyDeviceToken(mockUid, mockCardId, validToken, '').valid
|
||
).toBe(false);
|
||
});
|
||
|
||
it('传入畸形或格式错误的 Token 应安全拦截', () => {
|
||
expect(
|
||
service.verifyDeviceToken(mockUid, mockCardId, 'malformedToken', mockSecret).valid
|
||
).toBe(false);
|
||
expect(
|
||
service.verifyDeviceToken(mockUid, mockCardId, 'abc.def', mockSecret).valid
|
||
).toBe(false);
|
||
expect(
|
||
service.verifyDeviceToken(mockUid, mockCardId, `${Date.now()}.short`, mockSecret).valid
|
||
).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('6. verifyDeviceToken - 时钟容差保护', () => {
|
||
it('时间稍微超前的令牌(在容差范围内)应被允许', () => {
|
||
const futureTs = Date.now() + 30 * 1000; // 30秒未来时间(网络或机器微小偏差)
|
||
const token = service.generateDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
mockSecret,
|
||
futureTs
|
||
);
|
||
|
||
const result = service.verifyDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
token,
|
||
mockSecret
|
||
);
|
||
expect(result.valid).toBe(true);
|
||
});
|
||
|
||
it('异常超前(超过容差阈值)的未来令牌应被拒绝', () => {
|
||
const futureTs = Date.now() + 10 * 60 * 1000; // 10分钟后
|
||
const token = service.generateDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
mockSecret,
|
||
futureTs
|
||
);
|
||
|
||
const result = service.verifyDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
token,
|
||
mockSecret
|
||
);
|
||
expect(result.valid).toBe(false);
|
||
expect(result.reason).toMatch(/未来|时钟偏差|future/i);
|
||
});
|
||
});
|
||
|
||
describe('7. verifyDeviceToken - 防重放保护 (Replay Attack)', () => {
|
||
it('同一合法令牌不可重复验签使用', () => {
|
||
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||
|
||
// 首次验签通过
|
||
const firstResult = service.verifyDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
token,
|
||
mockSecret
|
||
);
|
||
expect(firstResult.valid).toBe(true);
|
||
|
||
// 再次使用同一令牌验签,触发防重放拦截
|
||
const secondResult = service.verifyDeviceToken(
|
||
mockUid,
|
||
mockCardId,
|
||
token,
|
||
mockSecret
|
||
);
|
||
expect(secondResult.valid).toBe(false);
|
||
expect(secondResult.reason).toMatch(/重放|已使用|replay/i);
|
||
});
|
||
});
|
||
|
||
describe('8. 安全审计日志留痕', () => {
|
||
it('拦截到恶意篡改、重放或非法请求时,应记录审计日志', () => {
|
||
service.clearAuditLogs();
|
||
|
||
// 1. 触发一次篡改
|
||
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||
service.verifyDeviceToken(mockUid, 9999, token, mockSecret);
|
||
|
||
// 2. 触发一次重放
|
||
const validToken = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
|
||
service.verifyDeviceToken(mockUid, mockCardId, validToken, mockSecret);
|
||
service.verifyDeviceToken(mockUid, mockCardId, validToken, mockSecret);
|
||
|
||
const logs: NfcSecurityAuditLog[] = service.getAuditLogs();
|
||
expect(logs.length).toBeGreaterThanOrEqual(2);
|
||
|
||
const tamperLog = logs.find(l => l.cardId === 9999);
|
||
expect(tamperLog).toBeDefined();
|
||
expect(tamperLog?.reason).toMatch(/签名无效|篡改/);
|
||
|
||
const replayLog = logs.find(l => l.token === validToken && /重放|已使用/.test(l.reason));
|
||
expect(replayLog).toBeDefined();
|
||
});
|
||
});
|
||
});
|