golang json反序列化科学计数法的坑

问题背景

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这个分支。

相关推荐
一点媛艺32 分钟前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风36 分钟前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生2 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功2 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨2 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程2 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man3 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang