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

相关推荐
猷咪17 分钟前
C++基础
开发语言·c++
IT·小灰灰19 分钟前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧20 分钟前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q21 分钟前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳021 分钟前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾21 分钟前
php 对接deepseek
android·开发语言·php
2601_9498683625 分钟前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
星火开发设计39 分钟前
类型别名 typedef:让复杂类型更简洁
开发语言·c++·学习·算法·函数·知识
qq_177767371 小时前
React Native鸿蒙跨平台数据使用监控应用技术,通过setInterval每5秒更新一次数据使用情况和套餐使用情况,模拟了真实应用中的数据监控场景
开发语言·前端·javascript·react native·react.js·ecmascript·harmonyos
一匹电信狗1 小时前
【LeetCode_21】合并两个有序链表
c语言·开发语言·数据结构·c++·算法·leetcode·stl