2020年10月

在自己电脑(Windows)开发测试代码都没问题,但一上生产环境就报错了。经过对比,本机和服务器的PHP版本和OpenSSL版本不一样,猜测可能是这个原因导致的。经过一番查找,找到了从代码上解决问题的办法,规避了调整生产服务器的风险。

报错的代码

/**
 * 字符串加密(加密方法:DES-ECB)
 * @param string $data 待加密字符串
 * @param string $key 对称加密密钥
 * @return string
 */
function encryptData(string $data, string $key)
{
    // 获取密码iv长度
    $length = openssl_cipher_iv_length('DES-ECB');
    // 生成一个伪随机字节串
    $iv = openssl_random_pseudo_bytes($length);
    // 加密数据
    $ciphertext = openssl_encrypt($data, 'DES-ECB', $key, OPENSSL_RAW_DATA, $iv);

    // 把包含数据的二进制字符串转换为十六进制值,然后返回结果
    return bin2hex($ciphertext);
}

报错信息

Fatal error: Uncaught Error: Length must be greater than 0 in frame.php:87
Stack trace: 
    #0 frame.php(87): openssl_random_pseudo_bytes()
    #1 frame.php(60): encryptData()
    #2 {main} thrown in frame.php on line 87

根据报错信息,主要问题是openssl_cipher_iv_length()返回的长度为0,而openssl_random_pseudo_bytes()的参数的长度必须大于0,所以就产生了报错。
但是openssl_cipher_iv_length()为什么返回0呢?难道是不支持DES-ECB加密方法?
使用openssl_get_cipher_methods()方法获取可用的加密算法的列表,发现DES-ECB在列表内,那应该是支持的!
这时候,我就猜想是不是openssl低版本的BUG,因为以前也见过一些因为openssl版本过低导致的问题,于是继续Google一番。
经过查找,发现了两条有用的结果:

在第一条中得知ECB 加密模式是不安全的,因为它没有初始化矢量openssl_cipher_iv_length()返回的长度为0的原因就得知了。

但结论没有经过仔细论证。也不知道为什么java为什么要ECB。
在第二条中找到了调整代码的思路。
最终得到了以下没有报错的代码~
/**
 * 字符串加密(加密方法:DES-ECB)
 * @param string $data 待加密字符串
 * @param string $key 对称加密密钥
 * @return string
 */
function encryptData(string $data, string $key)
{
    // 获取密码iv长度
    $length = openssl_cipher_iv_length('DES-ECB');
    // 生成一个伪随机字节串
    if ($length > 0) {
        $iv = openssl_random_pseudo_bytes($length);
    } else {
        $iv = '';
    }
    // 加密数据
    $ciphertext = openssl_encrypt($data, 'DES-ECB', $key, OPENSSL_RAW_DATA, $iv);

    // 把包含数据的二进制字符串转换为十六进制值,然后返回结果
    return bin2hex($ciphertext);
}

在制作项目中,难免会遇到有跨域问题,需要增加指定响应头来满足跨域的需求。但ThinkPHP5.1版本的手册中,对跨域怎么设置提供的方法比较局限,所以这里经过研究,总结出了几种办法,推荐使用第三种。

一、路由

这方法是手册当中介绍的,这里简单复制下,深入了解可以查看手册

如果某个路由或者分组需要支持跨域请求,可以使用

Route::get('new/:id', 'News/read')->ext('html')->allowCrossDomain();
Route::group('index', function() {
    Route::get('new/:id', 'News/read');
})->prefix('index/')->allowCrossDomain();

这方法,仅适合定义了路由的情况下使用,如果是默认路由,这方法不适用。

二、header()函数

可以在入口文件index.php、公共函数文件common.php等文件里使用header()函数定义跨域响应头。代码如下:

header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE');
header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With');
header('Access-Control-Allow-Origin: *');
全局允许跨域的话,在入口文件或全局公共函数文件里增加跨域代码
单模块允许跨域的话,在模块公共函数文件里增加跨域代码
单控制器允许跨域的话,在控制器文件里增加跨域代码(命名空间与类声明之间区域)
单方法允许跨域的话,在方法代码开头增加跨域代码。或者使用路由设置跨域。

这个方法可能会存在响应头被覆盖的问题(框架输出响应内容时,设置了相同的响应头,后设置覆盖前设置)

三、中间件

可以使用中间件,更改响应输出内容。这种方法适合全局或部分模块使用。
创建application/http/middleware/AllowCrossDomain.php文件,文件内容如下:

注意修改允许跨域的域名
<?php
namespace app\http\middleware;

/**
 * 跨域中间件
 * @package app\http\middleware
 */
class AllowCrossDomain
{
    /**
     * @param \think\Request $request
     * @param \Closure $next
     * @return mixed|\think\Response
     */
    public function handle($request, \Closure $next)
    {
        // 允许跨域的域名
        $allowOriginDomain = ['www.kancloud.cn', 'll00.cn'];
        // HTTP请求头中的Origin
        $origin = $request->header('origin', '');
        // 附加响应头
        $header = [];

        if (!empty($origin)) {
            $domain = explode('://', $origin)[1] ?? '';
            if (in_array($domain, $allowOriginDomain)) {
                $header['Access-Control-Allow-Credentials'] = 'true';
                $header['Access-Control-Allow-Methods']     = 'GET, POST, PATCH, PUT, DELETE';
                $header['Access-Control-Allow-Headers']     = 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With';
                $header['Access-Control-Allow-Origin']      = '*';
            }
        }

        return $next($request)->header($header);
    }
}

application/middleware.phpapplication/index/middleware.php文件加入

\app\http\middleware\AllowCrossDomain::class

示例:

<?php
return [
    // 允许跨域
    \app\http\middleware\AllowCrossDomain::class,
    // 登录认证
    //\app\wxamp\middleware\CheckLogin::class,
    // 权限验证
    //\app\wxamp\middleware\CheckPermission::class,
];

四、输出响应对象实例设置

可以在json()jsonp()xml()等函数里设置跨域响应头,如:

return json(
    [
        'code' => 0,
        'msg'  => '操作成功',
    ],
    200,
    [
        'Access-Control-Allow-Credentials' => 'true',
        'Access-Control-Allow-Methods'     => 'GET, POST, PATCH, PUT, DELETE',
        'Access-Control-Allow-Headers'     => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With',
        'Access-Control-Allow-Origin'      => '*',
    ]
);