php7实现JWT认证

class JwtAuth
{
    //交换密钥
    private static $key = '123456';

    //头部信息
    private static $header = [
        'alg' => 'HS256', //算法名称
        'typ' => 'JWT', //令牌类型
    ];

    //加密方式
    private static $hash_algos = [
        'HS256' => 'sha256',
    ];

    /**
     * 获取 jwt token
     * @param array $payload jwt载荷,格式如下非必须
     * [
     * 'iss'=>'jwt_admin', //该JWT的签发者
     * 'iat'=>time(), //签发时间
     * 'exp'=>time()+7200, //过期时间
     * 'nbf'=>time()+60, //该时间之前不接收处理该Token
     * 'sub'=>'www.admin.com', //面向的用户
     * 'jti'=>md5(uniqid('JWT').time()) //该Token唯一标识
     * ]
     * @return bool|string
     */
    public static function getToken($payload)
    {
        if (is_array($payload)) {
            $base64header = self::base64UrlEncode(json_encode(self::$header, JSON_UNESCAPED_UNICODE));
            $base64payload = self::base64UrlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
            $sign = self::signature($base64header . '.' . $base64payload, self::$header['alg']);
            return $base64header . '.' . $base64payload . '.' . $sign;
        }
        return false;
    }

    /**
     * 验证token是否有效,默认验证exp,nbf,iat时间
     * @param string $token 需要验证的token
     * @return bool|string
     */
    public static function verifyToken($token)
    {
        $tokens = explode('.', $token);
        if (count($tokens) != 3) {
            return false;
        }

        list($base64header, $base64payload, $sign) = $tokens;

        //获取算法
        $header = json_decode(self::base64UrlDecode($base64header), JSON_OBJECT_AS_ARRAY);
        if (empty($header['alg'])) {
            return false;
        }

        //签名验证
        if ($sign != self::signature($base64header . '.' . $base64payload, $header['alg'])) {
            return false;
        }

        $payload = json_decode(self::base64UrlDecode($base64payload), JSON_OBJECT_AS_ARRAY);

        //签发时间大于当前服务器时间验证失败
        if (isset($payload['iat']) && $payload['iat'] > time()) {
            return false;
        }

        //过期时间小宇当前服务器时间验证失败
        if (isset($payload['exp']) && $payload['exp'] < time()) {
            return false;
        }

        //该nbf时间之前不接收处理该Token
        if (isset($payload['nbf']) && $payload['nbf'] > time()) {
            return false;
        }

        return $payload;
    }

    /**
     * base64UrlEncode https://jwt.io/ 中base64UrlEncode编码实现
     * @param string $input 需要编码的字符串
     * @return string
     */
    private static function base64UrlEncode($input)
    {
        return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
    }

    /**
     * base64UrlDecode https://jwt.io/ 中base64UrlEncode解码实现
     * @param string $input 需要解码的字符串
     * @return bool|string
     */
    private static function base64UrlDecode($input)
    {
        $remainder = strlen($input) % 4;
        if ($remainder) {
            $addlen = 4 - $remainder;
            $input .= str_repeat('=', $addlen);
        }
        return base64_decode(strtr($input, '-_', '+/'));
    }

    /**
     * HMACSHA256签名 https://jwt.io/ 中HMACSHA256签名实现
     * @param string $input 为base64UrlEncode(header).'.'.base64UrlEncode(payload)
     * @param string $alg  算法方式
     * @return mixed
     */
    private static function signature($input, $alg = 'HS256')
    {
        return self::base64UrlEncode(
            hash_hmac(self::$hash_algos[$alg], $input, self::$key, true)
        );
    }
}

//测试和官网示例是否匹配
$payload = ['sub' => '1234567890', 'name' => 'John Doe', 'iat' => 1516239022];
$token = JwtAuth::getToken($payload);

//对token进行验证签名
$getPayload = JwtAuth::verifyToken($token);

echo $token;
echo "\n\n";
var_dump($getPayload);

用Matomo/Piwik定时导入日志实现统计网站访问情况

安装并配置piwik以后,就需要导入access.log中的访问数据了,

首先需要一个导入日志的脚本,这个脚本运行时,需要把当前access.log中的日志导入pwiki,同时再把导入过的备份到别的地方。脚本如下:

#!/bin/sh

LOG_DIR=/var/log/nginx

LOG_ACCESS=$LOG_DIR/access.log
LOG_IMPORTING=$LOG_DIR/access.log.importing
LOG_IMPORTED=$LOG_DIR/imported.log

# move current log file to a tmp file, and restart log server
mv $LOG_ACCESS $LOG_IMPORTING 2>/dev/null
kill -USR1 $(cat /var/run/nginx.pid 2>/dev/null) 2>/dev/null

# import temp log file
/www/piwik/misc/log-analytics/import_logs.py \
    --idsite=1 \
    --url=piwik.rehiy.com \
    --log-format-name=ncsa_extended \
    $LOG_IMPORTING

# join importing log to imported
cat $LOG_IMPORTING >>$LOG_IMPORTED

# remove temp file
rm $LOG_IMPORTING 2>/dev/null

最后还需要配置cron每个小时运行一次导入脚本,再配置logrotate不要处理access.log。

PHP版百度云加速API/SDK封装

百度云加速API参考文档 https://su.baidu.com/help/index.html#/7_kaifazhinan/2_APIcankao-NEW/2_wangzhanjieru/2.1.1_tianjiayuming.md

<?php

/**
 * Author: anrip <https://www.arnip.com>
 * Update: 2021-04-13
 */

class Yunjiasu
{
    private $su;

    private $name = '';
    private $zone = [];

    public function __construct($domain, $access_key, $secret_key)
    {
        $this->su = new YunjiasuCore($access_key, $secret_key);
        $this->zone = $this->su->zones(['name' => $domain]);
        $this->name = $domain;
    }

    public function __call($name, $arguments)
    {
        array_unshift($arguments, $this->zone['id']);
        return call_user_func_array(array($this->su, $name), $arguments);
    }

    public function dns_records($data = [])
    {
        $list = $this->su->dns_records($this->zone['id']);
        if (empty($list) || empty($data)) {
            return $list;
        }
        return array_filter(
            $list,
            function ($item) use ($data) {
                isset($data['name']) && $data['name'] .= '.' . $this->name;
                return $data === array_intersect_assoc($item, $data);
            }
        );
    }

    public function dns_records_delete($data)
    {
        return array_map(
            function ($rs) {
                return $this->su->dns_records_delete($this->zone['id'], $rs['id']);
            },
            $this->dns_records($data)
        );
    }
}

class YunjiasuCore
{
    private $api_base = 'https://api.su.baidu.com/';

    private $access_key = '';
    private $secret_key = '';

    public function __construct($access_key, $secret_key)
    {
        $this->access_key = $access_key;
        $this->secret_key = $secret_key;
    }

    ////////////////////////////////////////////////////////////////

    public function zones($data)
    {
        $path = 'zones';
        return $this->api_call('GET', $path, $data);
    }

    ////////////////////////////////////////////////////////////////

    public function dns_records($zone_id)
    {
        $path = 'zones/' . $zone_id . '/dns_records';
        return $this->api_call('GET', $path);
    }

    public function dns_records_insert($zone_id, $data)
    {
        $path = 'zones/' . $zone_id . '/dns_records';
        return $this->api_call('POST', $path, $data);
    }

    public function dns_records_update($zone_id, $data)
    {
        $path = 'zones/' . $zone_id . '/dns_records';
        return $this->api_call('PATCH', $path, $data);
    }

    public function dns_records_delete($zone_id, $id)
    {
        $path = 'zones/' . $zone_id . '/dns_records/' . $id;
        return $this->api_call('DELETE', $path);
    }

    ////////////////////////////////////////////////////////////////

    public function purge_cache($zone_id, $data)
    {
        $path = 'zones/' . $zone_id . '/purge_cache';
        return $this->api_call('DELETE', $path, $data);
    }

    ////////////////////////////////////////////////////////////////

    private function api_call($method, $path, $data = NULL)
    {
        $path = 'v31/yjs/' . $path;

        print_r("\n> " . $method . ' /' . $path);

        $url = $this->api_base . $path;
        $header = $this->api_header($path, $data);
        $result = $this->http_repuest($method, $url, $header, $data);

        if (!empty($result['errors'])) {
            $error = json_encode($result['errors']);
            throw new Exception($error);
        }

        if (!empty($result['result'])) {
            return $result['result'];
        }

        if (!empty($result['success'])) {
            return ['success' => true];
        }

        return $result;
    }

    private function api_header($path, $data = NULL)
    {
        $params = [
            'X-Auth-Access-Key' => $this->access_key,
            'X-Auth-Nonce' => uniqid(),
            'X-Auth-Path-Info' => $path,
            'X-Auth-Signature-Method' => 'HMAC-SHA1',
            'X-Auth-Timestamp' => time(),
        ];

        if (is_array($data)) {
            $params = array_merge($params, $data);
        }

        ksort($params);

        $header = $signls = [];

        foreach ($params as $k => $v) {
            if (is_bool($v)) {
                $v = $v ? 'true' : 'false';
            }
            if (is_array($v)) {
                $v = str_replace('","', '", "', json_encode($v, JSON_UNESCAPED_SLASHES));
            }
            if (strpos($k, 'X-Auth') === 0) {
                $header[] = $k . ':' . $v;
            }
            if ($v !== '') {
                $signls[] = $k . '=' . $v;
            }
        }

        $header[] = 'X-Auth-Sign:' . base64_encode(
            hash_hmac('sha1', implode('&', $signls), $this->secret_key, true)
        );

        return $header;
    }

    ////////////////////////////////////////////////////////////////

    private function http_repuest($method, $url, $header = NULL, $body = NULL)
    {
        $ch = curl_init();

        if ($method == 'GET' && $body) {
            $url .= '?' . http_build_query($body);
            $body = NULL;
        }

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);

        if ($header) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
        }

        if ($body) {
            if (is_array($body)) {
                $body = json_encode($body);
            }
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        }

        $result = curl_exec($ch);
        $errno = curl_errno($ch);
        $error = curl_error($ch);

        curl_close($ch);

        if ($errno) {
            return ['error' => $errno, 'message' => $error];
        }

        return json_decode($result, true);
    }
}

手动删除Matomo/Piwik历史日志(MySQL删除大量数据)

不知不觉间,Piwik数据的容量已经达到500GB,虽然并没有感觉到性能有什么影响,但也是时候考虑如何清理其最占空间的访问记录数据了。于是决定拿备份库演练一番:

首先想到的是使用后台的自动清理功能,但是发现其每次10000条的删除能力,对于此等量级的数据可谓是杯水车薪。最后只能翻看源码,得出最基本的两条删除语句。

DELETE FROM `wk_log_conversion` WHERE server_time < '2021-01-01 00:00:00';
DELETE FROM `wk_log_visit` WHERE visit_last_action_time < '2021-01-01 00:00:00';
DELETE FROM `wk_log_link_visit_action` WHERE server_time < '2021-01-01 00:00:00';

至于 wk_log_action 这个表里是被引用的内容,下次遇到相同的引用内容,仍然需要添加一条记录。不删除反而可以确保idaction的唯一性。

至此,我们将遇到第二个问题:

如果要从InnoDB大表中删除许多行,则可能会超出表的锁定表大小InnoDB。也许删除500G的数据需要8个小时左右。

为了避免这个问题,或者只是为了最小化表保持锁定的时间,官方给出以下策略(根本不使用 DELETE):

  • 选择不要删除的行到与原始表具有相同结构的空表中

    INSERT INTO t_copy SELECT * FROM t WHERE ... ;
  • 使用RENAME TABLE以原子移动原始表的方式进行,并重新命名拷贝到原来的名称

    RENAME TABLE t TO t_old, t_copy TO t;
  • 删除原始表

    DROP TABLE t_old;

最终,我们优化查询如下

CREATE TABLE wk_log_conversion_copy LIKE wk_log_conversion;
INSERT INTO wk_log_conversion_copy SELECT * FROM wk_log_conversion WHERE server_time >= '2021-01-01 00:00:00';
RENAME TABLE wk_log_conversion TO wk_log_conversion_old, wk_log_conversion_copy TO wk_log_conversion;

CREATE TABLE wk_log_visit_copy LIKE wk_log_visit;
INSERT INTO wk_log_visit_copy SELECT * FROM wk_log_visit WHERE visit_last_action_time >= '2021-01-01 00:00:00';
RENAME TABLE wk_log_visit TO wk_log_visit_old, wk_log_visit_copy TO wk_log_visit;

CREATE TABLE wk_log_link_visit_action_copy LIKE wk_log_link_visit_action;
INSERT INTO wk_log_link_visit_action_copy SELECT * FROM wk_log_link_visit_action WHERE server_time >= '2021-01-01 00:00:00';
RENAME TABLE wk_log_link_visit_action TO wk_log_link_visit_action_old, wk_log_link_visit_action_copy TO wk_log_link_visit_action;

DROP TABLE wk_log_conversion_old, wk_log_visit_old, wk_log_link_visit_action_old;

官方文档链接 https://matomo.org/faq/how-to/faq_20184/

解决iOS拍照上传图片旋转问题

html5应用,可以直接使用file功能调用相机拍照并上传,但是在iOS上有个奇葩的问题,图片不会自动翻转,上传到服务器上的图片可能是倒立的。

解决此问题有2种思路:

1.使用客户端JS检测图片信息,旋转后再上传。此方法实现需消耗客户端资源。

懒得整理js代码了,暂时按下不表。

2.使用服务端PHP检测图片信息,旋转后保存。此方法需要消耗少量的服务器资源。

function correct_image_orientation($target) {
    if(!function_exists('exif_read_data')) {
        return false;
    }
    $exif = exif_read_data($target);
    if($exif && isset($exif['Orientation']) && $exif['Orientation'] != 1) {
        switch ($exif['Orientation']) {
            case 3: $deg = 180; break;
            case 6: $deg = 270; break;
            case 8: $deg = 90; break;
            default: $deg = 0;
        }
        if($deg > 0) {
            $img = imagecreatefromjpeg($target);
            $img = imagerotate($img, $deg, 0);
            imagejpeg($img, $target, 95);
        }
    }
}

纯CSS3实现瀑布流效果

1、该效果使用CSS3的column-width实现,和js版的瀑布流不同:图片将纵向排列。

2、代码中使用了一小段JS,和瀑布流效果无关,主要用来动态插入元素,并实现模拟翻页。

<!Doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>瀑布流(html5,css3,column) - by http://www.rehiy.com</title>
<style>
* {
    padding: 0;
    margin: 0;
}
#waterfall {
    margin: 15px 15px -15px 15px;
    position: relative;
    -webkit-column-width: 200px;
       -moz-column-width: 200px;
            column-width: 200px;
}
#waterfall div {
    width: 100%;
    background: #eee;
    margin-bottom: 15px;
    display: inline-block;
}
</style>
</head>
<body>
<div id="waterfall"></div>
<input type="button" onclick="more()" value="加载更多..."/>
<script type="text/javascript">
    var i = 0;
    function more() {
        var w = document.getElementById('waterfall');
        for(var n = i + 30; i < n; i++) {
            height = Math.floor( Math.random()*200 + 200 );
            w.innerHTML += '<div style="height:' + height + 'px;">'+i+'</div>';
        }
    }
    more();
</script>
</body>
</html>

js中escape、encodeURI、encodeURIComponent的区别

三个函数均可把字符串作为 URI 组件进行编码。

escape/unescape

该方法不会对 ASCII 字母和数字进行编码,也不会对下面这些 ASCII 标点符号进行编码: * @ – _ + . / 。其他所有的字符都会被转义序列替换。

ECMAScript v3 反对使用该方法,应用使用 encodeURI() 和 encodeURIComponent() 替代它。

encodeURI/decodeURI

该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: – _ . ! ~ * ‘ ( ) 。

该方法的目的是对 URI 进行完整的编码,因此对以下在 URI 中具有特殊含义的 ASCII 标点符号,encodeURI() 函数是不会进行转义的:;/?:@&=+$,#

如果 URI 组件中含有分隔符,比如 ? 和 #,则应当使用 encodeURIComponent() 方法分别对各组件进行编码。

encodeURIComponent/decodeURIComponent

该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: – _ . ! ~ * ‘ ( ) 。
其他字符(比如 :;/?:@&=+$,# 这些用于分隔 URI 组件的标点符号),都是由一个或多个十六进制的转义序列替换的。

请注意 encodeURIComponent() 函数 与 encodeURI() 函数的区别之处,前者假定它的参数是 URI 的一部分(比如协议、主机名、路径或查询字符串)。因此 encodeURIComponent() 函数将转义用于分隔 URI 各个部分的标点符号。

使用php实现mysql转sqlite语句

在写ArkPlus框架过程中一直使用的基于PDO驱动的MySQL,因为项目需求,要转一个SQLite版本,于是写了这个简单的转换函数,实现MySQL建表语句到SQLite的平滑转换,有需要的童鞋们可以拿去。

/**
 * mysql(ctreate_table)转sqlite语句
 * @author anrip[wang@rehiy.com]
 * @version 2.1, 2013-01-18 17:02
 * @link http://www.rehiy.com/?arkplus
 */
function ark_table_mysql2sqlite($sql) {
  $expr = array(
    '/`(\w+)`\s/' => '[$1] ',
    '/\s+UNSIGNED/i' => '',
    '/\s+[A-Z]*INT(\([0-9]+\))/i' => ' INTEGER$1',
    '/\s+INTEGER\(\d+\)(.+AUTO_INCREMENT)/i' => ' INTEGER$1',
    '/\s+AUTO_INCREMENT(?!=)/i' => ' PRIMARY KEY AUTOINCREMENT',
    '/\s+ENUM\([^)]+\)/i' => ' VARCHAR(255)',
    '/\s+ON\s+UPDATE\s+[^,]*/i' => ' ',
    '/\s+COMMENT\s+(["\']).+\1/iU' => ' ',
    '/[\r\n]+\s+PRIMARY\s+KEY\s+[^\r\n]+/i' => '',
    '/[\r\n]+\s+UNIQUE\s+KEY\s+[^\r\n]+/i' => '',
    '/[\r\n]+\s+KEY\s+[^\r\n]+/i' => '',
    '/,([\s\r\n])+\)/i' => '$1)',
    '/\s+ENGINE\s*=\s*\w+/i' => ' ',
    '/\s+CHARSET\s*=\s*\w+/i' => ' ',
    '/\s+AUTO_INCREMENT\s*=\s*\d+/i' => ' ',
    '/\s+DEFAULT\s+;/i' => ';',
    '/\)([\s\r\n])+;/i' => ');',
  );
  $sql = preg_replace(array_keys($expr), array_values($expr), $sql);
  return $sql === null ? '{table_mysql2sqlite_error}' : $sql;
}

使用php计算一些时间段

在审查项目代码的过程中,发现一处计算时间的地方很是难懂,于是耐着性子看了下去,看完那300多行代码,我终于明悟了它们只是为了获取几个时间段而存在,苦闷!

下面给出我的计算方法(或许还有更简洁的方法,烦请告知);免得有些人再写300行代码去实现~~

//年/月/日/星期/本月天数
list($year, $month, $day, $week, $days) = explode('/', date('Y/m/d/w/t'));
//计算今日时间范围
echo '<br/>本日开始 '.date('Y-m-d H:i:s w', strtotime($year.'-'.$month.'-'.$day));
echo '<br/>本日结束 '.date('Y-m-d H:i:s w', strtotime($year.'-'.$month.'-'.$day)+86399);
//计算本周时间范围
echo '<br/>本周开始 '.date('Y-m-d H:i:s w', strtotime($year.'-'.$month.'-'.$day)-86400*$week);
echo '<br/>本周结束 '.date('Y-m-d H:i:s w', strtotime($year.'-'.$month.'-'.$day)+86400*(7-$week)-1);
//计算本月时间范围
echo '<br/>本月开始 '.date('Y-m-d H:i:s w', strtotime($year.'-'.$month.'-1'));
echo '<br/>本月结束 '.date('Y-m-d H:i:s w', strtotime($year.'-'.$month.'-'.$days)+86399);