蜜雪冰城小程序 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 — 手机号验证码登录(切换账号)复现(需手动过阿里云滑块)