蜜雪冰城小程序 sign 参数分析报告
- 目标:
wx7696c66d2245d107(蜜雪冰城微信小程序,版本 2.8.43,主域名https://mxsa.mxbc.net) - 代码来源:
appservice.app.js(编译后 bundle)与 H5 前端mxsa-h5.mxbc.net各页面 chunk
1. sign 总览(共 3 套签名,同一盐家族)
| # | 场景 | s 值 | 盐 | 形式 |
|---|---|---|---|---|
| v1 | 主网关全部接口(request/index.js 自动附加) | 3 | salt_weixin |
md5hex + 数字后缀 |
| v2 | ThirdActivity 域登录系 4 接口(手动附加) | 2 | salt_md5_common |
纯 md5hex |
| v3 | H5 发验证码 captchaSms(手写固定串) | 2 | salt_md5_common |
纯 md5hex |
核心常量(config/index.js):
salt_md5_common: "c274bac6493544b89d9c4f9d8d542b84",
salt_weixin: "0d787c102fe2f7b4279af8925819d5fd",
appId_weixin: "d82be6bbc1da11eb9dd000163e122ecb",
miniAppId_weixin:"wx7696c66d2245d107",
2. 通用签名 sign_v1(enhanceMD5,s=3)
关键代码(utils/enhanceMD5.js,appservice.app.js:14642):
exports.default = function(r) {
delete r.sign;
r.appId = config.appId_weixin; // d82be6bbc1da11eb9dd000163e122ecb
r.t = Date.now() - (getApp().configData.timeOffset || 0); // 服务器校准时间
r.s = 3;
var n = md5hex(createStrBeforeSign(r) + config.salt_weixin);
var o = (function(e) { // 数字后缀: 16字节分4组大端int32取abs
for (var t = [], r = 0; r < 4; r++) {
var a = e[4*r] << 24 | e[4*r+1] << 16 | e[4*r+2] << 8 | e[4*r+3],
i = -2147483648 === a ? 2147483647 : Math.abs(a);
t.push(i)
}
return t.join("")
})(bytes(hex(n))); // hex 解析为 16 字节
return n + o; // 最终 sign = md5hex + 数字串
};
触发条件(request/index.js): 请求参数 v.s 为空或 v.s == 3 时自动签名;s == 2 时跳过(调用方手动签)。
createStrBeforeSign(utils/index.js,appservice.app.js:15285):
createStrBeforeSign: function(e) {
for (var r = "", t = Object.keys(e).sort(), n = 0; n < t.length; n++) {
var o = t[n];
if (e[o] || 0 === e[o]) {
var u = "object" === typeof e[o] ? JSON.stringify(e[o]) : e[o];
r += (n ? "&" : "") + o + "=" + u
} else "" !== e[o] && delete e[o] // 空串值剔除
}
return r
}
- 键名字母序排序,
k=v&k2=v2拼接,对象值 JSON 序列化(无空格) - 关键坑: JS 位运算是有符号 int32,
0x80000000特例取2147483647
3. 签名 v2(ThirdActivity,s=2)
关键代码(request/thirdActivity.js,4 个登录系接口 code2Session2/loginByAuthCode2/regByUnionid2/bindPhone2):
sign: md5hex(createStrBeforeSign(params) + config.salt_md5_common)
// params 含 {..., s: 2, third: "wxmini", miniAppId: "wx7696c66d2245d107"}
// 域: https://third-activity.mxbc.net/activity
4. 签名 v3(H5 captchaSms 手签)
关键代码(H5 changePhone-c0985c9e.js):
function h(s, t) { // s=手机号, t=captchaVerifyParam(阿里云验证码返回值)
return e.post("/v1/app/captchaSms", {
sign: md5(`captchaVerifyParam=${t}&mobilePhone=${s}&s=2` + "c274bac6493544b89d9c4f9d8d542b84"),
mobilePhone: s, captchaVerifyParam: t, s: 2
})
}
(固定顺序串恰好等于 createStrBeforeSign 排序结果;v2/v3 纯 md5 无数字后缀)
5. Python 完整实现(已与 Node 原始 JS 逻辑交叉验证一致)
import hashlib, json
SALT_WEIXIN = "0d787c102fe2f7b4279af8925819d5fd"
SALT_MD5_COMMON = "c274bac6493544b89d9c4f9d8d542b84"
APPID_WEIXIN = "d82be6bbc1da11eb9dd000163e122ecb"
BASE_URL = "https://mxsa.mxbc.net/api"
def create_str_before_sign(params: dict) -> str:
r = ""
for n, o in enumerate(sorted(params.keys())):
v = params[o]
if v or v == 0:
u = json.dumps(v, separators=(",", ":"), ensure_ascii=False) if isinstance(v, (dict, list)) else str(v)
r += ("" if n == 0 else "&") + o + "=" + u
elif v != "":
del params[o]
return r
def _suffix(md5_hex: str) -> str:
b = bytes.fromhex(md5_hex)
out = []
for r in range(4):
raw = (b[4*r] << 24) | (b[4*r+1] << 16) | (b[4*r+2] << 8) | b[4*r+3]
if raw >= 2**31: raw -= 2**32 # JS int32 有符号回绕
a = 2147483647 if raw == -2147483648 else abs(raw)
out.append(str(a))
return "".join(out)
def sign_v1(params: dict, t_now_ms: int, time_offset_ms: int = 0) -> dict:
p = dict(params); p.pop("sign", None)
p["appId"] = APPID_WEIXIN
p["t"] = t_now_ms - (time_offset_ms or 0)
p["s"] = 3
n = hashlib.md5((create_str_before_sign(p) + SALT_WEIXIN).encode()).hexdigest()
p["sign"] = n + _suffix(n)
return p
def sign_v2(params: dict) -> str:
return hashlib.md5((create_str_before_sign(params) + SALT_MD5_COMMON).encode()).hexdigest()
def sign_v3(mobile: str, captcha_verify_param: str) -> str:
return hashlib.md5(f"captchaVerifyParam={captcha_verify_param}&mobilePhone={mobile}&s=2{SALT_MD5_COMMON}".encode()).hexdigest()
6. 实测验证(真实调用)
JWT token(HS512,sub=wx_1722101504179081218,2h 有效)+ 上述签名实测:
| 接口 | 结果 |
|---|---|
GET /v1/app/config(无签无鉴权,校准 timeOffset≈531ms) |
code=0 |
GET /v1/customer/signin 签到 |
code=500 "签到功能已升级" — 接口已下线失效 |
GET /v1/customer/info 用户信息 |
code=0,customerId 与 JWT sub 一致,sign/token 均通过 |
GET /v1/customer/levels 会员等级 |
code=0 |
POST /v1/customercoupons/total 优惠券 |
code=0 |
POST /v2/shopinfo/findOften 常去门店 |
code=0 |
结论: sign_v1 算法完全正确(全部业务接口 code=0 返回真实数据);token 有效;签到接口本身已升级下线(500 为业务下线提示,非签名失败)。
7. 交付文件
mxbc_sign.py— sign 算法库(v1/v2/v3,含固定输入自检)mxbc_test_sign.py— 实测脚本(config 校准 → 签到 → 用户/等级/券/门店)mxbc_switch_login.py— 手机号验证码登录(切换账号)复现(需手动过阿里云滑块)