JS 逆向入门指南

1. 逆向的核心思路(#1-%E9%80%86%E5%90%91%E7%9A%84%E6%A0%B8%E5%BF%83%E6%80%9D%E8%B7%AF) 2. 第一步:抓包定位目标参数(#2-%E7%AC%AC%E4%B8%80%E6%AD%A5%EF%BC%9A%E6%8A%93%E5%8C%85%E5%AE%9A%E4%BD%8D%E7%9B%AE%E6%A0%87%E5%8F%82%E6%95%B0) 3. 第二步:找到签名来源(断点 + Hook)(#3-%E7%AC%AC%E4%BA%8C%E6%AD%A5%EF%BC%9A%E6%89%B

分享

官方文档:https://developer.chrome.com/docs/devtools/

以抖音 a_bogus 逆向为实战案例,从零讲解思路、工具与 Hook 片段


目录

  1. 逆向的核心思路
  2. 第一步:抓包定位目标参数
  3. 第二步:找到签名来源(断点 + Hook)
  4. 第三步:在浏览器环境里验证
  5. 第四步:识别加密算法
  6. 第五步:在 Node.js 中还原签名
  7. 第六步:插桩(Instrumentation)深入分析
  8. 常见坑与解决方案
  9. 常用 Hook 片段速查
  10. 工具链推荐

1. 逆向的核心思路

逆向不是破解,是追踪数据流

浏览器发出一个神秘参数
       ↓
这个参数从哪来?(定位生成点)
       ↓
生成它需要哪些输入?(追踪输入)
       ↓
中间做了什么运算?(识别算法)
       ↓
用 Python/Node.js 复现同样的运算

黄金原则

  • 先观察,再动手。不要一上来就扣代码,先用 DevTools 观察几个请求,感受参数的规律。
  • 缩小范围。JS 文件往往几十万行,不要试图全部读懂,只找签名相关的那一小块。
  • 验证要及时。每往前推进一步就立刻验证,避免在错误路径上走太远。

2. 第一步:抓包定位目标参数

2.1 找到含签名的请求

打开 Chrome DevTools → Network 面板,过滤关键词:

a_bogus        # 抖音签名参数
sign           # 通用签名
X-Bogus
token

点击那个请求,查看:

  • Headers → 有没有自定义请求头(X-Signx-secsdk-web-signature 等)
  • URL → 查询参数里有没有签名字段(a_bogus=...

2.2 观察参数规律

多刷新几次,对比多个请求的签名值:

  • 每次都不一样?→ 含时间戳或计数器
  • 同一个 URL 每次相同?→ 纯哈希,可离线复现
  • 长度固定?→ 猜算法(32字节=MD5/SM3,64字节=SHA-512...)

3. 第二步:找到签名来源(断点 + Hook)

3.1 最简单的方法:XHR/Fetch 断点

DevTools → Sources → 右侧 "XHR/fetch Breakpoints" → 添加 URL 关键词

/aweme/v1/     ← 抖音 API 路径

刷新页面,断点触发后查看调用栈,往上翻就能找到签名函数。

3.2 Hook URLSearchParams.append

很多签名是最后通过 URLSearchParams 拼到 URL 上的。在 DevTools Console 运行:

// 在 Console 里运行,拦截所有 append 调用
const _orig = URLSearchParams.prototype.append;
URLSearchParams.prototype.append = function(key, value) {
  if (key === 'a_bogus' || key === 'sign') {
    console.log('[Hook] append:', key, '=', value);
    console.trace();  // 打印调用栈!
  }
  return _orig.call(this, key, value);
};

注意:如果 SDK 在页面加载时就 hook 了原型,Console 里注入已经太晚了。
需要用 initScript(见下面)。

3.3 Hook fetch / XMLHttpRequest

// 拦截 fetch,查看请求 URL
const _fetch = window.fetch;
window.fetch = function(...args) {
  const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
  if (url?.includes('aweme')) {
    console.log('[Hook] fetch:', url);
    console.trace();
  }
  return _fetch.apply(this, args);
};

// 拦截 XHR
const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
  if (String(url).includes('aweme')) {
    console.log('[Hook] XHR.open:', url);
    console.trace();
  }
  return _open.apply(this, arguments);
};

3.4 initScript:在所有脚本之前注入(最强)

适用于 SDK 在页面加载时就读取原型引用的情况。

使用 Chrome DevTools Protocol (CDP) 或 Playwright 的 addInitScript:

// Playwright:
await page.addInitScript(() => {
  const _orig = URLSearchParams.prototype.append;
  URLSearchParams.prototype.append = function(key, value) {
    if (key === 'a_bogus') {
      window.__captured_bogus = value;
      console.log('[initScript] a_bogus:', value);
    }
    return _orig.call(this, key, value);
  };
});

或用 Chrome DevTools MCP / Puppeteer:

// 在页面加载前通过 CDP 注入
await session.send('Page.addScriptToEvaluateOnNewDocument', {
  source: `
    const _orig = URLSearchParams.prototype.append;
    URLSearchParams.prototype.append = function(k, v) {
      if (k === 'a_bogus') window.__ab = v;
      return _orig.call(this, k, v);
    };
  `
});

4. 第三步:在浏览器环境里验证

找到签名函数后,先在 DevTools Console 里手动调用它,确认能复现签名,再考虑搬到 Node.js。

// 假设找到了签名函数叫 bdms.sign
bdms.sign('https://www.douyin.com/aweme/v1/web/social/count?aid=6383')
// 看看能不能返回一个 a_bogus 值

如果函数依赖 this 或闭包变量,在 Console 里直接调用可能报错,这很正常,继续往调用栈上层找包装函数。


5. 第四步:识别加密算法

5.1 搜索魔法常量

每种哈希/加密算法都有固定的初始化常量,这些常量不会被混淆。

MD5:    0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476
SHA-1:  0x67452301, 0xEFCDABCD, 0x98BADCFE
SHA-256: 0x6a09e667, 0xbb67ae85, 0x3c6ef372
SM3:    0x7380166f, 0x4914b2b9  ← 中国国密算法
AES:    0x63, 0x7c, 0x77, 0x7b (S-box 前几个值)

在 DevTools 搜索(Ctrl+Shift+F):

2043430169    ← SM3 的轮常量 Tj (十进制)
1937774191    ← SM3 的 IV[0] (十进制)

5.2 看函数名残留

混淆工具有时保留部分函数名,搜索:

SM3, sm3, Sm3
FF, GG           ← SM3 的轮函数名
SHA, sha
MD5, md5
AES, aes
HMAC
pbkdf2, bcrypt

5.3 看输出长度

长度 可能算法
32 hex 字符 (16B) MD5
40 hex 字符 (20B) SHA-1
64 hex 字符 (32B) SHA-256 / SM3
128 hex 字符 (64B) SHA-512

5.4 用 Python 快速验证猜测

import hashlib, hmac

data = b"your_input_here"

# 测试各种算法
print("MD5:    ", hashlib.md5(data).hexdigest())
print("SHA1:   ", hashlib.sha1(data).hexdigest())
print("SHA256: ", hashlib.sha256(data).hexdigest())

# SM3 (需要 gmssl 库: pip install gmssl)
from gmssl import sm3, func
print("SM3:    ", sm3.sm3_hash(func.bytes_to_list(data)))

对比浏览器里捕获到的哈希值,看哪个匹配。


6. 第五步:在 Node.js 中还原签名

6.1 最简单的方式:直接扣 SDK 在 Node.js 里跑

如果 SDK 是纯 JS(不依赖 DOM),可以直接用 Node.js 运行:

// run_sdk.mjs
import { readFileSync } from 'fs';

// 加载 SDK 代码
const sdk_code = readFileSync('./lib/some_sdk.js', 'utf-8');

// 创建沙箱全局对象
const globalObj = {
  window: {},
  document: { createElement: () => ({}) },
  navigator: { userAgent: 'Mozilla/5.0...' },
  location: { href: 'https://www.example.com/' },
};

// 用 Function 构造器在沙箱里执行
const fn = new Function('window', 'document', 'navigator', 'location',
  sdk_code + '\n return typeof sign !== "undefined" ? sign : typeof module !== "undefined" ? module.exports : null;'
);
const sign = fn(globalObj.window, globalObj.document, globalObj.navigator, globalObj.location);

6.2 用 jsdom 模拟完整浏览器环境

适用于 SDK 依赖 DOM API 的情况:

import { JSDOM } from 'jsdom';
import { readFileSync } from 'fs';

const src = readFileSync('./lib/sdk.js', 'utf-8');

const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
  url: 'https://www.example.com/',   // 必须设置,SDK 会读取 location
  pretendToBeVisual: true,           // 重要!防止 canvas 相关代码崩溃
  runScripts: 'dangerously',         // 允许执行脚本
});

const win = dom.window;

// 在加载 SDK 之前安装 Mock
class MockXHR {
  open(method, url) { this._url = url; }
  send() {}
  setRequestHeader() {}
  addEventListener() {}
  removeEventListener() {}
}
win.XMLHttpRequest = MockXHR;

// 加载 SDK
const scr = win.document.createElement('script');
scr.textContent = src;
win.document.head.appendChild(scr);

// 调用 SDK 的初始化函数
win.bdms.init({ aid: 6383, paths: ['^/aweme/v1/'] });

// Hook URLSearchParams 捕获签名(必须在 init 之前,或用 initScript)
const origAppend = win.URLSearchParams.prototype.append;
win.URLSearchParams.prototype.append = function(k, v) {
  if (k === 'a_bogus') {
    console.log('Captured a_bogus:', v);
  }
  return origAppend.call(this, k, v);
};

6.3 封装为 HTTP 服务(Python 调用)

Node.js 启动一个本地 HTTP 服务,Python 调用它:

// sign_server.mjs
import http from 'http';
// ... 初始化 SDK(同上)...

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, 'http://localhost');
  const target = url.searchParams.get('url');

  const a_bogus = await getSignature(target);
  res.end(JSON.stringify({ a_bogus }));
});

server.listen(8888, () => {
  // Python 端监听这行输出来判断服务已就绪
  process.stdout.write(JSON.stringify({ status: 'ready' }) + '\n');
});
# Python 端
import subprocess, requests, json, time

class Signer:
    def __init__(self, port=8888):
        self._proc = subprocess.Popen(
            ['node', 'sign_server.mjs'],
            stdout=subprocess.PIPE, text=True
        )
        # 等待服务就绪
        for _ in range(30):
            line = self._proc.stdout.readline()
            if '"ready"' in line:
                break
            time.sleep(0.2)
        self._port = port

    def sign(self, url):
        r = requests.get(f'http://localhost:{self._port}/sign',
                         params={'url': url}, timeout=5)
        return r.json()['a_bogus']

    def close(self):
        self._proc.terminate()

    def __enter__(self): return self
    def __exit__(self, *_): self.close()

# 使用
with Signer() as s:
    token = s.sign('https://www.douyin.com/aweme/v1/...')

7. 第六步:插桩(Instrumentation)深入分析

当算法藏在 VM 里看不懂时,不必读懂 VM,只需在关键函数的入口/出口插入日志

7.1 源码字符串替换插桩

最稳定的方法:加载 SDK 前,用字符串替换在源码里直接插入日志。

let src = readFileSync('./lib/sdk.js', 'utf-8');

// 目标:在 someFunc(t) 的开头插入日志
// 原代码片段:
//   function someFunc(t){var r=...
// 替换为:
//   function someFunc(t){globalThis.__logs.push({fn:'someFunc',in:t});var r=...

src = src.replace(
  'function someFunc(t){var r=',
  'function someFunc(t){if(globalThis.__logs)globalThis.__logs.push({fn:"someFunc",in:typeof t==="string"?t:Array.from(t)});var r='
);

关键技巧

  • globalThis.__logs 而不是局部变量,避免变量名冲突
  • if(globalThis.__logs) 做开关,只在需要时开启
  • 对 byte array 用 Array.from(t) 转换,方便打印
  • 替换的代码片段要足够独特,不能在文件里有多处匹配

7.2 验证替换成功

// 检查替换是否生效
if (!src.includes('globalThis.__logs')) {
  console.error('替换失败!原始代码片段未找到');
  process.exit(1);
}
console.log('替换成功,共', (src.match(/globalThis\.__logs/g) || []).length, '处');

7.3 控制日志开关

win.__logs = null;  // 默认关闭

// 需要分析时打开
win.__logs = [];
// ... 触发目标操作 ...
const log = [...win.__logs];
win.__logs = null;  // 关闭

// 分析日志
log.forEach((entry, i) => {
  console.log(`[${i}] ${entry.fn}: input=`, entry.in);
});

7.4 完整的哈希函数插桩示例

以抖音 SM3 分析为例:

// 插桩 write() 入口,记录所有输入
src = src.replace(
  'key:"write",value:function(t){var r="string"==typeof t?',
  'key:"write",value:function(t){' +
  'if(globalThis.__sm3Calls)globalThis.__sm3Calls.push({op:"W",d:typeof t==="string"?t:Array.prototype.slice.call(t)});' +
  'var r="string"==typeof t?'
);

// 插桩 sum() 输出,记录哈希值
src = src.replace(
  'return this.reset(),u}},{key:"_compress"',
  'if(globalThis.__sm3Calls)globalThis.__sm3Calls.push({op:"S",d:u});return this.reset(),u}},{key:"_compress"'
);

8. 常见坑与解决方案

坑 1:Hook 时机太晚

现象:在 Console 里 hook 了 URLSearchParams.prototype.append,但签名时没有触发。

原因:SDK 在初始化时就把原型方法的引用存到了内部变量里,之后的修改不影响它。

// SDK 内部(简化):
var _append = URLSearchParams.prototype.append;  // 启动时存了引用
// ... 之后调用 _append(key, value)  ← 不受后来的 hook 影响

解决:使用 initScript(在 SDK 加载前注入),或用 Playwright/Puppeteer 的 addInitScript


坑 2:Hook 内部抛异常破坏 VM

现象:某些 hook 注入后,SDK 的 VM 报各种奇怪错误(TypeError: undefined is not a function 等)。

原因:VM 有 try/catch,hook 抛出的错误被 VM 捕获,导致 VM 状态错乱。

错误做法

// 在原生方法上 hook 并抛出 Error 获取堆栈——会破坏 VM
Number.prototype.toString = function() {
  throw new Error('trace');  // ← 危险!VM 内部大量调用 toString
};

正确做法:hook 里只做记录,不抛异常,调用原始方法后正常返回。


坑 3:测试 URL 不匹配路径过滤规则

现象:签名函数调用了,但 a_bogus 就是没出现。

原因:SDK 只对特定路径签名,测试 URL 不在白名单里。

// 抖音 bdms 只签名这些路径:
paths: ['^/aweme/v1/', '^/aweme/v2/', '^/webcast/']

// 用 http://test.com/api/xxx 当然不会签名!
// 必须用真实的 API URL

解决:在 Network 面板找一个真实请求的 URL 来测试。


坑 4:jsdom 缺少 canvas 导致崩溃

现象:jsdom 里运行 SDK 时报 TypeError: Cannot read properties of undefined (reading 'getContext')

解决pretendToBeVisual: true 让 jsdom 声称自己支持 canvas,SDK 一般会降级处理。

const dom = new JSDOM('...', {
  pretendToBeVisual: true,  // ← 加上这行
  runScripts: 'dangerously',
});

坑 5:Node.js 进程挂起不退出

现象:脚本执行完了,但进程一直不退出。

原因:jsdom 内部有 setTimeout/setInterval 保持了事件循环。

解决:在所有输出打印完后强制退出:

// 放在文件最末尾
process.exit(0);

坑 6:Windows 终端中文乱码

现象:Python 脚本输出中文时报 UnicodeEncodeError

解决

import sys, io
# 强制使用 UTF-8 输出
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

或在运行脚本前设置环境变量:

set PYTHONIOENCODING=utf-8

9. 常用 Hook 片段速查

URLSearchParams 签名捕获

// 适合 initScript 注入
(function() {
  const _orig = URLSearchParams.prototype.append;
  URLSearchParams.prototype.append = function(key, value) {
    if (['a_bogus', 'sign', 'signature'].includes(key)) {
      window.__signatures = window.__signatures || {};
      window.__signatures[key] = value;
      // 可选:通知外部代码
      window.dispatchEvent(new CustomEvent('sign-captured', { detail: { key, value } }));
    }
    return _orig.call(this, key, value);
  };
})();

fetch 请求拦截

(function() {
  const _fetch = window.fetch;
  window.fetch = async function(input, init) {
    const url = typeof input === 'string' ? input : input.url;
    console.log('[fetch]', url?.substring(0, 100));
    const resp = await _fetch.apply(this, arguments);
    return resp;
  };
})();

XHR 全拦截

(function() {
  const _open = XMLHttpRequest.prototype.open;
  const _send = XMLHttpRequest.prototype.send;
  const _setHeader = XMLHttpRequest.prototype.setRequestHeader;

  XMLHttpRequest.prototype.open = function(method, url) {
    this.__url = url;
    return _open.apply(this, arguments);
  };

  XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
    console.log('[XHR header]', name, ':', value?.substring(0, 60));
    return _setHeader.apply(this, arguments);
  };

  XMLHttpRequest.prototype.send = function(body) {
    console.log('[XHR send]', this.__url?.substring(0, 100));
    return _send.apply(this, arguments);
  };
})();

函数调用追踪(不破坏原函数)

function wrapFn(obj, fnName, label) {
  const orig = obj[fnName];
  obj[fnName] = function() {
    const args = Array.from(arguments).map(a =>
      typeof a === 'string' ? a.substring(0, 60) : typeof a
    );
    console.log(`[${label}] ${fnName}(`, args.join(', '), ')');
    const result = orig.apply(this, arguments);
    console.log(`[${label}] ${fnName} => `, String(result).substring(0, 60));
    return result;
  };
}

// 使用
wrapFn(window.crypto.subtle, 'digest', 'Crypto');
wrapFn(window, 'btoa', 'Encode');

localStorage/sessionStorage 监控

(function() {
  ['localStorage', 'sessionStorage'].forEach(storeName => {
    const store = window[storeName];
    const _setItem = store.setItem.bind(store);
    const _getItem = store.getItem.bind(store);

    Object.defineProperty(window, storeName, {
      get() {
        return new Proxy(store, {
          get(target, prop) {
            if (prop === 'setItem') return function(k, v) {
              console.log(`[${storeName}] set "${k}" =`, String(v).substring(0, 80));
              return _setItem(k, v);
            };
            return typeof target[prop] === 'function'
              ? target[prop].bind(target) : target[prop];
          }
        });
      }
    });
  });
})();
(function() {
  let _cookie = document.cookie;
  Object.defineProperty(document, 'cookie', {
    get() { return _cookie; },
    set(val) {
      console.log('[cookie set]', val.substring(0, 80));
      _cookie = val;
    }
  });
})();

10. 工具链推荐

必装

工具 用途
Chrome DevTools 抓包、断点、Console 注入
Node.js (v18+) 运行 JS SDK
jsdom npm 包 模拟浏览器 DOM 环境
gmssl pip 包 Python 里验证 SM3
requests pip 包 Python HTTP 请求
npm install jsdom          # Node.js 浏览器模拟
pip install gmssl requests # Python 工具

进阶

工具 用途
Playwright 带真实浏览器的自动化,支持 initScript
Fiddler / mitmproxy HTTPS 流量解密抓包
AST Explorer 分析混淆 JS 的语法树
de4js.com 在线 JS 反混淆
CyberChef 在线算法验证工具箱

DevTools 快捷键

Ctrl+Shift+F    全局搜索(所有 JS 文件中搜索)
Ctrl+P          按文件名搜索
{}              格式化压缩代码(点击底部花括号图标)
F8              继续执行(断点中)
F10             单步跳过
F11             单步进入
Ctrl+Shift+E    查看 Event Listener

实战流程回顾:抖音 a_bogus

1. Network 找到含 a_bogus 的请求
         ↓
2. 全局搜索 "a_bogus" → 找到 bdms SDK 加载位置
         ↓
3. initScript hook URLSearchParams.append → 确认 a_bogus 从 bdms 产生
         ↓
4. 下载 bdms JS 文件,搜索常量 2043430169 → 发现 SM3 实现
         ↓
5. 在 jsdom 中加载 bdms,Mock XHR,成功生成 a_bogus
         ↓
6. 源码插桩 SM3 的 write/sum 方法 → 记录完整 5 步哈希链
         ↓
7. Python gmssl 验证每一步哈希值完全一致
         ↓
8. 封装 Node.js HTTP 服务 + Python ABogus 类,完成集成

记住:逆向的本质是调试和观察,而不是猜测。
每一步都要有验证,不要在没有证据的假设上构建逻辑。


最佳实践

从网络请求入手,不要从代码入手:打开 DevTools Network,过滤目标请求,观察哪些参数在刷新后变化、哪些不变。变化的参数就是签名目标,然后再去找代码。直接搜索代码库效率低,因为混淆后的变量名毫无规律。

XHR/fetch 断点优先于全局搜索:在 DevTools Sources 面板设置 XHR 断点匹配目标 URL,比全局搜索参数名快得多。断到请求发出的位置后,调用栈里就能看到参数从哪里来。

Hook 验证参数来源:通过调用栈定位到疑似生成签名的函数后,在该函数入口 console.log(arguments) + console.trace(),刷新页面验证输入输出,再和 Network 里的实际值比对,确认后才开始分析逻辑。

魔法数字是识别算法的最快方式:MD5 的 0x67452301、SHA-1 的 0x67452301、SM3 的 0x79cc4519 这类初始化常量在混淆代码里不会被改变。搜索这些常量立刻定位算法实现,比从函数名、调用链入手快一个数量级。参见 魔法数字速查

用 Python 做交叉验证,不要只靠浏览器:还原出算法后,在 Python 里用标准库(hashlibpycryptodome)实现同样的计算,用相同输入对比输出。一致才算真的还原,否则可能有字节序、编码或 padding 细节遗漏。


常见陷阱

陷阱:在混淆代码里搜索参数名找不到

现象: Network 里看到 a_bogus=xxx,全局搜索 a_bogus 在 JS 里找不到任何赋值。

原因: 混淆代码把字符串分散存储,或通过字符串拼接、解密后动态生成键名,静态搜索无法找到。

解决: 改用运行时 Hook:Object.defineProperty(URLSearchParams.prototype, 'append', ...) 拦截所有 URLSearchParams 添加操作,或 Hook Object.defineProperty(document, 'cookie', ...) 拦截 Cookie 写入,在断点的调用栈里找来源。

陷阱:在 Node.js 里跑浏览器代码,window/document 报错

现象: 复制 JS 文件到 Node.js 执行,立刻报 ReferenceError: window is not defineddocument is not defined

原因: 浏览器 JS 依赖全局的 windowdocumentnavigator 等对象,Node.js 环境没有。

解决: 使用 jsdom 提供完整 DOM 环境,或手动补充全局变量。参见 jsdom完全指南补环境

陷阱:逆向出的算法输出和真实值不一致

现象: 以为还原了签名算法,Python 里算出的结果和抓包数据不一样。

原因: 常见遗漏点:字节序(大端/小端)、字符串编码(UTF-8/GBK/Latin-1)、HMAC 密钥的编码方式、哈希输入是字符串还是字节数组、输出是 hex 还是 Base64。

解决: 在浏览器里打印算法每一步的中间值,Python 里逐步对比,定位到第几步开始不一致,就能精确找到差异。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog