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

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
}