

IpPure 的 x-k/x-t 请求签名分析
分析 IPPure API 中 x-k/x-t 的密钥下发、时间校准与 HMAC-SHA256 请求签名机制,并使用 Python 复现 GET 请求流程。
TL;DR
- 首次直接发送不带 x-k/x-t 的请求;从响应头取出 x-k 作为 HMAC 密钥,取出 x-t 作为 serverTime,同时记录 localTime = 当前毫秒时间。
- 重试时计算 timestamp = Date.now() - localTime + serverTime,签名原文是“请求方法-完整URL-Body-timestamp”;GET 无 Body,例如“GET-https://api.123169.xyz/api/info/ip-risk/172.67.214.188--1750860000000”。
- 用 x-k 对签名原文做 HMAC-SHA256 并输出 hex,请求头设为“x-k: <服务端Key>”和“x-t: <timestamp>-<HMAC hex>”。如果响应又返回新 x-k,更新密钥和服务器时间后再重试,建议最多 5 次。
前言#
研究IP风险检测的时候,在IPPure找到了这个接口:
https://api.123169.xyz/api/info/ip-risk/114.5.14txtIPPure 网页里面请求是正常的,但是请求中有两个应该是签名的东西 x-k 和 x-t,拿来研究一下玩玩。
x-k 从哪里来#
翻一下调用堆栈,在 security.D_hK1zoa.js 调用的
最开始看到的是这段:
async function l({request: n, modifiedRequest: e, response: t, context: r, meta: o}) {
const a = t.headers.get("x-k")
, s = t.headers.get("x-t");
if (a) {
if (localStorage.setItem("x-k", a),
s && localStorage.setItem("x-t", JSON.stringify({
serverTime: parseInt(s),
localTime: Date.now()
})),
o.loopIndex <= 3) {
const i = new c({
name: "api client",
beforeRequest: h,
afterResponse: l,
meta: {
loopIndex: o.loopIndex + 1
},
defaultOrigin: b.config.defaultOrigin
});
return f(await i.fetch(n), e.headers.get("x-t"))
}
return new Response({
ok: !1,
message: "too many failures"
})
}
return f(t, e.headers.get("x-t"))
}js这段并不是生成 Header 的,而是接收服务端返回的 Header。
响应中有 x-k 就存进 localStorage,然后把刚才的请求重新发一遍。loopIndex 超过次数后会直接返回 too many failures,猜测如果服务端一直返回新 Key 就死循环了。
x-t 在这里保存的是服务器时间,同时记录一下本机收到它时的时间:
{
serverTime: parseInt(s),
localTime: Date.now()
}js所以 x-k 不用自己算。
第一次可以直接请求,服务端返回一个 x-k,保存后再用它重新请求。Key 过期看起来也是返回新的 x-k 走这段代码。
x-t 的计算#
请求之前会从 localStorage 取出 x-k,然后拼接下面这几个值:
请求方式-完整URL-Body-时间戳txt使用 x-k 做 HMAC-SHA256,最后的 Header 是:
x-k: 服务端返回的Key
x-t: 时间戳-HMAC结果txt时间戳也没有直接用本机时间,而是这样计算,可能是为了防止电脑时间不准:
Date.now() - localTime + serverTimetxtGET 没有 Body,所以这个 IP 接口实际参与签名的字符串大概长这样:
GET-https://api.123169.xyz/api/info/ip-risk/172.67.214.188--1750860000000txtPython实现#
按照这个流程写出来就行,不需要提前准备 x-k。第一次请求会从响应里面拿,第二次才会带上签名。
当然我是让 CodeX 写的实现
import hashlib
import hmac
import time
import requests
url = 'https://api.123169.xyz/api/info/ip-risk/172.67.214.188'
session = requests.Session()
x_k = None
server_time = None
local_time = None
def now_ms():
return int(time.time() * 1000)
def make_headers():
if x_k is None:
return {}
timestamp = now_ms()
if server_time is not None:
timestamp = timestamp - local_time + server_time
text = f'GET-{url}--{timestamp}'
sign = hmac.new(
x_k.encode(),
text.encode(),
hashlib.sha256,
).hexdigest()
return {
'x-k': x_k,
'x-t': f'{timestamp}-{sign}',
}
for _ in range(5):
response = session.get(url, headers=make_headers(), timeout=15)
new_x_k = response.headers.get('x-k')
if new_x_k is None:
break
x_k = new_x_k
if value := response.headers.get('x-t'):
server_time = int(value)
local_time = now_ms()
else:
raise RuntimeError('too many failures')
print(response.status_code)
print(response.text)python这个脚本只处理了 GET,用来请求 IP 信息已经够了。
比较容易看错的是 x-t,服务端响应中的 x-t 是服务器时间,请求中的 x-t 才是 时间戳-HMAC。我最开始把响应处理这段当成生成 Header 的地方,所以一直没看到签名到底在哪里算的…