本地化踩坑(1):签名验证

7 阅读3分钟

日常开发中,服务端对客户端请求上来的数据进行签名验证是常用的安全手段之一。

签名验证可以确保数据在传输过程中未被篡改,并且请求确实来自合法的客户端。

对于下面这个Json请求串:

{
    "strData": "Hello, World!",
    "floatData": 3.14,
    "intData": 42,
    "timestamp": 1627891234
}

我们通常用下面两种方式来进行签名验证。

Json格式化成字符串后进行签名

  1. 将Json对象转换成字符串A(通常是紧凑格式,没有多余的空格和换行)。

  2. 使用预共享的密钥对该字符串A进行签名(如HMAC-SHA256),生成签名值S

  3. 重新组成下面新Json请求串发送给服务器,服务器端使用相同的密钥和算法验证签名。

    {
       "data": "这里是Json对象转换成的字符串A",
       "signature": "这里是签名值S"
    }
    

将请求数据进行排序后拼接成字符串,再进行签名

  1. 对请求中的字段按字典序排序,得到 floatData=3.14&intData=42&strData=Hello, World!&timestamp=1627891234

  2. 使用预共享的密钥对该字符串进行签名(如HMAC-SHA256),生成签名值S

  3. 将签名值S附加到原来请求中发送给服务器,服务器端使用相同的密钥和算法验证签名。比如:

    {
       "strData": "Hello, World!",
       "floatData": 3.14,
       "intData": 42,
       "timestamp": 1627891234,
       "signature": "这里是签名值S"
    }
    

如果你们是第一种,那么这种方式在本地化环境下不会有坑,放心使用。

现在讲第二种方式的两个坑:

  • 默认的字符串排序在不同地区环境下可能不一样,从而导致签名验证失败。

    比如i和I在土耳其语环境下排序时会和我们不一样。

    解决办法是强制指定字符串比较时使用CultureInfo.InvariantCulture或者CultureInfo.Ordinal

  • 浮点数在不同地区环境下转换成字符串时可能不一样,从而导致签名验证失败。

    比如3.14在德国环境下会被转换成"3,14"(逗号),而在我们环境下会被转换成"3.14"(点)。

    解决办法是强制指定浮点数转换成字符串时使用CultureInfo.InvariantCulture

下面是C#签名示例代码,供参考:

// 对请求参数进行签名
public static string Sign(Dictionary<stringobject> args, string salt)
{
   // 使用InvariantCulture的字典序排序参数
   SortedDictionary<stringobject> dict =
      new SortedDictionary<stringobject>(StringComparer.InvariantCulture);
   foreach (var iter in args)
   {
      dict.Add(iter.Key, iter.Value);
   }

   string str = BuildInvariantDictionaryString(dict, "&""=");

   // 我封装的MD5签名函数,你也可以用HMAC-SHA256等其他签名算法
   return CodecUtils.Md5String(str + salt);
}
// 使用InvariantCulture将排序后的字典拼接成字符串
private static string BuildInvariantDictionaryString(SortedDictionary<string, object> dict,
   string delimiter1 = ";", string delimiter2 = ",")
{
   if (dict == null) return string.Empty;
   StringBuilder sb = new StringBuilder();
   foreach (var iter in dict)
   {
         string key = iter.Key ?? string.Empty;
         object val = iter.Value;
         string v;
         if (val == null)
         {
            v = string.Empty;
         }
         else if (val is string s)
         {
            v = s;
         }
         else if (val is IFormattable formattable)
         {
            v = formattable.ToString(null, CultureInfo.InvariantCulture);
         }
         else
         {
            v = Convert.ToString(val, CultureInfo.InvariantCulture) ?? string.Empty;
         }

         sb.Append(key).Append(delimiter2).Append(v).Append(delimiter1);
   }

   if (sb.Length > 0)
   {
         return sb.ToString(0, sb.Length - 1);
   }
   return sb.ToString();
}