You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
80 lines
3.6 KiB
80 lines
3.6 KiB
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>.<msgName>"),绑定了时间与目标接口;
|
|
// 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
|
|
}
|
|
|