【渣渣程序員踩過的坑】PHP的hash_hmac簽名加密,PHP迷同樣的base64_encode

介紹一下問題的背景:

本人一枚小小PHPer,有一天公司的Java找到我,讓我幫忙寫一個接口的Demo,心想:‘最喜歡寫接口了,來來來來!’,因而Java就帶着Java版Demo來了,大概看了一遍,具體涉及如下幾點:(不想了解的看最後一部分,就行了,那是中心思想)php

  • md5加密:

java中定義hashMap,儲存userid,再使用toJSONString將其轉換成Json串,再將Json使用md5Hex加密,再放入hashMap中java

  • map參數格式轉換:

將map中的數據轉換成String,對key,value進行數據拼接,拼接成字符串,此字符串具體要求以下:
‘按照參數名ASCII碼從小到大排序,使用URL鍵值對的格式(即key1=value1&key2=value2…)構形成字符串signPlainText’算法

  • hash_hmac簽名驗證:

這個沒什麼說的,PHP中有hash_hmac函數json

談論一下遇到的坑:

本PHPer比較渣渣,不知道Java中的hashMap是用來作啥的,但我知道,它最後作了一件事:JSONObject.toJSONString(body),沒錯,轉換成字符串了,因此有了如下代碼:(不負責的貼圖,不知道正確與否)安全

md5加密

Java版app

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,轉成了Md5post

clipboard.png

接下來就是map轉換string了,再貼兩段代碼:(不負責的貼,不知道正確與否)
(按照參數名ASCII碼從小到大排序,使用URL鍵值對的格式(即key1=value1&key2=value2…)構形成字符串signPlainText)ui

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), '+/', '-_'), '=');
    }

終是曉夢迷了蝶,你是恩賜也是劫!

相關文章
相關標籤/搜索