Laravel 框架加密解密如何實現 key 值多變的需求

houxin發表於2020-04-10

需求分析

我們有一臺總的資料處理伺服器,又有很多臺資料來源伺服器。為了讓他們相互通訊,我們在每個伺服器上搭建了相同的laravel網站框架。分伺服器之會和總伺服器進行資料互動。
同時,為了資料安全,總伺服器和分伺服器之間的資料傳輸使用laravel預設的encrypt加密和decrypt解密。
起初,為了資料能被正確解密,每一臺伺服器的key值要保持一致。但是隨之,問題也就來了。如果後期我們的一臺分伺服器資料洩露了的話,那麼就意味著,別人掌握了我們的所有的資料,有很大的安全隱患。所以,我們需要為每臺伺服器設定獨立的key值,而且又要保證資料能正確解密。

雖然非對稱加密是個很好的解決方案,但是要自己額外安裝依賴包,額外設定

原理分析

我通過對laravel的原始碼分析發現,encrypt()其實是在app啟動的時候,使用預設的key配置註冊了一次類。

public function register()
    {
        $this->app->singleton('encrypter', function ($app) {
            $config = $app->make('config')->get('app');

            // If the key starts with "base64:", we will need to decode the key before handing
            // it off to the encrypter. Keys may be base-64 encoded for presentation and we
            // want to make sure to convert them back to the raw bytes before encrypting.
            if (Str::startsWith($key = $this->key($config), 'base64:')) {
                $key = base64_decode(substr($key, 7));
            }

            return new Encrypter($key, $config['cipher']);
        });
    }

以上程式碼摘自\vendor\laravel\framework\src\Illuminate\Encryption\EncryptionServiceProvider.php

既然是這樣,我們就可以對該Encrypter類進行多次例項化來達成我們的目的。

解決方法

  • 我習慣在控制器Controllers目錄多生成一個Service目錄,來放置控制類中經常使用的類和方法。這次我們使用如下命令新建了EncryptService類。

    php artisan make:controller Service/EncryptService
  • 使用單例模式來構建。

    // 單例函式的例項化
      static private $instance;
    
      static public function getInstance(){
          if(!self::$instance){
              self::$instance = new self();
          }
          return self::$instance;
      }
      // 定義初始化
      private function __construct()
      {
          // If the key starts with "base64:", we will need to decode the key before handing
          // it off to the encrypter. Keys may be base-64 encoded for presentation and we
          // want to make sure to convert them back to the raw bytes before encrypting.
          // 如果沒有傳key的值,可以使用預設的key值
          $this->default();
      }
  • getEncrypter對配置進行判斷和處理,並new一個新的Encrypter類,如下:

      /*
       * 加密之前的驗證
       */
      private function getEncrypter(){
          if (Str::startsWith($this->key, 'base64:')) {
              $this->key = base64_decode(substr($this->key, 7));
          }
          $this->key = (string) $this->key;
          if (!static::supported($this->key, $this->cipher)) {
              throw new RuntimeException('The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths.');
          }
    
          return new Encrypter($this->key, $this->cipher);
      }
  • encrypt為例,使用$this->getEncrypter()方法來幫我們做加密中轉。

      /*
       * 定義公開的加密函式
       */
      public function encrypt($value, $serialize = true){
          // 處理之前進行一步驗證
          return $this->getEncrypter()->encrypt($value, $serialize);
      }
  • 使用setKey()來設定每次的加密的值,使用default()來恢復使用預設的值

      /*
       * 設定預設的屬性
       * 本類是一個單例模式的類,如果前期設定了配置,後期又想使用預設配置,你需要呼叫default方法才行
       */
      public function default(){
          $this->key = config('app.key');
          $this->cipher = config('app.cipher');
          return $this;
      }
      // 設定key的值
      public function setKey($key){
          $this->key = $key;
          return $this;
      }
  • 呼叫的時候,我們只需要對我們自己的EncryptService來操作就行了。使用setKey()來設定新的key的值。如下

    use App\Http\Controllers\Controller;
    use App\Http\Controllers\Service\EncryptService;
    class HomeController extends Controller
    {
        public function index(Content $content)
        {
            $encrypt = EncryptService::getInstance()->setKey('base64:uHNY0XQRxDLtQzl8ZWEMUDIbFGteGZA0o9BMP+L5sa8=')->encrypt('123');
            dd($encrypt);
        }
    }   

附錄

以下是類的完整原始碼,有不足的地方,還請多多指教。

<?php

namespace App\Http\Controllers\Service;

use RuntimeException;
use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Str;

class EncryptService
{
    // 定義加密引數
    private $key;
    // 定義加密方式
    private $cipher;

    // 單例函式的例項化
    static private $instance;

    static public function getInstance(){
        if(!self::$instance){
            self::$instance = new self();
        }
        return self::$instance;
    }
    // 定義初始化
    private function __construct()
    {
        // If the key starts with "base64:", we will need to decode the key before handing
        // it off to the encrypter. Keys may be base-64 encoded for presentation and we
        // want to make sure to convert them back to the raw bytes before encrypting.
        // 如果沒有傳key的值,可以使用預設的key值
        $this->default();
    }
    /*
     * 設定預設的屬性
     * 本類是一個單例模式的類,如果前期設定了配置,後期又想使用預設配置,你需要呼叫default方法才行
     */
    public function default(){
        $this->key = config('app.key');
        $this->cipher = config('app.cipher');
        return $this;
    }
    // 設定key的值
    public function setKey($key){
        $this->key = $key;
        return $this;
    }
    // 設定cipher的值
    public function setCipher($cipher){
        $this->cipher = $cipher;
        return $this;
    }
    /*
     * 定義公開的加密函式
     */
    public function encrypt($value, $serialize = true){
        // 處理之前進行一步驗證
        return $this->getEncrypter()->encrypt($value, $serialize);
    }
    /*
     * 直接加密
     */
    public function encryptString($value)
    {
        return $this->getEncrypter()->encrypt($value, false);
    }
    /*
     * 定義公開的解密函式
     */
    public function decrypt($payload, $unserialize = true){
        // 處理之前進行一步驗證
        return $this->getEncrypter()->decrypt($payload, $unserialize);
    }
    /*
     * 直接解密
     */
    public function decryptString($payload)
    {
        return $this->getEncrypter()->decrypt($payload, false);
    }
    /**
     * Determine if the given key and cipher combination is valid.
     *
     * @param  string  $key
     * @param  string  $cipher
     * @return bool
     */
    private function supported($key, $cipher)
    {
        $length = mb_strlen($key, '8bit');

        return ($cipher === 'AES-128-CBC' && $length === 16) ||
            ($cipher === 'AES-256-CBC' && $length === 32);
    }
    /*
     * 加密之前的驗證
     */
    private function getEncrypter(){
        if (Str::startsWith($this->key, 'base64:')) {
            $this->key = base64_decode(substr($this->key, 7));
        }
        $this->key = (string) $this->key;
        if (!static::supported($this->key, $this->cipher)) {
            throw new RuntimeException('The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths.');
        }

        return new Encrypter($this->key, $this->cipher);
    }
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章