当前位置: 首页 > news >正文

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

问题背景

func CheckSign(c *gin.Context, signKey string, singExpire int) (string, error) {r := c.Requestvar 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 + signKeyhash := md5.Sum([]byte(str))md5Str := hex.EncodeToString(hash[:])if sign != md5Str {return md5Str, errors.New("签名错误~")}if gin.Mode() == "prod" {md5Str = ""}return md5Str, nil
}

前端传参:

{"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类型出来的是科学计数法的表示样式呢?这个问题我们到源码中寻找答案,我们先看这个问题的解决方案。

解决方案

d := json.NewDecoder(bytes.NewReader([]byte(bodyBytes)))
d.UseNumber()
d.Decode(&jsonBody)

可以通过这种方式解决这个问题,这个问题很容易解决。但是有一点值得注意,通过这种方式反序列化数字类型会被反射为Number类型。

而这个json.Number的类型本质是个string类型。

问题原因

我们通过json.Unmarshal这个方法进到源码去看一下其执行逻辑。


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 decodeStateerr := checkValid(data, &d.scan)if err != nil {return err}d.init(data)return d.unmarshal(v)
}

 chekValid这个方法是校验json格式是否合法,貌似是通过逐字节进行处理的。这个部分跟我们的问题不太相关,我们暂且略过。

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中的逻辑。

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
}
func stateBeginValue(s *scanner, c byte) int {if isSpace(c) {return scanSkipSpace}switch c {case '{':s.step = stateBeginStringOrEmptyreturn s.pushParseState(c, parseObjectKey, scanBeginObject)case '[':s.step = stateBeginValueOrEmptyreturn s.pushParseState(c, parseArrayValue, scanBeginArray)case '"':s.step = stateInStringreturn scanBeginLiteralcase '-':s.step = stateNegreturn scanBeginLiteralcase '0': // beginning of 0.123s.step = state0return scanBeginLiteralcase 't': // beginning of trues.step = stateTreturn scanBeginLiteralcase 'f': // beginning of falses.step = stateFreturn scanBeginLiteralcase 'n': // beginning of nulls.step = stateNreturn scanBeginLiteral}//以数字开头的都是字面量if '1' <= c && c <= '9' { // beginning of 1234.5s.step = state1return scanBeginLiteral}return s.error(c, "looking for beginning of value")
}

以数字开头的都归属于字面量类型。所以,我们看一下d.vale中的scanBeginLiteral这个分支。

http://www.lryc.cn/news/398869.html

相关文章:

  • 罗技K380无线键盘及鼠标:智慧互联,一触即通
  • 卸载wps office的几种方法收录
  • SpringCloud第一篇Docker基础
  • 从零开始学习PX4源码3(如何上传官网源码到自己的仓库中)
  • Docker Compose 启动容器例子
  • 守护服务之门:Eureka中分布式认证与授权的实现策略
  • 核密度估计KDE和概率密度函数PDF(深入浅出)
  • 免开steam 脱离steam 进行游戏的小工具
  • 深度学习--系统配置流程
  • 把Docker的虚拟磁盘文件移动到别的盘符
  • Oracle 19c RAC 心跳异常处理
  • 微信小程序引入自定义子组件报错,在 C:/Users/***/WeChatProjects/miniprogram-1/components/路径下***
  • 【图解大数据技术】流式计算:Spark Streaming、Flink
  • 启动完 kubelet 日志显示 failed to get azure cloud in GetVolumeLimits, plugin.host: 1
  • C语言基础and数据结构
  • 【超万卡GPU集群关键技术深度分析 2024】_构建10万卡gpu集群的技术挑战
  • RuntimeError: CUDA error: invalid device ordinal
  • 如何在Qt中添加文本
  • 解决打印PDF文本不清楚的处理办法
  • 【Cesium开发实战】火灾疏散功能的实现,可设置火源点、疏散路径、疏散人数
  • imx6ull/linux应用编程学习(16)emqx ,mqtt创建连接mqtt.fx
  • Debezium系列之:验证mysql、mariadb等兼容mysql协议数据库账号权限
  • Vue.js学习笔记(五)抽奖组件封装——转盘抽奖
  • 使用pip或conda离线下载安装包,使用pip或conda安装离线安装包
  • 产品访问分析
  • 【算法】代码随想录之链表(更新中)
  • react 18中,使用useRef 获取其他组件的dom并操作节点,flushSync强制同步更新useState
  • Jupyter Notebook基础:用IPython实现动态编程
  • Python 爬虫:使用打码平台来识别各种验证码:
  • 理解算法复杂度:空间复杂度详解