package comm import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "errors" "fmt" "strconv" "time" ) // console → 业务服务的「运维调用」鉴权。 // // 背景:console 是单例服务(lego/base/single),不接 ETCD/rpcx 集群,无法直接 RPC 业务服务; // 它只能走业务网关的公网 HTTP 入口。而这类接口(如"重载业务配置")绝不能裸奔在公网上, // 故用 console 与各业务服务**本就必须一致**的 ${FIELD_ENCRYPT_KEY} 做 HMAC 共享密钥签名—— // 不新增需要两边同步的配置项(多一把密钥就多一次"两边不一致"的事故)。 // // 签名串 = HMAC-SHA256(key, "."),绑定了时间与目标接口; // ts 偏差超过 consoleSignSkew 即拒绝,限制重放窗口。 const ( ConsoleTsHeader = "X-Console-Ts" // 秒级 unix 时间戳 ConsoleSignHeader = "X-Console-Sign" // hex(HMAC-SHA256) ConsoleArgHeader = "X-Console-Arg" // 接口参数(如模块名)。已并入签名,不可篡改 // SessionMeta_ConsoleArg 网关把 ConsoleArgHeader 搬进 args.Meta 用的键。 // 业务侧 handler 通过 session.GetMateToString 读取——这样参数不必新增 pb 字段。 SessionMeta_ConsoleArg = "console_arg" // consoleSignSkew 允许的时间偏差。窗口内同一签名可被重放,但影响仅限于"多触发一次重载/重置", // 二者都幂等;再收紧会被两机时钟漂移误伤。 consoleSignSkew = 5 * time.Minute ) // consoleOnlyRoutes 只允许 console 携带合法签名调用的路由(网关据此免登录放行 + 强制验签)。 // 不放进 gateway.yaml 的 WhiteList——那是"免登录公开接口",语义完全不同。 // 挂在 api 模块上:api 服务是业务侧的管理入口,且与其它业务服务同在 rpcx 集群,可扇出下发。 var consoleOnlyRoutes = map[string]bool{ "api_reloadmoduleconfig": true, "api_resetmoduleconfig": true, } // IsConsoleOnlyRoute 判断某路由是否为「仅 console 可调、必须验签」的运维接口。 func IsConsoleOnlyRoute(msgName string) bool { return consoleOnlyRoutes[msgName] } // ModuleResetAll 传给「重置」接口的特殊模块名,表示整库还原为配置文件初始值(而非单个模块)。 const ModuleResetAll = "*" // SignConsoleCall 生成调用签名。ts 为秒级 unix 时间戳字符串;arg 为接口参数(无参数传 "")。 // 签名同时绑定 msgName 与 arg——否则拿到一个"重置 email"的签名就能改成"重置 wechatpay"。 func SignConsoleCall(key, ts, msgName, arg string) string { mac := hmac.New(sha256.New, []byte(key)) mac.Write([]byte(ts + "." + msgName + "." + arg)) return hex.EncodeToString(mac.Sum(nil)) } // VerifyConsoleCall 校验签名与时间戳。key 取 ${FIELD_ENCRYPT_KEY}。 // 用 hmac.Equal 做常数时间比较,避免按字节比较泄漏信息。 func VerifyConsoleCall(key, ts, sign, msgName, arg string, now time.Time) error { if key == "" { return errors.New("未配置 ${FIELD_ENCRYPT_KEY},无法校验 console 调用签名") } if ts == "" || sign == "" { return fmt.Errorf("缺少 %s / %s 请求头", ConsoleTsHeader, ConsoleSignHeader) } sec, err := strconv.ParseInt(ts, 10, 64) if err != nil { return errors.New("时间戳格式错误") } if d := now.Sub(time.Unix(sec, 0)); d > consoleSignSkew || d < -consoleSignSkew { return errors.New("时间戳超出允许偏差(检查两端时钟)") } expect := SignConsoleCall(key, ts, msgName, arg) if !hmac.Equal([]byte(sign), []byte(expect)) { return errors.New("签名不匹配(检查 console 与本服务的 ${FIELD_ENCRYPT_KEY} 是否一致)") } return nil }