【渣渣程序员踩过的坑】PHP的hash_hmac签名加密,PHP迷一样的base64_encode
介绍一下问题的背景:
本人一枚小小PHPer,有一天公司的Java找到我,让我帮忙写一个接口的Demo,心想:‘最喜欢写接口了,来来来来!’,于是Java就带着Java版Demo来了,大概看了一遍,具体涉及以下几点:(不想了解的看最后一部分,就好了,那是中心思想)
- md5加密:
java中定义hashMap,储存userid,再使用toJSONString将其转换成Json串,再将Json使用md5Hex加密,再放入hashMap中
- map参数格式转换:
将map中的数据转换成String,对key,value进行数据拼接,拼接成字符串,此字符串具体要求如下:
‘按照参数名ASCII码从小到大排序,使用URL键值对的格式(即key1=value1&key2=value2…)构造成字符串signPlainText’
- hash_hmac签名验证:
这个没什么说的,PHP中有hash_hmac函数
谈论一下遇到的坑:
本PHPer比较渣渣,不知道Java中的hashMap是用来做啥的,但我知道,它最后做了一件事:JSONObject.toJSONString(body),没错,转换成字符串了,所以有了以下代码:(不负责的贴图,不知道正确与否)
md5加密
Java版
Map<String, String> body = new HashMap<>(); body.put("userId", userId); //post请求body为json格式,将json格式进行md5加密 String postBody = JSONObject.toJSONString(body); String bodyMd = DigestUtils.md5Hex(postBody);
PHP版
$body = ["userId"=>$userId]; $postbody = json_encode($body); $bodyMd = md5($postbody);
上边我们之间应该是在做一件事请吧?应该是的,反正都是转成了Json,转成了Md5
接下来就是map转换string了,再贴两段代码:(不负责的贴,不知道正确与否)
(按照参数名ASCII码从小到大排序,使用URL键值对的格式(即key1=value1&key2=value2…)构造成字符串signPlainText)
map参数格式转换:
Java版
Map<String, Object> resultMap = new TreeMap<>(); for (Map.Entry entry : map.entrySet()) { String key = (String) entry.getKey(); Object value = entry.getValue(); if (StringUtils.isEmpty(key) || value == null) { continue; } resultMap.put(key, value); } StringBuilder buff = new StringBuilder(); for (Map.Entry<String, Object> entry : resultMap.entrySet()) { buff.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } String signPlanText = buff.toString(); if (StringUtils.isNotEmpty(signPlanText)) { signPlanText = signPlanText.substring(0, signPlanText.length() - 1); } return signPlanText;
PHP版
$signPlanText = ''; ksort($params); foreach ($params as $key => $param){ if(empty($key) || $param == null){ continue; } $signPlanText .= $key.'='.$param.'&'; } if($signPlanText){ $signPlanText = substr($signPlanText,0,strlen($signPlanText)-1); } return $signPlanText;
遇到的问题: 因为Java中Treemap是有序的,按照自然值进行排序,但PHP中好像没有这样一个东西,所以决定使用Ksort来进行排序,不知道是否正确!应该是正确的吧?各位大神拍砖~
HmacSHA256签名验证:
Java版
String signature = ""; try { Mac sha256HMAC = Mac.getInstance(HMACSHA256); SecretKeySpec secretkey = new SecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8), HMACSHA256); sha256HMAC.init(secretkey); byte[] bytes = sha256HMAC.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); signature = Base64.encodeBase64URLSafeString(bytes); } catch (Exception e) { e.printStackTrace(); } return signature;
PHP版( 呀?看起来是不是没有问题?)
$hmac_sha256_str = base64_encode(hash_hmac("sha256", $signPlanText, $appSecret)); $signature = urlencode($hmac_sha256_str); return $signature;
还在想,PHP这么点代码就解决了?不愧是世界上最好的语言’,(大佬们,我加引号了)
23:35分终于翻译完以上Java代码,放一首歌,美美的睡觉,心想明天可以美美的交工~ (滴滴滴滴滴滴等待~地理地理等待~~)
坑来了,运行上边代码,你会发现,接口提示签名错误!!错误一直是错误!!为什么呢???明明是一样的啊!百思不得其解!!
继续往下看···
再看PHP文档中的hash_hmac的声明:
string hash_hmac ( string $algo , string $data , string $key [, bool $raw_output = FALSE ] ) algo 要使用的哈希算法名称,例如:"md5","sha256","haval160,4" 等。 如何获取受支持的算法清单,请参见 hash_algos()。 data 要进行哈希运算的消息。 key 使用 HMAC 生成信息摘要时所使用的密钥。 raw_output 设置为 TRUE 输出原始二进制数据, 设置为 FALSE 输出小写 16 进制字符串。
在Java中sha256HMAC后得到的值为二进制,So,PHP也要转换为二进制,所以改进为以下代码:
hash_hmac("sha256", $signPlanText, $appSecret,true);//由此生成出的为二进制格式
这还没完,最重要的出现了:
java中Base64.encodeBase64URLSafeString(bytes) 会将 特殊字符‘+/’替换为'-_',会将‘=’去掉
但是!!PHP不会!!! PHP 没有提供url安全的base64编码函数,需要自己手动去撸!还是自己水平不够
So,出现了以下代码,也是最终的解决方案:
/** * 签名验证 */ public static function generateSHASign($signPlanText,$appSecret){ $signature = self::base64UrlEncode(hash_hmac("sha256",$signPlanText, $appSecret, true)); return $signature; } /** * 替换特殊符号 */ public static function base64UrlEncode($str){ return rtrim(strtr(base64_encode($str),'+/','-_'),'='); }
这时,你会发现,签名正确,原来生活如此美好、怪我太年轻
同时有朋友说:
1.是有两套不相同的标准,Ios一套,Java一套
2.在PHP中url_encode这个函数,分两种: urlencode 和 rawurlencode
两者的差异:urlencode和rawurlencode两个方法在处理字母数字,特殊符号,中文的时候结果都是一样的,唯一的不同是对空格的处理,urlencode处理成“+”,rawurlencode处理成%20
总结下来就是:
php的base64_encode和base64_decode用来转换url是不符合要求,所以需要自己实现方法
base64url_decode
public static function base64url_decode($data) { return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT)); }
base64url_encode
public static function base64url_encode($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); }
终是晓梦迷了蝶,你是恩赐也是劫!