Linux中國

PHP-RSA加密跨域通訊實戰

首先要生成密匙對

openssl genrsa 1024 > private.key
openssl rsa -in private.key -pubout > public.key

JS的RSA加密流程

下載最新版本請移步到github:jsencrypt 代碼在目錄BIN下面是否用壓縮的根據情況決定。

生成KEY

var keySize = 1024; //加密強度
var crypt = new JSEncrypt({default_key_size: keySize});  //RSA 操作對象
//方法1 (async)
crypt.getKey(function () {
    crypt.getPrivateKey();
    crypt.getPublicKey();
});
//方法2:
crypt.getKey();
crypt.getPrivateKey();
crypt.getPublicKey();

客戶端加密場景:

var crypt1 = new JSEncrypt(); //新建rsa對象
        var publickey = '
        -----BEGIN PUBLIC KEY-----
        MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC3N8LJFqlsa6loCgFpgZVMr/Sx
        DMQY7pr0euNQfh2g+UVPbB0MGhoc7nWL0FQhCgDedbjQw/nGFStFx7W1+0o1oRTY
        u5ebNVivZSobraUv7LJvwT8O66Zs8cxbKLqQ/nE/WwJvXomSIckH6R8iOUO8/QT9
        kv6/L0Uma3qA07pmDQIDAQAB
        -----END PUBLIC KEY-----
                ';
crypt1.setPublicKey(publickey );//添加來自服務端的publickey
crypt1.encrypt('abc'); //返回值為加密後的結果

客戶端解密場景:

        var privatekey = '-----BEGIN RSA PRIVATE KEY-----
        MIICXQIBAAKBgQC3N8LJFqlsa6loCgFpgZVMr/SxDMQY7pr0euNQfh2g+UVPbB0M
        Ghoc7nWL0FQhCgDedbjQw/nGFStFx7W1+0o1oRTYu5ebNVivZSobraUv7LJvwT8O
        66Zs8cxbKLqQ/nE/WwJvXomSIckH6R8iOUO8/QT9kv6/L0Uma3qA07pmDQIDAQAB
        AoGBAKba3UWModbfZXQeSJLxNCqWw9zJp3ydL/keQQ35DLqgyIJAD2QKEWXvtJUT
        sMo19fyicSGOmFXQyYvPCKkmpLkOMAj1XaNpSMtSrcMx+gC01PO6Ey9rsUxW1g3u
        fpqbEk9E3a5AtCS0I61nbUpRL6rqMtR5o2wcNR3TLtJt7pjxAkEA7hlFJKU1zWGp
        OvvkJDnHc2NOCEJoGjqCR9wwv96+/xAykl2laI6WvEbbhjoO0+8+d17oigjhneS5
        2UKFcfqw7wJBAMT+MCQ5TYLQlvjrBaDMqOdLsqtaDE6CpkrgwV820QMvHOo3R4Xd
        uSbrA2tOr9t2/x+FzF971lRGdPFIch9UYMMCQQCZtO6SDaWCBP3++gX57OL5dq41
        XsldxU+9nERMWTvr5UUAgDv8F7Dvsr6dFHXmE5i77yUnlzwvdi0UOIF1Z2U5AkBV
        wyRKYPgx34Ya0JcerntKV1Zt60I4XADx0G/feAn/DN/VyENHMISPQPm4GgXN0jy4
        CJQ1bcCd6B65fQTSRvXpAkA2Vv5yXzeKDls/AyxHEoros/VYftVc1HOFC++q13Rw
        NH2rnlRT8FMTFEqL9MYRqvvYAFf5VmH0M1Nx5t4LRN+l
        -----END RSA PRIVATE KEY-----
                ';
var crypt2 = new JSEncrypt();//新建加密對象
crypt2.setPrivateKey(privatekey);//給加密對象設置privatekey
crypt2.getPublicKey();//Tip 我們是不需要存儲publickey的直接用private能得到publickey
crypt2.decrypt("MeUqWB5LwTh8crzPqbZtEtKuZxYvPWH9CTCChK1qoBzIgIXGPCdzNMbiH0cCYHl5qWSERIDOgDIgv4dXsIMjEJ5q0cp/qNQYHM5va0iw0UvKvQB1E8aWtY2nFEPy4F+ArQ0Mj/ijr/CntEP1jHKC3WU9nu2kYrBIBnbj14Bs+kI=");//調用解密方法

但是雖然寫到了這裡,加密方面還是不夠用,因為1024長度的RSA加密最多只能加密長度為117的字元串。而URL長度最多為4k因此這裡我們要讓加密長度達到2691以達到能用的程度。

那麼這種加密長度大概能容納多少數據呢? 我們藉助json-generator來幫忙生成JSON

    sdata =[
        {
            "_id": "542f9ac2359c7d881bc0298e",
            "index": 0,
            "guid": "db1dacc1-b870-4e3c-bc1a-80dfd9506610",
            "isActive": false,
            "balance": "$1,570.15",
            "picture": "http://placehold.it/32x32",
            "age": 36,
            "eyeColor": "blue",
            "name": "Effie Barr",
            "gender": "female",
            "company": "ZORK",
            "email": "effiebarr@zork.com",
            "phone": "+1 (802) 574-3379",
            "address": "951 Cortelyou Road, Wikieup, Colorado, 4694",
            "about": "Sunt reprehenderit do laboris velit qui elit duis velit qui. Nostrud sit eiusmod cillum exercitation veniam ad sint irure cupidatat sunt consectetur magna. Amet nisi velit laboris amet officia et velit nisi nostrud ipsum. Cupidatat et fugiat esse minim occaecat cillum enim exercitation laboris velit nisi est enim aute. Enim do pariaturrn",
            "registered": "2014-05-08T15:26:35 -08:00",
            "latitude": 48.576424,
            "longitude": 146.634137,
            "tags": [
                "esse",
                "proident",
                "quis",
                "consectetur",
                "magna",
                "tempor",
                "anim"
            ],
            "friends": [
                {
                    "id": 0,
                    "name": "Trisha Cannon"
                },
                {
                    "id": 1,
                    "name": "Todd Bullock"
                },
                {
                    "id": 2,
                    "name": "Eileen Drake"
                },
                {
                    "id": 3,
                    "name": "Ferrell Kelly"
                },
                {
                    "id": 4,
                    "name": "Fischer Blankenship"
                },
                {
                    "id": 5,
                    "name": "Morales Mann"
                },
                {
                    "id": 6,
                    "name": "Brandie Pittman"
                },
                {
                    "id": 7,
                    "name": "Virgie Kerr"
                }
            ],
            "greeting": "Hello, Effie Barr! You have 1 unread messages.",
            "favoriteFruit": "apple"
        },
        {
            "_id": "542f9ac21c260d03e763a4f2",
            "index": 1,
            "guid": "9e3a3d8a-26f8-46b7-aca0-336a194808b1",
            "isActive": true,
            "balance": "$3,617.89",
            "picture": "http://placehold.it/32x32",
            "age": 31,
            "eyeColor": "brown",
            "name": "Butler Best",
            "gender": "male",
            "company": "SPORTAN",
            "email": "butlerbest@sportan.com",
            "phone": "+1 (905) 428-3046",
            "address": "798 Joval Court, Wanship, Delaware, 8974",
            "about": "Nostrud occaecat id sunt pariatur ad nisi do veniam sit officia non consequat amet fugiat. Est eiusmod labore ut cillum qui eu elit ut eiusmod exercitation. Ut anim nostrud eiusmod voluptate tempor proident id do pariatur. In Lorem ullamco ea irure adipisicing. Quis est dolor ex commodo aliqua nisi elit sit elit anim fugiat sunt amet. Enim consequat ipsum occaecat ipsum tempor deserunt dolor veniam nostrud. Anim cillum ullamco cupidatat aute velit fugiat sit enim in amet anim mollit dolor eiusmod.rn",
            "registered": "2014-08-02T06:15:44 -08:00",
            "latitude": -20.529765,
            "longitude": 2.396578,
            "tags": [
                "consequat",
                "enim",
                "magna",
                "sunt",
                "Lorem",
                "quis",
                "commodo"
            ],
            "friends": [
                {
                    "id": 0,
                    "name": "Kenya Rice"
                },
                {
                    "id": 1,
                    "name": "Hale Knowles"
                },
                {
                    "id": 2,
                    "name": "Michael Stephens"
                },
                {
                    "id": 3,
                    "name": "Holder Bailey"
                },
                {
                    "id": 4,
                    "name": "Garner Luna"
                },
                {
                    "id": 5,
                    "name": "Alyce Sawyer"
                },
                {
                    "id": 6,
                    "name": "Rivas Owens"
                },
                {
                    "id": 7,
                    "name": "Jan Petersen"
                }
            ],
            "greeting": "Hello, Butler Best! You have 8 unread messages.",
            "favoriteFruit": "banana"
        }
    ]

表單json能達到這麼長已經是很極端的情況了。因此這種方法絕對是夠用的。

長表單內容加解密方法:

function encrypt_data(publickey,data)
{
    if(data.length> 2691){return;} // length limit
    var crypt = new JSEncrypt();
    crypt.setPublicKey(publickey);
    crypt_res = "";
    for(var index=0; index < (data.length - data.length%117)/117+1 ; index++)
    {
        var subdata = data.substr(index * 117,117);
        crypt_res += crypt.encrypt(subdata);
    }
    return crypt_res;
}
function decrypt_data(privatekey,data)
{
    var crypt = new JSEncrypt();
    crypt.setPrivateKey(privatekey);
    datas=data.split(&apos;=&apos;);
    var decrypt_res="";
    datas.forEach(function(item)
    {
        if(item!=""){de_res += crypt.decrypt(item);}
    });
    return decrypt_res;
}

##########NextPage[title=]##########

PHP的RSA加密

php加密解密類

首先要檢查phpinfo裡面有沒有openssl支持

class mycrypt {  

    public $pubkey;  
    public $privkey;  

    function __construct() {  
                $this->pubkey = file_get_contents(&apos;./public.key&apos;);  
                $this->privkey = file_get_contents(&apos;./private.key&apos;);  
    }  

    public function encrypt($data) {  
        if (openssl_public_encrypt($data, $encrypted, $this->pubkey))  
            $data = base64_encode($encrypted);  
        else  
            throw new Exception(&apos;Unable to encrypt data. Perhaps it is bigger than the key size?&apos;);  

        return $data;  
    }  

    public function decrypt($data) {  
        if (openssl_private_decrypt(base64_decode($data), $decrypted, $this->privkey))  
            $data = $decrypted;  
        else  
            $data = &apos;&apos;;  

        return $data;  
    }  

}  

密匙文件位置問題,是放到訪問介面的附近就可以了如果是CI的話就放到index.php旁邊就行了。 但是要注意一點,一定要做訪問設置,不然key會暴出來的,那時候信息一旦截獲就慘了。

類的使用

$rsa = new mycrypt();  
echo $rsa -> encrypt(&apos;abc&apos;);  
echo $rsa -> decrypt(&apos;W+ducpssNJlyp2XYE08wwokHfT0bm87yBz9vviZbfjAGsy/U9Ns9FIed684lWjYyyofi/1YWrU0Mp8vLOYi8l6CfklBY=&apos;);  

長數據加密解密

function encrypt_data($publickey,$data)
{
    $rsa = new mycrypt();
    if($publickey != ""){
        $rsa -> pubkey = $publickey;
    }
    $crypt_res = "";
    for($i=0;$i<((strlen($data) - strlen($data)%117)/117+1); $i++)
    {
        $crypt_res = $crypt_res.($rsa -> encrypt(mb_strcut($data, $i*117, 117, &apos;utf-8&apos;)));
    }
    return $crypt_res;
}
function decrypt_data($privatekey,$data)
{
    $rsa = new mycrypt();
    if($privatekey != ""){  // if null use default
        $rsa ->privkey = $privatekey;
    }
    $decrypt_res = "";
    $datas = explode(&apos;=&apos;,$data);
    foreach ($datas as $value)
    {
        $decrypt_res = $decrypt_res.$rsa -> decrypt($value);
    }
    return $decrypt_res;
}

JSONP 跨域通訊

我們經過千辛萬苦經過加密終於能做到通訊安全了。 當然我們的下一步是通過JSONP 的get通訊來實現跨域通訊啦。 經過測試:我們的JS中最長的Case url長度是3956 在加上跨域url callbac參數,經過測試正好差20到4095 (一般的URI長度限制為4K)

$.ajax({  
    type:"get",  
    async:false,  // 設置同步通訊或者非同步通訊
    url:"http://22500e31b5a12457.sinaapp.com/ubtamat/getPubKey?c=hknHQKIy3dyeeajyAwZ+raUkV1ezFbgU8zk+54cNQtrcEGozUjXpYhbC6fxz2hCOgp9feIsM1xKJFm5pkAGQ2UcUOc5EJNCAz6L0mXkZbTBmh3PufWxOE7TaicqRCRtZGGNB2qpm2WruXjYg1lPcrPz/rhFZx4DSJvEHkCm7ZU0=......(加密後的結果太長,省略)",  
    dataType:"jsonp",
    jsonp: "",
}); 
header("Content-type: application/javascript; charset=utf-8");
$response = "console.log(&apos;test response!&apos;)";
$callback = $this->input->GET(&apos;callback&apos;);
echo $callback.$response;

PHP代碼是CI框架controler中的部分代碼 並且經過了必要的裁剪。 更加細節的參數都放到GET裡面就可以了。 處理之後按照上面的形式處理返回值就ok 如果你配置成功了,你將會在網頁的控制台上看到自己動態的, 或者像我一樣靜態的控制台輸出。 如果要是想獲取數據到網頁的話還是要藉助回調函數來實現

JSONP跨域獲取通訊結果

請看下面代碼:

客戶端代碼

var global = null;
function jpc(result)
{
    global = result.msg;
}

$.ajax({  
    type:"get",  
    async:false,  // 設置同步通訊或者非同步通訊
    url:"http://22500e31b5a12457.sinaapp.com/ubtamat/getPubKey",  
    dataType:"jsonp",
    jsonp: "jpc",
}); 

伺服器端代碼

header("Content-type: application/javascript; charset=utf-8");
$response = "jpc({&apos;msg&apos;:123456})";
$callback = $this->input->GET(&apos;callback&apos;);
echo $callback.$response;

此次通訊的結果會在jcp當中調用執行,並且返回的內容會記錄到 global 變數當中。

實戰

從上文中,我們已經找到了整個加密過程方法了,但是距離實戰還是有一定距離的。 首先我們實戰的話需要克服介面比較少,功能比較多,單個介面維護用時比較長的問題。

為了解決上面的問題我們做出如下設計。

客戶端方面:

設計一個通訊類:只管跟伺服器通訊。別的業務什麼都不管。

//create connection object.
var ConnServ = new Object();

ConnServ.tmpResponse = "not initial";

//call back function register slot.
ConnServ.CallBackFunction=function(){console.log(
    "call back function set error ! U must set a business call back function!"
)};

//input only encrypted data!!!
//send data to server
ConnServ.send=function(data)
{
    data = data.replace(/+/g,"$");  //replace all + as $
    $.ajax({
        type:"get",
        async:false,  
        url:"http://22500e317.sinaapp.com/ubtamat?c="+data,
        dataType:"jsonp",
        jsonp: "jpc"
    });
    return "Send Finish";
}

//default call back funcation
function jpc(res)
{
    ConnServ.tmpResponse = res.msg;
    ConnServ.CallBackFunction();
}

//public key store.
ConnServ.getpublickey = function()
{
    return "-" +
        "----BEGIN PUBLIC KEY----- " +
...................................................
        "-----END PUBLIC KEY-----";
}

在上面代碼中請注意,RSA加密過後的字元串當中有一個非法字元+要轉換成其他合法字元發送到伺服器才可以。 不然參數會錯誤。 等傳輸到伺服器中自己轉換回來在解密就好了。

伺服器端方面:

首先我們接收到消息之後要對消息進行解密,之後根據報文內容選擇伺服器上的功能。然後把其他參數輸入到業務類中執行即可。 因此我們使用了命令模式來實現單一介面的豐富業務功能。 其他的我們需要對CI框架的配置進行調整: 首先global config裡面需要調整 $config['global_xss_filtering'] = FALSE; 因為如果傳輸過來的報文解密不了就直接拋棄不進行處理(防止CC攻擊第一層)這樣就從url上防止了攻擊的可能性。 當然我們還是沒有完全避免注入風險這時我們就需要在業務類裡面調用安全模塊:

 $this->security->xss_clean()

來實現第二層的XSS攻擊。這是伺服器端設計主要需要說的位置。

伺服器獲取數據處理全過程

  1. 從get介面獲得參數c的加密數據
  2. 對數據進行RSA解密。
  3. 判斷數據包時間戳。如果超時直接拋棄(防止從瀏覽器記錄中直接發送request到伺服器,下面是安全方面的說明)
    • 首先如果不修改數據只修改時間戳不可能從截獲的數據報文中實現,因為需要重新加密,如果想得到內容需要伺服器上的privatekey解密保證安全
    • 如果數據包截獲直接發送數據包在超時範圍內直接獲取數據包內容,也不能實現攻擊,因為在客戶端有臨時RSA密匙對生成並且在發送的時候會同時發送publickey 給伺服器做session的存儲內容並且偽裝客戶的客戶端沒有privatekey所以獲取任何關於登陸之後的消息根本無法解析。
  4. 對解密後的數據進行xss檢查
  5. 解析報文中需要調用什麼功能直接調用反射得到業務類的實例
  6. 調度業務類,並且把得到的參數賦值給業務執行函數的參數。

伺服器處理數據過程只跟業務有關

伺服器返回數據全過程

  1. 業務處理完成之後針對每一個用戶的登陸情況對返回值進行加密。
  2. response

以上業務涉及的部分代碼(給出的代碼未涉及以上說的安全部分。)

//CI 控制器裡面的方法
public function index()
{
    header("Content-Type: text/html;charset=UTF-8");
    $callback = $this -> input->GET(&apos;callback&apos;);
    $input_data = str_replace("$","+",$this->input->GET(&apos;c&apos;));
    $input_data =$this -> rsa->decrypt_data($input_data);
    if($input_data == ""){return;}//如果數據不對解析就會失敗,直接拋棄數據包,避免cracker構造數據包問題
    //這裡插入時間戳檢查代碼
    //這插入xss檢查
    $output_data = command($input_data);
    $response = "jpc({&apos;msg&apos;:".$output_data."})";
    $callback = $this->input->GET(&apos;callback&apos;);
    echo $callback.$response;
}
//命令模式中的業務調度方法
function command($input)
{
    try
    {
        $obj_input = json_decode($input);
        $action = $obj_input -> {"action"};
        $business_action = new ReflectionClass($action);
        $instance  = $business_action->newInstanceArgs();
        $output = $instance->Action($obj_input);
        //對output變數進行rsa加密
        return "&apos;".$output."&apos;"; // here only accept string
    }
    catch(Exception $e)
    {
        return  "&apos;".$e->getMessage()."&apos;";
    }
}

以下是配合業務進行的工具函數:

//命令介面定義
interface ICommand {
    function Action($arg_obj);
} 
//把此函數放到system/core/common.php
//實現了輸入一個文件夾就自動載入所有文件夾中的所有的類。
if ( ! function_exists(&apos;require_once_dir&apos;))
{
    function require_once_dir($path)
    {
        $dir_list = scandir($path);
        foreach($dir_list as $file)
        {
            if ( $file != ".." && $file != "." )
            {
                require_once($path."/".$file);
            }
        }
    }
}

//使用:
//在application/config/autoload.php中添加類似如下代碼:
require_once_dir(APPPATH."/controllers/lib");
require_once_dir(APPPATH."/controllers/actions");

以下是實現業務的例子:

class register implements  ICommand{
    public function Action($arg_obj)
    {
        return "we are do nothing: ".json_encode($arg_obj);
    }
}

通過以上基本方法,我們可以實現,只要業務繼承我們聲明的介面就可以開始寫業務了。 別的什麼都不用管,專註於業務即可,其他的安全、IO等問題都已經一併解決。 並且每一個業務都進行了rsa加密xss攻擊過濾偽造數據包攻擊。 以及在response加密只能是固定客戶端才能看到報文內容的全過程。 但是一定要注意一點,註冊這個業務後面要嵌套登陸進行,不然看不到返回值。

數據包必須包含的要素:

  1. acton (業務名)
  2. req_time (請求時間)
  3. public_key (如果是註冊跟登陸時候需要提交臨時公匙)

總結

因為時間倉促所以只能寫到這裡了。 如果您發現了我文章中的bug歡迎發email批評指正。非常感謝! 同時本方案也會成為我們開源社區linux52.com後台系統中的介面設計方案。 當然我們社區所有維護的文檔都會進行反覆驗證,如果出問題我們會及時更新。 以維護文檔的正確性。 點擊=這=里=查看文檔最新版本。

關鍵詞

php js rsa get jsonp 跨域 安全


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國