国产+高潮+在线,国产 av 仑乱内谢,www国产亚洲精品久久,51国产偷自视频区视频,成人午夜精品网站在线观看

??基于webman 的全功能 api 接口簽名驗(yàn)證

0.0.25 版本
2024-04-01 版本更新時(shí)間
397 安裝
18 star

簡介

插件基于 http://www.wtbis.cn/plugin/83 修改而來,感謝原作者的無私提供。
由于我需要一些功能,原插件沒有,所以二次開發(fā)加了些功能。

功能如下:

1.新增replay防重放攻擊/請求
2.DatabaseDriver驅(qū)動改為默認(rèn)ThinkORM,并且新增 db_cache_time 參數(shù),查詢后按照app_key緩存應(yīng)用配置數(shù)據(jù)
3.新增應(yīng)用RSA非對稱加密功能,主要防止一成不變 app_secret
4.新增指定路由略過簽名驗(yàn)證
5.新增控制器內(nèi)$noNeedSign = ['login'],可指定action略過簽名驗(yàn)證
6.加密報(bào)文,防止中間人代理抓包獲取明文數(shù)據(jù)

安裝

composer require gitfei1231/webman-api-sign

使用

return [
    'enable' => true,

    /**
     * 配置 driver
     * 數(shù)組配置驅(qū)動   \Wengg\WebmanApiSign\Driver\ArrayDriver::class
     * 數(shù)據(jù)庫配置驅(qū)動 \Wengg\WebmanApiSign\Driver\DatabaseDriver::class (使用的是 ThinkORM)
     * 如需要自定義驅(qū)動,繼承 \Wengg\WebmanApiSign\Driver\BaseDriver::class
    */
    'driver' => \Wengg\WebmanApiSign\Driver\ArrayDriver::class,
    'encrypt' => 'sha256', //加密sign方式
    'timeout' => 60, //timestamp超時(shí)時(shí)間秒,0不限制
    'table' => 'app_sign', //表名

    /**
     * 防重放請求是否開啟 true只能請求一次,時(shí)間是上面 timeout內(nèi)
     * replay 主要借助與 timeout + noncestr隨機(jī)值進(jìn)行驗(yàn)證, 一定的時(shí)間內(nèi)noncestr如果重復(fù),那就判定重放請求
     * noncestr 建議生成隨機(jī)唯一UUID 或者你使用 13位時(shí)間戳+18位隨機(jī)數(shù)。1678159075243(13位)+隨機(jī)數(shù)(18位)
     */
    'replay' => false,

    /**
     * 如果使用 DatabaseDriver 需要緩存查詢后的數(shù)據(jù)
     * 設(shè)置緩存時(shí)間即可緩存對應(yīng)的app_id數(shù)據(jù)
     * db_cache_time => null 關(guān)閉緩存
     */
    'db_cache_time' => 604800, // null 關(guān)閉緩存

    //字段對照,可從(header,get,post)獲取的值
    'fields' => [
        'app_id'     => 'appId',     //app_id
        'app_key'    => 'appKey',    //app_key rsa加密才需要傳,appKey為前端隨機(jī)生成的app_secret秘鑰,用于加密sign和報(bào)文數(shù)據(jù)
        'timestamp'  => 'timestamp', //時(shí)間戳
        'noncestr'   => 'nonceStr',  //隨機(jī)字符串
        'signature'  => 'signature', //簽名字符串
    ],

    //driver為ArrayDriver時(shí)生效,對應(yīng)table
    'app_sign' => [
        [
            'app_id' => '1661408635', //應(yīng)用id
            'app_secret' => 'D81668E7B3F24F4DAB32E5B88EAE27AC', //應(yīng)用秘鑰
            'app_name' => '默認(rèn)', //應(yīng)用名稱
            'status' => 1, //狀態(tài):0=禁用,1=啟用
            'expired_at' => null, //過期時(shí)間,例如:2023-01-01 00:00:00,null不限制
            'encrypt_body' => 0, //狀態(tài):0=禁用,1=啟用 算法:aes-128-cbc 是否加密body傳入加密后的報(bào)文字符串,啟用RSA需要使用自動生成的app_secret進(jìn)行對稱加密,否則使用固定的app_secret進(jìn)行對稱加密
            'rsa_status' => 0, //狀態(tài):0=禁用,1=啟用 啟用RSA,主要用rsa加密隨機(jī)生成的app_secret,而不使用固定app_secret
            /**
             * sign私鑰 RS256加密
             */
            'private_key' => <<<EOD
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
EOD,
            /**
             * sign公鑰 RS256加密
             */
            'public_key' => <<<EOD
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
EOD
        ],
    ],
];

指定路由不需要簽名驗(yàn)證

不設(shè)置 setParams 或者 設(shè)置notSign為 false 都要經(jīng)過驗(yàn)證

// 此路由不經(jīng)過sign驗(yàn)證
Route::get('/login', [app\api\controller\LoginController::class, 'login'])->setParams(['notSign' => true]);

// 此路由經(jīng)過sign驗(yàn)證
Route::get('/login', [app\api\controller\LoginController::class, 'login'])->setParams(['notSign' => false]);

// 此路由經(jīng)過sign驗(yàn)證
Route::get('/login', [app\api\controller\LoginController::class, 'login']);
// 控制器中配置排除sign校驗(yàn)
class TestController
{
    /**
     * 無需sign校驗(yàn)
     * index、save不需要校驗(yàn) ['index','save']
     * 所有方法都不需要sign校驗(yàn) ['*'] 
     * @var string[]
     */
    protected $noNeedSign = ['login'];

    /**
     * 登錄
     */
    public function login()
    {

    }
}

開啟非對稱加密 rsa_status

注意:開啟后客戶端需自行隨機(jī)動態(tài)生成app_secret(不開啟則使用服務(wù)端固定的app_secret),用公鑰進(jìn)行加密app_secret,服務(wù)器端會進(jìn)行解密出app_secret, 生成sign進(jìn)行比對

非對稱加密算法為 RSAES-PKCS1-V1_5

  1. app_secret 客戶端自行生成
  2. sign使用自動生成的app_secret按照下面簽名算法客戶端計(jì)算出來
  3. 使用公鑰加密app_secret,通過header中的appKey字段進(jìn)行傳輸(未開啟rsa,此字段不用傳)

php端非對稱加密和解密代碼例子

<?php
require_once __DIR__ . '/../vendor/autoload.php';

//當(dāng)前庫
use Wengg\WebmanApiSign\Encryption\RSA;

///=================================下面是生成公鑰和私鑰================================///
$rsa = new RSA();

// 生成公鑰和私鑰
$data = $rsa->rsa_create();

echo "私鑰:\n";
echo $data['private_key'];
echo "\n";
echo "\n";
echo "公鑰:\n";
echo $data['public_key'];

///=================================下面是解密 加密=================================///

// 私鑰,可以使用 上面生成 $data['private_key']
$private_key = '-----BEGIN PRIVATE KEY-----
xxxxxxxxxxxxxx
-----END PRIVATE KEY-----
';

// 公鑰,可以使用 上面生成 $data['public_key']
$public_key = '-----BEGIN PUBLIC KEY-----
xxxxxxxxxxxxxx
-----END PUBLIC KEY-----
';

// 加密內(nèi)容
$str1 = "你好,二蛋!";

$str2 = $rsa->rsa_encode($str1, $public_key);

echo "加密內(nèi)容:\n";
echo $str2;
echo "\n";
echo "\n";

$res = $rsa->rsa_decode($str2, $private_key);

echo "解密內(nèi)容:\n";
echo $res;

js端非對稱加密和解密代碼例子

const rs = require('jsrsasign');
const fs = require('fs');

// 加載私鑰和公鑰
const private_key = fs.readFileSync('./key/private.pem', 'utf8');
const public_key = fs.readFileSync('./key/public.pem', 'utf8');

// 待加密的數(shù)據(jù)
const data = 'Hello, world!';

// 解析公鑰,將字符串轉(zhuǎn)換為 KeyObject 對象
const publicKeyObj = rs.KEYUTIL.getKey(public_key);

// 使用公鑰加密數(shù)據(jù)
const encryptedData = publicKeyObj.encrypt(data, 'RSAES-PKCS1-V1_5');

// 將加密后的數(shù)據(jù)轉(zhuǎn)換成 Base64 編碼
const base64CipherText = rs.hex2b64(encryptedData);

// 輸出加密后的數(shù)據(jù)
console.log(`加密后的數(shù)據(jù):${base64CipherText}`);

// 將 Base64 編碼的密文還原成二進(jìn)制數(shù)據(jù)
const binaryCiphertext = rs.b64tohex(base64CipherText);

// 解析私鑰,將字符串轉(zhuǎn)換為 KeyObject 對象
const privateKeyObj = rs.KEYUTIL.getKey(private_key);

// 使用私鑰解密數(shù)據(jù)
const decryptedData = privateKeyObj.decrypt(binaryCiphertext, 'RSAES-PKCS1-V1_5');

// 輸出解密后的數(shù)據(jù)
console.log(`解密后的數(shù)據(jù):${decryptedData}`);

開啟body報(bào)文加密 encrypt_body,非明文傳輸參數(shù)安全性更高(不加密get參數(shù))

注意:如果啟用的RSA,那么需使用自行隨機(jī)動態(tài)生成app_secret進(jìn)行對稱加密(否則使用服務(wù)端固定的app_secret進(jìn)行對稱加密)

app_secret秘鑰必須為32位,如:3ddc81a729c34c50b097a098b0512f16

接口使用https已經(jīng)可以達(dá)到報(bào)文加密的作用了,開發(fā)這個(gè)為啥?因?yàn)榉乐?“中間人”抓包,使用代理軟件抓包可以獲取https明文數(shù)據(jù)

1、開啟了rsa_status
  1. 把body傳輸?shù)膉son數(shù)據(jù)進(jìn)行轉(zhuǎn)為字符串
  2. 使用自動生成的app_secret作為密鑰進(jìn)行aes-128-cbc對稱加密
  3. 將加密后的字符串直接通過body進(jìn)行傳輸
2、未開啟rsa_status
  1. 把body傳輸?shù)膉son數(shù)據(jù)進(jìn)行轉(zhuǎn)為字符串
  2. 使用固定的app_secret作為密鑰進(jìn)行aes-128-cbc對稱加密
  3. 將加密后的字符串直接通過body進(jìn)行傳輸

php對稱加密代碼例子

<?php
namespace Wengg\WebmanApiSign\Encryption;

// 當(dāng)前庫就用的此類
class AES
{
    //$key秘鑰必須為32位,如:3ddc81a729c34c50b097a098b0512f16
    private $key; 
    private $method = 'aes-128-cbc';

    public function __construct($key)
    {
        $this->key = hex2bin($key);
    }

    /**
     * 加密
     * @param string $plaintext 加密內(nèi)容
     * @return string
     */
    public function encrypt(string $plaintext)
    {
        // 獲取加密算法要求的初始化向量的長度
        $ivlen = openssl_cipher_iv_length($this->method);
        // 生成對應(yīng)長度的初始化向量. aes-128模式下iv長度是16個(gè)字節(jié), 也可以自由指定.
        $iv = openssl_random_pseudo_bytes($ivlen);
        // 加密數(shù)據(jù)
        $ciphertext = openssl_encrypt($plaintext, $this->method, $this->key, OPENSSL_RAW_DATA, $iv);

        return base64_encode($iv . $ciphertext);
    }

    /**
     * 解密
     * @param string $ciphertext 加密內(nèi)容
     * @return string
     */
    public function decrypt($ciphertext)
    {
        $ciphertext = base64_decode($ciphertext);
        $ivlen = openssl_cipher_iv_length($this->method);
        $iv = substr($ciphertext, 0, $ivlen);
        $ciphertext = substr($ciphertext, $ivlen);

        $plaintext = openssl_decrypt($ciphertext, $this->method, $this->key, OPENSSL_RAW_DATA, $iv);
        return $plaintext;
    }
}

JS端對稱加密/解密類,可與本庫加解密互通

const CryptoJS = require("crypto-js");

class AES {
  constructor(key) {
    //key秘鑰必須為32位,如:3ddc81a729c34c50b097a098b0512f16
    this.key = key;
    this.method = "aes-128-cbc";
  }

  encrypt(plaintext) {
    const iv = CryptoJS.lib.WordArray.random(16);
    const ciphertext = CryptoJS.AES.encrypt(
      plaintext,
      this.key,
      { iv: iv, padding: CryptoJS.pad.Pkcs7, mode: CryptoJS.mode.CBC }
    );
    return iv.concat(ciphertext.ciphertext).toString(CryptoJS.enc.Base64);
  }

  decrypt(ciphertext) {
    ciphertext = CryptoJS.enc.Base64.parse(ciphertext);
    const iv = ciphertext.clone();
    iv.sigBytes = 16;
    iv.clamp();
    ciphertext.words.splice(0, 4); // remove IV from ciphertext
    ciphertext.sigBytes -= 16;
    const decrypted = CryptoJS.AES.decrypt(
      { ciphertext: ciphertext },
      this.key,
      { iv: iv, padding: CryptoJS.pad.Pkcs7, mode: CryptoJS.mode.CBC }
    );
    const plaintext = decrypted.toString(CryptoJS.enc.Utf8);
    return plaintext;
  }

  toHex() {
    return this.key.toString(CryptoJS.enc.Hex);
  }

  static fromHex(hexString) {
    return new AES(CryptoJS.enc.Hex.parse(hexString));
  }

  static fromBase64(base64String) {
    return new AES(CryptoJS.enc.Base64.parse(base64String));
  }

  toBase64() {
    return this.key.toString(CryptoJS.enc.Base64);
  }
}

// 秘鑰
const keyHex = '0123456789abcdef0123456789abcdef';
const aes = AES.fromHex(keyHex);

// 加密使用示例
const plaintext = '你好,二蛋!';
const encrypted = aes.encrypt(plaintext);
console.log("加密內(nèi)容:", encrypted);

// 解密使用示例
const str = aes.decrypt(encrypted);
console.log("解密內(nèi)容:", str);

其他客戶端語言加解密可以參考上面php/js端類進(jìn)行編寫

再support/Request.php新增方法

class Request extends \Webman\Http\Request
{
    /**
     * 設(shè)置post參數(shù)
     * @author tangfei <957987132@qq.com> 2023-03-07
     * @param array $data
     * @return void
     */
    public function setPostData(array $data)
    {
        if(empty($data)){ return; }
        $this->post();

        foreach ($data as $key => $value) {
            $this->_data['post'][$key] = $value;
        }
    }
}

簽名計(jì)算

注意:簽名數(shù)據(jù)除業(yè)務(wù)參數(shù)外需加上app_key,timestamp,nonceStr對應(yīng)的字段數(shù)據(jù),上面的加密報(bào)文和簽名sign計(jì)算不相干,sign還是按照傳輸?shù)淖侄芜M(jìn)行計(jì)算,密文到后端會轉(zhuǎn)為字段后再進(jìn)行sign計(jì)算

  1. 簽名數(shù)據(jù)先按照鍵名升序排序
  2. 使用 & 鏈接簽名數(shù)組(參數(shù)不轉(zhuǎn)義,空數(shù)據(jù)不參與加密),再在尾部加上app_secret
  3. 再根據(jù)配置的加密方式 hash() 簽名數(shù)據(jù)

示例

排序前

{
    "a": "1",
    "b": [
        "你好世界",
        "abc123"
    ],
    "c": {
        "e": [],
        "d": "hello"
    },
    "appId": "1661408635",
    "timestamp": "1662721474",
    "nonceStr": "ewsqam"
}

排序后

{
    "a": "1",
    "appId": "1661408635",
    "b": [
        "你好世界",
        "abc123"
    ],
    "c": {
        "d": "hello",
        "e": []
    },
    "nonceStr": "ewsqam",
    "timestamp": "1662721474"
}

鏈接后

a=1&appId=1661408635&b[0]=你好世界&b[1]=abc123&c[d]=hello&nonceStr=ewsqam&timestamp=1662721474D81668E7B3F24F4DAB32E5B88EAE27AC

加密

// $data = sortData($data);
// $str = urldecode(http_build_query($data)) . $key;
// $signature = hash('sha256', $str);
$signature = hash('sha256', 'a=1&appId=1661408635&b[0]=你好世界&b[1]=abc123&c[d]=hello&nonceStr=ewsqam&timestamp=1662721474D81668E7B3F24F4DAB32E5B88EAE27AC');

方法一:提供一個(gè)js http_build_query 比較高效的寫法

// 該函數(shù)的實(shí)現(xiàn)方式和 PHP 中的 http_build_query
function http_build_query(data, prefix = null) {
  const queryParts = [];

  for (const [key, value] of Object.entries(data)) {
    // 處理數(shù)組和對象
    if (typeof value === "object" && value !== null) {
      // 判斷是否為空數(shù)組或空對象
      if (Array.isArray(value) && value.length === 0) {
        continue;
      }
      if (Object.prototype.toString.call(val[k]) === '[object Object]' && Object.keys(value).length === 0) {
        continue;
      }
      const newPrefix = prefix ? `${prefix}[${encodeURIComponent(key)}]` : encodeURIComponent(key);
      queryParts.push(http_build_query(value, newPrefix));
    }
    // 處理 true 值
    else if (value === true) {
      const encodedKey = encodeURIComponent(prefix ? `${prefix}[${encodeURIComponent(key)}]` : encodeURIComponent(key));
      queryParts.push(`${encodedKey}=1`);
    } 
    // 處理 false 值
    else if (value === false) {
      const encodedKey = encodeURIComponent(prefix ? `${prefix}[${encodeURIComponent(key)}]` : encodeURIComponent(key));
      queryParts.push(`${encodedKey}=0`); // 加上等號
    }
    // 處理 null 值
    else if (value === null) {
      // 空值直接跳過
      continue;
      // const encodedKey = encodeURIComponent(prefix ? `${prefix}[${encodeURIComponent(key)}]` : encodeURIComponent(key));
      // queryParts.push(`${encodedKey}=`); // 加上等號
    }
    // 處理普通值
    else {
      const encodedKey = encodeURIComponent(prefix ? `${prefix}[${encodeURIComponent(key)}]` : encodeURIComponent(key));
      const encodedValue = encodeURIComponent(value);
      queryParts.push(`${encodedKey}=${encodedValue}`);
    }
  }

  return queryParts.join('&');
}

方法二:提供一個(gè)locutus庫中 js 仿php http_build_query 寫法

修復(fù)了該庫的方法在有空數(shù)組空對象時(shí)多出連續(xù)&符號問題

function urldecode (str) {
  return decodeURIComponent((str + '')
    .replace(/%(?![\da-f]{2})/gi, function () {
      // PHP tolerates poorly formed escape sequences
      return '%25'
    })
    .replace(/\+/g, '%20'))
}

function rawurldecode (str) {
  return decodeURIComponent((str + '')
    .replace(/%(?![\da-f]{2})/gi, function () {
      // PHP tolerates poorly formed escape sequences
      return '%25'
    }))
}

function urlencode (str) {
  str = (str + '')
  return encodeURIComponent(str)
    .replace(/!/g, '%21')
    .replace(/'/g, '%27')
    .replace(/\(/g, '%28')
    .replace(/\)/g, '%29')
    .replace(/\*/g, '%2A')
    .replace(/~/g, '%7E')
    .replace(/%20/g, '+')
}

function rawurlencode (str) {
  str = (str + '')
  return encodeURIComponent(str)
    .replace(/!/g, '%21')
    .replace(/'/g, '%27')
    .replace(/\(/g, '%28')
    .replace(/\)/g, '%29')
    .replace(/\*/g, '%2A')
}

function http_build_query (formdata, numericPrefix, argSeparator, encType) {
  let encodeFunc

  switch (encType) {
    case 'PHP_QUERY_RFC3986':
      encodeFunc = rawurlencode
      break

    case 'PHP_QUERY_RFC1738':
    default:
      encodeFunc = urlencode
      break
  }

  let value
  let key
  const tmp = []

  var _httpBuildQueryHelper = function (key, val, argSeparator) {
    let k
    const tmp = []
    if (val === true) {
      val = '1'
    } else if (val === false) {
      val = '0'
    }
    if (val !== null) {
      if (typeof val === 'object') {
        for (k in val) {
          if (val[k] !== null) {
            // 判斷是否為空數(shù)組或空對象
            if (Array.isArray(val[k]) && val[k].length === 0) {
              continue;
            }
            if (Object.prototype.toString.call(val[k]) === '[object Object]' && Object.keys(val[k]).length === 0) {
              continue;
            }
            tmp.push(_httpBuildQueryHelper(key + '[' + k + ']', val[k], argSeparator))
          }
        }
        return tmp.join(argSeparator)
      } else if (typeof val !== 'function') {
        return encodeFunc(key) + '=' + encodeFunc(val)
      } else {
        throw new Error('There was an error processing for http_build_query().')
      }
    } else {
      return ''
    }
  }

  if (!argSeparator) {
    argSeparator = '&'
  }
  for (key in formdata) {
    value = formdata[key]
    if (numericPrefix && !isNaN(key)) {
      key = String(numericPrefix) + key
    }
    const query = _httpBuildQueryHelper(key, value, argSeparator)
    if (query !== '') {
      tmp.push(query)
    }
  }

  return tmp.join(argSeparator)
}

提供一個(gè)js sortData 排序方法

function sortData(data, sortOrder = "asc") {
  const compareFunction = (a, b) => {
    if (a === b) {
      return 0;
    }
    return sortOrder === "desc" ? (a > b ? -1 : 1) : (a < b ? -1 : 1);
  };

  if (Array.isArray(data)) {
    return Object.keys(data).sort(compareFunction).map((value) =>{
      value = data[value];
      return typeof value === "object" && value !== null
      ? sortData(value, sortOrder)
      : value
    });
  }

  if (typeof data === "object" && data !== null) {
    const sortedObject = {};
    const sortedKeys = Object.keys(data).sort(compareFunction);

    for (const key of sortedKeys) {
      sortedObject[key] =
        typeof data[key] === "object" && data[key] !== null
          ? sortData(data[key], sortOrder)
          : data[key];
    }

    return sortedObject;
  }

  return data;
}

提供一個(gè)php sortData 排序方法

/**
 * 數(shù)據(jù)排序
 * @param array $data
 * @return array
 */
public function sortData(array $data)
{
    $sort = function (array &$data) use (&$sort) {
        ksort($data);
        foreach ($data as &$value) {
            if (is_array($value)) {
                $sort($value);
            }
        }
    };
    $sort($data);
    return $data;
}

測試js版本 http_build_query + sortData 對比 php版本 http_build_query + sortData

js版本使用的是上面 http_build_query方法(上面) + sortData方法(上面) + urldecode方法(上面)

php版本使用的http_build_query + sortData(上面) + urldecode

console.log(urldecode(http_build_query(sortData({
  "n": "ewsqam3",
  "t": "1677933143",
  "a": "1661408635",
  "members": {
      "p":{},
      "o":[],
      "f": "",
      "a": null,
      "b": 1,
      "c": false,
      "d": true,
      "members": {
          "c": true,
          "a": 0,
          "b": false,
          "members": {
              "s": false,
              "e": true,
              "f": 0,
              "g": null,
              "j": ""
          }
      }
  }
}))))

// js結(jié)果
a=1661408635&members[b]=1&members[c]=0&members[d]=1&members[f]=&members[members][a]=0&members[members][b]=0&members[members][c]=1&members[members][members][e]=1&members[members][members][f]=0&members[members][members][j]=&members[members][members][s]=0&n=ewsqam3&t=1677933143

//php結(jié)果
a=1661408635&members[b]=1&members[c]=0&members[d]=1&members[f]=&members[members][a]=0&members[members][b]=0&members[members][c]=1&members[members][members][e]=1&members[members][members][f]=0&members[members][members][j]=&members[members][members][s]=0&n=ewsqam3&t=1677933143

效果圖(可選)

贊助商