问题背景
Go
func CheckSign(c *gin.Context, signKey string, singExpire int) (string, error) {
r := c.Request
var formParams map[string]interface{}
if c.Request.Body != nil {
bodyBytes, _ := io.ReadAll(c.Request.Body)
defer c.Request.Body.Close()
if len(bodyBytes) > 0 {
//原始有问题的写法
//err := json.Unmarshal(bodyBytes, &formParams)
// 直接解析,会导致数字类型解析出来的是科学计数法
d := json.NewDecoder(bytes.NewReader([]byte(bodyBytes)))
d.UseNumber()
err := d.Decode(&formParams)
if err != nil {
return "", err
}
}
// 创建新的reader,使用bytes.NewReader
// 恢复r.Body,以便可以多次读取
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
}
sign := c.GetHeader("api-sign")
timestamp := c.GetHeader("api-timestamp")
if sign == "" || timestamp == "" {
return "", errors.New("api-sign 或 api-timestamp为空")
}
//验证时间戳格式
timestampValue, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return "", errors.New("timetamp 格式错误")
}
//验证时间戳
nowstamp := time.Now().UnixNano() / int64(time.Millisecond)
ago := timestampValue + int64(singExpire)
if nowstamp > ago {
return "", errors.New("签名时间已过期")
}
//验证签名
// 按照字段名正序排序
keys := make([]string, 0, len(formParams))
for k := range formParams {
keys = append(keys, k)
}
sort.Strings(keys)
targetArr := make([]string, 0, len(formParams))
// TODO 暂不支持嵌套json格式
for _, k := range keys {
val := reflect.ValueOf(formParams[k])
switch val.Kind() {
case reflect.Slice:
strSlice := make([]string, 0, val.Len())
for i := 0; i < val.Len(); i++ {
v := val.Index(i)
strSlice = append(strSlice, fmt.Sprintf("%v", v))
}
targetArr = append(targetArr, fmt.Sprintf("%s=%v", k, strings.Join(strSlice, ",")))
default:
targetArr = append(targetArr, fmt.Sprintf("%s=%v", k, formParams[k]))
}
}
str := strings.Join(targetArr, "&") + timestamp + signKey
hash := md5.Sum([]byte(str))
md5Str := hex.EncodeToString(hash[:])
if sign != md5Str {
return md5Str, errors.New("签名错误~")
}
if gin.Mode() == "prod" {
md5Str = ""
}
return md5Str, nil
}
前端传参:
bash
{"id":33,"old_warranty_end_time":1720713600,"new_warranty_end_time":1720800000}
前端生成验签加密之前的字符串如下:
33&new_warranty_end_time=1720800000&old_warranty_end_time=17207136001720755503589{{加密盐值}}
服务端验签加密之前的字符串如下:
id=33&new_warranty_end_time=1.7208e+09&old_warranty_end_time=1.7207136e+091720755503589{{加密盐值}}
显而易见加密之前拼接的字符串不一样。服务端拼接的字符串变成了科学计数法的格式。
问题原因定位
经过查询发现,问题出现在json.Unmarshal(bodyBytes, &jsonBody)这个地方,反序列化之后,出来的就是科学计数法的类型。
我们可以看一下反序列化之后的数据类型和值,解析为了float类型。
为什么float类型出来的是科学计数法的表示样式呢?这个问题我们到源码中寻找答案,我们先看这个问题的解决方案。
解决方案
Go
d := json.NewDecoder(bytes.NewReader([]byte(bodyBytes)))
d.UseNumber()
d.Decode(&jsonBody)
可以通过这种方式解决这个问题,这个问题很容易解决。但是有一点值得注意,通过这种方式反序列化数字类型会被反射为Number类型。
而这个json.Number的类型本质是个string类型。
问题原因
我们通过json.Unmarshal这个方法进到源码去看一下其执行逻辑。
Go
func Unmarshal(data []byte, v any) error {
// Check for well-formedness.
// Avoids filling out half a data structure
// before discovering a JSON syntax error.
var d decodeState
err := checkValid(data, &d.scan)
if err != nil {
return err
}
d.init(data)
return d.unmarshal(v)
}
chekValid这个方法是校验json格式是否合法,貌似是通过逐字节进行处理的。这个部分跟我们的问题不太相关,我们暂且略过。
Go
func (d *decodeState) unmarshal(v any) error {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Pointer || rv.IsNil() {
return &InvalidUnmarshalError{reflect.TypeOf(v)}
}
d.scan.reset()
d.scanWhile(scanSkipSpace)
// We decode rv not rv.Elem because the Unmarshaler interface
// test must be applied at the top level of the value.
err := d.value(rv)
if err != nil {
return d.addErrorContext(err)
}
return d.savedError
}
我们重点关注d.vale中的逻辑。
Go
func (d *decodeState) value(v reflect.Value) error {
switch d.opcode {
default:
panic(phasePanicMsg)
case scanBeginArray:
if v.IsValid() {
if err := d.array(v); err != nil {
return err
}
} else {
d.skip()
}
d.scanNext()
case scanBeginObject:
if v.IsValid() {
if err := d.object(v); err != nil {
return err
}
} else {
d.skip()
}
d.scanNext()
case scanBeginLiteral:
// All bytes inside literal return scanContinue op code.
start := d.readIndex()
d.rescanLiteral()
if v.IsValid() {
if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil {
return err
}
}
}
return nil
}
Go
func stateBeginValue(s *scanner, c byte) int {
if isSpace(c) {
return scanSkipSpace
}
switch c {
case '{':
s.step = stateBeginStringOrEmpty
return s.pushParseState(c, parseObjectKey, scanBeginObject)
case '[':
s.step = stateBeginValueOrEmpty
return s.pushParseState(c, parseArrayValue, scanBeginArray)
case '"':
s.step = stateInString
return scanBeginLiteral
case '-':
s.step = stateNeg
return scanBeginLiteral
case '0': // beginning of 0.123
s.step = state0
return scanBeginLiteral
case 't': // beginning of true
s.step = stateT
return scanBeginLiteral
case 'f': // beginning of false
s.step = stateF
return scanBeginLiteral
case 'n': // beginning of null
s.step = stateN
return scanBeginLiteral
}
//以数字开头的都是字面量
if '1' <= c && c <= '9' { // beginning of 1234.5
s.step = state1
return scanBeginLiteral
}
return s.error(c, "looking for beginning of value")
}
以数字开头的都归属于字面量类型。所以,我们看一下d.vale中的scanBeginLiteral这个分支。