PHP开发

PHP开发

如何通过GuzzleHttp组件下载远程文件并保存在本地?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 1140 次浏览 • 2023-03-05 12:11 • 来自相关话题

PHP8.1的项目,用curl访问链接总是返回35号错误,怎么解决?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 1229 次浏览 • 2023-03-02 17:00 • 来自相关话题

Yii2的beforeAction中如何进行重定向?

回复

Yii框架zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 1125 次浏览 • 2022-12-21 11:27 • 来自相关话题

Nginx+PHP-FPM架构,结合PHP8.1搭建应用服务器时的一些总结和记录

PHPzkbhj 发表了文章 • 0 个评论 • 1530 次浏览 • 2022-12-08 14:07 • 来自相关话题

最近在做一个项目,使用了PHP8.1语言版本,语言框架使用的是Yii2的最新版本,然后再搭建服务器环境和部署应用的过程中遇到了一些问题,现总结如下,给同样场景可能会遇到一些类似问题的你提供一些解决方案,既然已经踩过一遍坑了,方便大家,也方便后续回顾总结。
 
一、编译安装PHP8.1:
因为编译过程中基本上遇到问题的概率并不大,我的编译过程还算顺利,因为服务器上保留了以前其他人运行的项目。PHP的版本是7.2.3,所以就编译安装了PHP8.1到一个指定的地方,然后独立安装的PHP-FPM。尽量不影响以前的项目运行。
我的编译配置信息(注意要一行,不能存在换行符):./configure --prefix=/usr/local/php8.1 --with-config-file-path=/usr/local/php8.1/etc --with-curl --with-freetype --enable-gd --with-jpeg --with-gettext --with-kerberos --with-libdir=lib64 --with-libxml --with-mysqli --with-openssl --with-pdo-mysql --with-pdo-sqlite --with-pear --enable-sockets --with-mhash --with-ldap-sasl --with-xsl --with-zlib -with-bz2 --with-iconv --enable-fpm --enable-pdo --enable-bcmath --enable-mbregex --enable-mbstring --enable-opcache --enable-pcntl --enable-soap --enable-sockets --enable-sysvsem --enable-xml --enable-sysvsem --enable-cli --enable-opcache --enable-intl --enable-calendar --enable-static --enable-mysqlnd最终我把编译好的php-fpm文件复制到了我的PHP8.1目录下,并完成了配置启动。这会儿还一切看似顺利。
 
编译过程中遇到了一个问题是:
configure: error: iconv does not support errno
 优先参考网上的解决方案安装缺失的iconv,如果不行,在运行configure之前先运行:
export LDFLAGS="$LDFLAGS -liconv"参考官网bug讨论 https://bugs.php.net/bug.php?id=80585,在此对歪果友人诚挚的感谢。

二、部署应用:
搭建好之后,顺利启动PHP-FPM服务器,监听端口9000,然后再Nginx配置文件中,将请求信息直接给到9000端口来处理,这里就有一些“东西”要注意了,否则就遇到了下面的问题:
 
问题1:部署应用之后,请求失败,页面返回“File not Found”,然后查看Nginx的报错日志,显示信息是:2022/12/08 13:15:51 [error] 31547#0: *29582 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream, client: 119.116.237.75, server: web.*.com, request: "GET /site/user-agreement HTTP/2.0", upstream: "fastcgi://127.0.0.1:9000", host: "web*.*com"网上说中问题, 比如php-fpm的 运行group和user不对啦,应用目录权限不足啦,但我发现根本原因是在Nginx转发时,少了一个重要的配置信息。
由于我的yii2项目启用了 URLManage功能,为了让链接看起来更漂亮一些,所以需要在Nginx配置文件中加入下面的这个配置:if (!-e $request_filename){
rewrite ^/(.*) /index.php last;
}

所以,完整的转发配置应该是:location / {
fastcgi_pass 127.0.0.1:9000;
if (!-e $request_filename){
rewrite ^/(.*) /index.php last;
}
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}所以这块,大家应该注意一下,更改完,重新启动Nginx服务之后,项目正常启动了!
 
问题二、页面图标显示异常:
由于使用了第三方图标工具,打开F12工具之后,发现是静态资源服务器访问字体文件时显示跨域:Access to Font at “xxx” from origin “xxx” has been by CORS plicy:
No ‘Access-Control-Allow-Origin’header is present on the requested resource.
Origin “xxx” is therefore not allowed access.这时候,只需要在静态资源服务器的Nginx配置文件中增加下面的配置项即可:location ~* .(eot|ttf|woff|swoff2|svg|otf)$ {
add_header Access-Control-Allow-Origin [url]http://www.zkbhj.com;[/url] //只允许单域名或者 ‘*' 。不推荐 ‘*' ,会导致安全问题
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
} 查看全部
最近在做一个项目,使用了PHP8.1语言版本,语言框架使用的是Yii2的最新版本,然后再搭建服务器环境和部署应用的过程中遇到了一些问题,现总结如下,给同样场景可能会遇到一些类似问题的你提供一些解决方案,既然已经踩过一遍坑了,方便大家,也方便后续回顾总结。
 
一、编译安装PHP8.1:
因为编译过程中基本上遇到问题的概率并不大,我的编译过程还算顺利,因为服务器上保留了以前其他人运行的项目。PHP的版本是7.2.3,所以就编译安装了PHP8.1到一个指定的地方,然后独立安装的PHP-FPM。尽量不影响以前的项目运行。
我的编译配置信息(注意要一行,不能存在换行符):
./configure --prefix=/usr/local/php8.1 --with-config-file-path=/usr/local/php8.1/etc --with-curl --with-freetype --enable-gd --with-jpeg --with-gettext --with-kerberos --with-libdir=lib64 --with-libxml --with-mysqli --with-openssl --with-pdo-mysql --with-pdo-sqlite --with-pear --enable-sockets --with-mhash --with-ldap-sasl --with-xsl --with-zlib -with-bz2 --with-iconv --enable-fpm --enable-pdo --enable-bcmath  --enable-mbregex --enable-mbstring --enable-opcache --enable-pcntl --enable-soap --enable-sockets --enable-sysvsem --enable-xml --enable-sysvsem --enable-cli --enable-opcache --enable-intl --enable-calendar --enable-static --enable-mysqlnd
最终我把编译好的php-fpm文件复制到了我的PHP8.1目录下,并完成了配置启动。这会儿还一切看似顺利。
 
编译过程中遇到了一个问题是:
configure: error: iconv does not support errno

 优先参考网上的解决方案安装缺失的iconv,如果不行,在运行configure之前先运行:
export LDFLAGS="$LDFLAGS -liconv"
参考官网bug讨论 https://bugs.php.net/bug.php?id=80585,在此对歪果友人诚挚的感谢。

二、部署应用:
搭建好之后,顺利启动PHP-FPM服务器,监听端口9000,然后再Nginx配置文件中,将请求信息直接给到9000端口来处理,这里就有一些“东西”要注意了,否则就遇到了下面的问题:
 
问题1:部署应用之后,请求失败,页面返回“File not Found”,然后查看Nginx的报错日志,显示信息是:
2022/12/08 13:15:51 [error] 31547#0: *29582 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream, client: 119.116.237.75, server: web.*.com, request: "GET /site/user-agreement HTTP/2.0", upstream: "fastcgi://127.0.0.1:9000", host: "web*.*com"
网上说中问题, 比如php-fpm的 运行group和user不对啦,应用目录权限不足啦,但我发现根本原因是在Nginx转发时,少了一个重要的配置信息。
由于我的yii2项目启用了 URLManage功能,为了让链接看起来更漂亮一些,所以需要在Nginx配置文件中加入下面的这个配置:
if (!-e $request_filename){
rewrite ^/(.*) /index.php last;
}

所以,完整的转发配置应该是:
location / {
fastcgi_pass 127.0.0.1:9000;
if (!-e $request_filename){
rewrite ^/(.*) /index.php last;
}
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
所以这块,大家应该注意一下,更改完,重新启动Nginx服务之后,项目正常启动了!
 
问题二、页面图标显示异常:
由于使用了第三方图标工具,打开F12工具之后,发现是静态资源服务器访问字体文件时显示跨域:
Access to Font at “xxx” from origin “xxx” has been by CORS plicy:
No ‘Access-Control-Allow-Origin’header is present on the requested resource.
Origin “xxx” is therefore not allowed access.
这时候,只需要在静态资源服务器的Nginx配置文件中增加下面的配置项即可:
location ~* .(eot|ttf|woff|swoff2|svg|otf)$ {
add_header Access-Control-Allow-Origin [url]http://www.zkbhj.com;[/url] //只允许单域名或者 ‘*' 。不推荐 ‘*' ,会导致安全问题
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
}

如何通过Composer只安装指定的包,不更新各种依赖的组件包?

回复

工具软件zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 2105 次浏览 • 2023-07-09 14:32 • 来自相关话题

在Yii2框架中使用rbac功能时,报错如何解决?

回复

Yii框架zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 1454 次浏览 • 2022-10-19 20:37 • 来自相关话题

PHP项目如何实现快速的批量数据处理?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 3033 次浏览 • 2021-01-31 17:02 • 来自相关话题

PHP中如何获取毫秒时间戳?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 4207 次浏览 • 2021-01-20 17:19 • 来自相关话题

PHP如何实现json_encode时不把中文和反斜杠进行转义?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 4634 次浏览 • 2020-07-07 10:08 • 来自相关话题

接口正常返回数据,但是状态码返回500,是什么原因?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 6993 次浏览 • 2020-02-03 20:48 • 来自相关话题

条新动态, 点击查看
zkbhj

zkbhj 回答了问题 • 2016-12-05 15:31 • 1 个回复 不感兴趣

为什么没有PHP6,直接上了PHP7?

赞同来自:

以下是PHP核心开发人员“鸟哥”的微博说明:

好吧, 很多人问为啥没有PHP6了, 我来解释下吧: 在很久很久以前, 有一群人, 创建了一个PHP6的项目, 主要的目的是为PHP引擎增加Unicode支持. 当时开发者们同时维护5和6的开发, 慢慢的大家发... 显示全部 »
以下是PHP核心开发人员“鸟哥”的微博说明:

好吧, 很多人问为啥没有PHP6了, 我来解释下吧: 在很久很久以前, 有一群人, 创建了一个PHP6的项目, 主要的目的是为PHP引擎增加Unicode支持. 当时开发者们同时维护5和6的开发, 慢慢的大家发现新功能都等着提交给6, 而6因为开发速度慢, 导致很多新特性没法提交, 状态很不理想. 再后来6就没人开发了. 于是.

微博地址: http://weibo.com/1170999921/BkvZ1aLqe?sudaref=www.baidu.com&retcode=6102&type=comment
 
另外的解释为:

PHP社群核心参与者Andrea Faulds与Zeev Suraski在PHP的Wiki上 ,共同发表文章表示,PHP6的开发遇到了一些问题,导致一直没办法释出正式版本(General Availability),他们认为PHP6是一个失败的专案,现在是该进到PHP7的时候了。PHP7原先是PHP社群中的惠新辰与Dmitry Stogov私下进行的PHPNG专案,后来专案较成型后公开而受大家所接受,并朝下一个PHP接班版本发展。
zkbhj

zkbhj 回答了问题 • 2017-06-02 10:43 • 1 个回复 不感兴趣

如何生成全局唯一ID并能大致有序?

赞同来自:

Twitter-Snowflake算法产生的背景相当简单,为了满足Twitter每秒上万条消息的请求,每条消息都必须分配一条唯一的id,这些id还需要一些大致的顺序(方便客户端排序),并且在分布式系统中不同机器产生的id必须不同。

Snowflake算法核... 显示全部 »
Twitter-Snowflake算法产生的背景相当简单,为了满足Twitter每秒上万条消息的请求,每条消息都必须分配一条唯一的id,这些id还需要一些大致的顺序(方便客户端排序),并且在分布式系统中不同机器产生的id必须不同。

Snowflake算法核心

把时间戳,工作机器id,序列号组合在一起。

133
 
除了最高位bit标记为不可用以外,其余三组bit占位均可浮动,看具体的业务需求而定。默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1023台机器,序列号支持1毫秒产生4095个自增序列id。下文会具体分析。
 结构为:
0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---0000000000 00
在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。

这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分),并且效率较高,经测试,snowflake每秒能够产生26万ID左右,完全满足需要。 
/**
* ID 生成策略
* 毫秒级时间41位+机器ID 10位+毫秒内序列12位。
* 0 41 51 64
+-----------+------+------+
|time |pc |inc |
+-----------+------+------+
* 最高位bit标记为不可用
* 前41bits是以微秒为单位的timestamp。
* 接着10bits是事先配置好的机器ID。
* 最后12bits是累加计数器。
* macheine id(10bits)标明最多只能有1024台机器同时产生ID,sequence number(12bits)也标明1台机器1ms中最多产生4096个ID,
*
*/
class SnowFlake{
private static $epoch = 1462264156000;


public function createID($machineId){
/*
* Time - 41 bits
*/
$time = floor(microtime(true) * 1000);


/*
* Substract custom epoch from current time
*/
$time -= SnowFlake::$epoch;

/*
* flag number - 1 bits - can not change, beacause is shoule be a positive number
*/
$suffix = 0;

/*
* Create a base and add time to it
*/
$base = decbin(pow(2,40) - 1 + $time);
//$base = sprintf("1s", decbin($time));
/*
* Configured machine id - 10 bits - up to 512 machines
*/
$machineid = decbin(pow(2,9) - 1 + $machineId);
//$machineid = sprintf("0s", decbin($machineId));
/*
* sequence number - 12 bits - up to 2048 random numbers per machine
*/
$random = mt_rand(1, pow(2,11)-1);
$random = decbin(pow(2,11)-1 + $random);
//$random = mt_rand(1, pow(2, 12) - 1);
//$random = sprintf("2s", decbin($random));

/*
* 拼装$base
*/
$base = $suffix.$base.$machineid.$random;
/*
* 讲二进制的base转换成long
*/
$base = bindec($base);


$id = sprintf('%.0f', $base);


return $id;
}
}
我这里的PHP代码序列号是随机产生的,因为我们的业务还不达不到需要有序生成的必要, 而且对于PHP来说要序列化生成必须把这个序列存储到缓存里面去。
 
GIT源码:https://github.com/search?l=PHP&q=snowflake&type=Repositories&utf8=%E2%9C%93
zkbhj

zkbhj 回答了问题 • 2018-07-19 21:12 • 1 个回复 不感兴趣

header头中的Referer和X-Requested-With是啥含义?

赞同来自:

Referer  是  HTTP  请求header 的一部分,当浏览器(或者模拟浏览器行为)向web 服务器发送请求的时候,头信息里有包含  Referer  。比如我在www.google.com 里有一个www.baidu.com 链接,那么点击这个ww... 显示全部 »
Referer  是  HTTP  请求header 的一部分,当浏览器(或者模拟浏览器行为)向web 服务器发送请求的时候,头信息里有包含  Referer  。比如我在www.google.com 里有一个www.baidu.com 链接,那么点击这个www.baidu.com ,它的header 信息里就有: Referer=http://www.google.com
由此可以看出来吧。它就是表示一个来源。看下图的一个请求的 Referer  信息。
 
Referer  的正确英语拼法是referrer 。由于早期HTTP规范的拼写错误,为了保持向后兼容就将错就错了。其它网络技术的规范企图修正此问题,使用正确拼法,所以目前拼法不统一。还有它第一个字母是大写。 
Referer的作用?

1.防盗链。

刚刚前面有提到一个小 Demo  。

我在www.google.com里有一个www.baidu.com链接,那么点击这个www.baidu.com,它的header信息里就有: Referer=http://www.google.com

那么可以利用这个来防止盗链了,比如我只允许我自己的网站访问我自己的图片服务器,那我的域名是www.google.com,那么图片服务器每次取到Referer来判断一下是不是我自己的域名www.google.com,如果是就继续访问,不是就拦截。

这是不是就达到防盗链的效果了?

将这个http请求发给服务器后,如果服务器要求必须是某个地址或者某几个地址才能访问,而你发送的referer不符合他的要求,就会拦截或者跳转到他要求的地址,然后再通过这个地址进行访问。

2.防止恶意请求。

比如静态请求是*.html结尾的,动态请求是*.shtml,那么由此可以这么用,所有的*.shtml请求,必须 Referer  为我自己的网站。

Referer= http://www.google.com  
空Referer是怎么回事?什么情况下会出现Referer?

首先,我们对空 Referer  的定义为, Referer  头部的内容为空,或者,一个 HTTP  请求中根本不包含 Referer  头部。

那么什么时候 HTTP  请求会不包含 Referer  字段呢?根据Referer的定义,它的作用是指示一个请求是从哪里链接过来,那么当一个请求并不是由链接触发产生的,那么自然也就不需要指定这个请求的链接来源。

比如,直接在浏览器的地址栏中输入一个资源的URL地址,那么这种请求是不会包含 Referer  字段的,因为这是一个“凭空产生”的 HTTP  请求,并不是从一个地方链接过去的。

那么在防盗链设置中,允许空Referer和不允许空Referer有什么区别?

允许 Referer  为空,意味着你允许比如浏览器直接访问,就是空。
  X-Requested-With
 
X-Requested-With请求头用于在服务器端判断request来自Ajax请求还是传统请求。
 
如果 requestedWith 为 null,则为同步请求。如果 requestedWith 为 XMLHttpRequest 则为 Ajax 请求。 if (request.getHeader("x-requested-with") != null
&& request.getHeader("x-requested-with").equalsIgnoreCase("XMLHttpRequest")) {
out.print("该请求是 AJAX 异步HTTP请求。");
}else{
out.print("该请求是传统的 同步HTTP请求。");
}  
如何在发送请求是去掉它?
$.ajax({
url: 'http://www.zhangruifeng.com',
beforeSend: function( xhr ) {
xhr.setRequestHeader('X-Requested-With', {toString: function(){ return ''; }});
},
success: function( data ) {
if (console && console.log){
console.log( 'Got data without the X-Requested-With header' );
}
}
});

如何通过GuzzleHttp组件下载远程文件并保存在本地?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 1140 次浏览 • 2023-03-05 12:11 • 来自相关话题

PHP8.1的项目,用curl访问链接总是返回35号错误,怎么解决?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 1229 次浏览 • 2023-03-02 17:00 • 来自相关话题

Yii2的beforeAction中如何进行重定向?

回复

Yii框架zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 1125 次浏览 • 2022-12-21 11:27 • 来自相关话题

如何通过Composer只安装指定的包,不更新各种依赖的组件包?

回复

工具软件zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 2105 次浏览 • 2023-07-09 14:32 • 来自相关话题

在Yii2框架中使用rbac功能时,报错如何解决?

回复

Yii框架zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 1454 次浏览 • 2022-10-19 20:37 • 来自相关话题

PHP项目如何实现快速的批量数据处理?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 3033 次浏览 • 2021-01-31 17:02 • 来自相关话题

PHP中如何获取毫秒时间戳?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 4207 次浏览 • 2021-01-20 17:19 • 来自相关话题

PHP如何实现json_encode时不把中文和反斜杠进行转义?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 4634 次浏览 • 2020-07-07 10:08 • 来自相关话题

接口正常返回数据,但是状态码返回500,是什么原因?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 6993 次浏览 • 2020-02-03 20:48 • 来自相关话题

PHP如何高效并且快速方便的读取html中的指定内容便于抓取数据?

回复

工具软件zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 3556 次浏览 • 2019-11-03 10:25 • 来自相关话题

Nginx+PHP-FPM架构,结合PHP8.1搭建应用服务器时的一些总结和记录

PHPzkbhj 发表了文章 • 0 个评论 • 1530 次浏览 • 2022-12-08 14:07 • 来自相关话题

最近在做一个项目,使用了PHP8.1语言版本,语言框架使用的是Yii2的最新版本,然后再搭建服务器环境和部署应用的过程中遇到了一些问题,现总结如下,给同样场景可能会遇到一些类似问题的你提供一些解决方案,既然已经踩过一遍坑了,方便大家,也方便后续回顾总结。
 
一、编译安装PHP8.1:
因为编译过程中基本上遇到问题的概率并不大,我的编译过程还算顺利,因为服务器上保留了以前其他人运行的项目。PHP的版本是7.2.3,所以就编译安装了PHP8.1到一个指定的地方,然后独立安装的PHP-FPM。尽量不影响以前的项目运行。
我的编译配置信息(注意要一行,不能存在换行符):./configure --prefix=/usr/local/php8.1 --with-config-file-path=/usr/local/php8.1/etc --with-curl --with-freetype --enable-gd --with-jpeg --with-gettext --with-kerberos --with-libdir=lib64 --with-libxml --with-mysqli --with-openssl --with-pdo-mysql --with-pdo-sqlite --with-pear --enable-sockets --with-mhash --with-ldap-sasl --with-xsl --with-zlib -with-bz2 --with-iconv --enable-fpm --enable-pdo --enable-bcmath --enable-mbregex --enable-mbstring --enable-opcache --enable-pcntl --enable-soap --enable-sockets --enable-sysvsem --enable-xml --enable-sysvsem --enable-cli --enable-opcache --enable-intl --enable-calendar --enable-static --enable-mysqlnd最终我把编译好的php-fpm文件复制到了我的PHP8.1目录下,并完成了配置启动。这会儿还一切看似顺利。
 
编译过程中遇到了一个问题是:
configure: error: iconv does not support errno
 优先参考网上的解决方案安装缺失的iconv,如果不行,在运行configure之前先运行:
export LDFLAGS="$LDFLAGS -liconv"参考官网bug讨论 https://bugs.php.net/bug.php?id=80585,在此对歪果友人诚挚的感谢。

二、部署应用:
搭建好之后,顺利启动PHP-FPM服务器,监听端口9000,然后再Nginx配置文件中,将请求信息直接给到9000端口来处理,这里就有一些“东西”要注意了,否则就遇到了下面的问题:
 
问题1:部署应用之后,请求失败,页面返回“File not Found”,然后查看Nginx的报错日志,显示信息是:2022/12/08 13:15:51 [error] 31547#0: *29582 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream, client: 119.116.237.75, server: web.*.com, request: "GET /site/user-agreement HTTP/2.0", upstream: "fastcgi://127.0.0.1:9000", host: "web*.*com"网上说中问题, 比如php-fpm的 运行group和user不对啦,应用目录权限不足啦,但我发现根本原因是在Nginx转发时,少了一个重要的配置信息。
由于我的yii2项目启用了 URLManage功能,为了让链接看起来更漂亮一些,所以需要在Nginx配置文件中加入下面的这个配置:if (!-e $request_filename){
rewrite ^/(.*) /index.php last;
}

所以,完整的转发配置应该是:location / {
fastcgi_pass 127.0.0.1:9000;
if (!-e $request_filename){
rewrite ^/(.*) /index.php last;
}
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}所以这块,大家应该注意一下,更改完,重新启动Nginx服务之后,项目正常启动了!
 
问题二、页面图标显示异常:
由于使用了第三方图标工具,打开F12工具之后,发现是静态资源服务器访问字体文件时显示跨域:Access to Font at “xxx” from origin “xxx” has been by CORS plicy:
No ‘Access-Control-Allow-Origin’header is present on the requested resource.
Origin “xxx” is therefore not allowed access.这时候,只需要在静态资源服务器的Nginx配置文件中增加下面的配置项即可:location ~* .(eot|ttf|woff|swoff2|svg|otf)$ {
add_header Access-Control-Allow-Origin [url]http://www.zkbhj.com;[/url] //只允许单域名或者 ‘*' 。不推荐 ‘*' ,会导致安全问题
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
} 查看全部
最近在做一个项目,使用了PHP8.1语言版本,语言框架使用的是Yii2的最新版本,然后再搭建服务器环境和部署应用的过程中遇到了一些问题,现总结如下,给同样场景可能会遇到一些类似问题的你提供一些解决方案,既然已经踩过一遍坑了,方便大家,也方便后续回顾总结。
 
一、编译安装PHP8.1:
因为编译过程中基本上遇到问题的概率并不大,我的编译过程还算顺利,因为服务器上保留了以前其他人运行的项目。PHP的版本是7.2.3,所以就编译安装了PHP8.1到一个指定的地方,然后独立安装的PHP-FPM。尽量不影响以前的项目运行。
我的编译配置信息(注意要一行,不能存在换行符):
./configure --prefix=/usr/local/php8.1 --with-config-file-path=/usr/local/php8.1/etc --with-curl --with-freetype --enable-gd --with-jpeg --with-gettext --with-kerberos --with-libdir=lib64 --with-libxml --with-mysqli --with-openssl --with-pdo-mysql --with-pdo-sqlite --with-pear --enable-sockets --with-mhash --with-ldap-sasl --with-xsl --with-zlib -with-bz2 --with-iconv --enable-fpm --enable-pdo --enable-bcmath  --enable-mbregex --enable-mbstring --enable-opcache --enable-pcntl --enable-soap --enable-sockets --enable-sysvsem --enable-xml --enable-sysvsem --enable-cli --enable-opcache --enable-intl --enable-calendar --enable-static --enable-mysqlnd
最终我把编译好的php-fpm文件复制到了我的PHP8.1目录下,并完成了配置启动。这会儿还一切看似顺利。
 
编译过程中遇到了一个问题是:
configure: error: iconv does not support errno

 优先参考网上的解决方案安装缺失的iconv,如果不行,在运行configure之前先运行:
export LDFLAGS="$LDFLAGS -liconv"
参考官网bug讨论 https://bugs.php.net/bug.php?id=80585,在此对歪果友人诚挚的感谢。

二、部署应用:
搭建好之后,顺利启动PHP-FPM服务器,监听端口9000,然后再Nginx配置文件中,将请求信息直接给到9000端口来处理,这里就有一些“东西”要注意了,否则就遇到了下面的问题:
 
问题1:部署应用之后,请求失败,页面返回“File not Found”,然后查看Nginx的报错日志,显示信息是:
2022/12/08 13:15:51 [error] 31547#0: *29582 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream, client: 119.116.237.75, server: web.*.com, request: "GET /site/user-agreement HTTP/2.0", upstream: "fastcgi://127.0.0.1:9000", host: "web*.*com"
网上说中问题, 比如php-fpm的 运行group和user不对啦,应用目录权限不足啦,但我发现根本原因是在Nginx转发时,少了一个重要的配置信息。
由于我的yii2项目启用了 URLManage功能,为了让链接看起来更漂亮一些,所以需要在Nginx配置文件中加入下面的这个配置:
if (!-e $request_filename){
rewrite ^/(.*) /index.php last;
}

所以,完整的转发配置应该是:
location / {
fastcgi_pass 127.0.0.1:9000;
if (!-e $request_filename){
rewrite ^/(.*) /index.php last;
}
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
所以这块,大家应该注意一下,更改完,重新启动Nginx服务之后,项目正常启动了!
 
问题二、页面图标显示异常:
由于使用了第三方图标工具,打开F12工具之后,发现是静态资源服务器访问字体文件时显示跨域:
Access to Font at “xxx” from origin “xxx” has been by CORS plicy:
No ‘Access-Control-Allow-Origin’header is present on the requested resource.
Origin “xxx” is therefore not allowed access.
这时候,只需要在静态资源服务器的Nginx配置文件中增加下面的配置项即可:
location ~* .(eot|ttf|woff|swoff2|svg|otf)$ {
add_header Access-Control-Allow-Origin [url]http://www.zkbhj.com;[/url] //只允许单域名或者 ‘*' 。不推荐 ‘*' ,会导致安全问题
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
}

PHP代码总结:时间处理相关的辅助类

PHPzkbhj 发表了文章 • 0 个评论 • 2435 次浏览 • 2019-05-30 14:49 • 来自相关话题

class TimeHelper
{
/**
* 得到当前时间的毫秒时间戳
* @return float 13位毫秒时间戳
*/
public static function getCurTimeMsec()
{
list($t1, $t2) = explode(' ', microtime());
return (float)sprintf('%.0f',(floatval($t1)+floatval($t2))*1000);
}

/**
* 获取当前时间17位的毫秒时间格式时间点(2017 07 29 21 44 43 129)
* @return integer
*/
public static function getCurTimeMsecFormat()
{
list($u_sec, $sec) = explode(' ', microtime());
return intval(date('YmdHis', intval($sec)) . str_pad(round(floatval($u_sec) * 1000), 3, 0, STR_PAD_LEFT));
}

/**
* 将20200629000000格式的日期转换成0000-00-00 00:00:00格式
*/
public static function dateNormalization($date)
{
if (strlen($date) == 14) {
return preg_replace('/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/', "$1-$2-$3 $4:$5:$6", $date);
}
return $date;
}

/**
* 将17位的毫秒时间格式(2017 07 29 21 44 43 129)转换成毫秒时间戳格式(15XXX)
*/
public static function dateToMescNormalization($date)
{
if (strlen($date) == 17) {
$secDate = preg_replace('/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/', "$1-$2-$3 $4:$5:$6", substr($date,0,14));
return strtotime($secDate).substr($date,14,3);
}
return $date;
}
} 查看全部
class TimeHelper
{
/**
* 得到当前时间的毫秒时间戳
* @return float 13位毫秒时间戳
*/
public static function getCurTimeMsec()
{
list($t1, $t2) = explode(' ', microtime());
return (float)sprintf('%.0f',(floatval($t1)+floatval($t2))*1000);
}

/**
* 获取当前时间17位的毫秒时间格式时间点(2017 07 29 21 44 43 129)
* @return integer
*/
public static function getCurTimeMsecFormat()
{
list($u_sec, $sec) = explode(' ', microtime());
return intval(date('YmdHis', intval($sec)) . str_pad(round(floatval($u_sec) * 1000), 3, 0, STR_PAD_LEFT));
}

/**
* 将20200629000000格式的日期转换成0000-00-00 00:00:00格式
*/
public static function dateNormalization($date)
{
if (strlen($date) == 14) {
return preg_replace('/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/', "$1-$2-$3 $4:$5:$6", $date);
}
return $date;
}

/**
* 将17位的毫秒时间格式(2017 07 29 21 44 43 129)转换成毫秒时间戳格式(15XXX)
*/
public static function dateToMescNormalization($date)
{
if (strlen($date) == 17) {
$secDate = preg_replace('/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/', "$1-$2-$3 $4:$5:$6", substr($date,0,14));
return strtotime($secDate).substr($date,14,3);
}
return $date;
}
}

两种数据序列化方案性能对比:Msgpack和Json

专业名词zkbhj 发表了文章 • 0 个评论 • 6581 次浏览 • 2019-01-23 11:00 • 来自相关话题

    MessagePack(简写msgpack)是一个高效的二进制序列化格式。它让你像JSON一样可以在各种语言之间交换数据。但是它比JSON更快、更小。小的整数会被编码成一个字节,短的字符串仅仅只需要比它的长度多一字节的大小。之前在lua脚本中使用过msgpack,因为有大量数据要入redis,而考虑到内存开销,使用了压缩比更大的msgpack。因为msgpack是一个二进制格式,所以没法像json后的字符串一样可直观地查看数据。
 
    msgpack的官网地址:http://pecl.php.net/package/msgpack 里面有各PHP版本windows下的dll扩展,也有源码包供linux下编译,所以像lua这样的脚本语言可以直接使用。msgpack和json_encode都是序列化存储数据,那么msgpack的效率与json的效率相比的话到底怎么样呢?看下面这个简单的对比程序:
//msgpack与json的性能对比
//1,数据拼凑
$data =array(
'youku' => '优酷视频', 'pptv' => 'PPTV', 'sohu' => '搜狐视频', 'qiyi' => '奇艺视频', 'letv' => '乐视视频',
'tencent' => '腾讯视频', 'sina' => '新浪视频', 'tudou' => '土豆视频', 'm1905' => '电影网', 'cntv' => 'CNTV',
);
$data = json_encode($data);
$newArr = array();
for($i = 1; $i<=500; $i++) //修改此处$i的最大值以控制数据的大小
{
$newArr [] =$data;
}

//数据大小对比
$msg_data = msgpack_pack($newArr);
echo "使用msgpack处理后大小:".strlen($msg_data);
$json_data = json_encode($newArr);
echo "<br>使用json处理后大小:".strlen($json_data);
echo "<br>msgpack处理后大小与json处理后大小比为1:".round((strlen($json_data)/strlen($msg_data)),2);

//计算1000次msgpack压缩用时
$time = microtime(true);
for($i = 1; $i<1000; $i++){
msgpack_pack($newArr);
}
echo '<br>1000次msgpack操作用时:'.(microtime(true)- $time);

//计算1000次json_encode压缩用时
$time1 = microtime(true);
for($i = 1; $i<1000; $i++){
json_encode($newArr);
}
echo '<br>1000次json_encode操作用时:'.(microtime(true)- $time1); 程序过程没什么可说的了,就是先拼凑了一个数组数据(用$i来控制它的大小)。然后对比对这个数组的处理用时,如果$i很小,假如为2,得到的结果如下:
使用msgpack处理后大小:609
使用json处理后大小:751
msgpack处理后大小与json处理后大小比为1:1.23
1000次msgpack操作用时:0.05400013923645
1000次json_encode操作用时:0.03600001335144 从上面的结果来看,msg_pack的效率根本不如json_encode的效率,只是msg_pack的压缩率大些而已。而当我把$i改大点,比如改到500后,结果就完全反转了:
使用msgpack处理后大小:152003
使用json处理后大小:187501
msgpack处理后大小与json处理后大小比为1:1.23
1000次msgpack操作用时:0.68599987030029
1000次json_encode操作用时:2.2000000476837     经过多次测试,最后做两个总结如下:    1,msg_pack的压缩效率比json_encode大是毫无疑问,但压缩比我这只看到提高了20%左右,可能和数据类型有关系,这个值只供参考。


    2,在数据量较小的情况下,msg_pack的效率不如json_encode.而在数据量较大时,msg_pack的效率就远大于json_encode。

    3,和数据序列化一样,对数据的反序列化上,也是数量量大时,msg_unpack的效率远大于json_decode.

    下面是进行反序列化时的结果:
1000次msgpack操作用时:0.80399990081787
1000次json_encode操作用时:2.6389999389648 查看全部
    MessagePack(简写msgpack)是一个高效的二进制序列化格式。它让你像JSON一样可以在各种语言之间交换数据。但是它比JSON更快、更小。小的整数会被编码成一个字节,短的字符串仅仅只需要比它的长度多一字节的大小。之前在lua脚本中使用过msgpack,因为有大量数据要入redis,而考虑到内存开销,使用了压缩比更大的msgpack。因为msgpack是一个二进制格式,所以没法像json后的字符串一样可直观地查看数据。
 
    msgpack的官网地址:http://pecl.php.net/package/msgpack 里面有各PHP版本windows下的dll扩展,也有源码包供linux下编译,所以像lua这样的脚本语言可以直接使用。msgpack和json_encode都是序列化存储数据,那么msgpack的效率与json的效率相比的话到底怎么样呢?看下面这个简单的对比程序:
//msgpack与json的性能对比
//1,数据拼凑
$data =array(
'youku' => '优酷视频', 'pptv' => 'PPTV', 'sohu' => '搜狐视频', 'qiyi' => '奇艺视频', 'letv' => '乐视视频',
'tencent' => '腾讯视频', 'sina' => '新浪视频', 'tudou' => '土豆视频', 'm1905' => '电影网', 'cntv' => 'CNTV',
);
$data = json_encode($data);
$newArr = array();
for($i = 1; $i<=500; $i++) //修改此处$i的最大值以控制数据的大小
{
$newArr [] =$data;
}

//数据大小对比
$msg_data = msgpack_pack($newArr);
echo "使用msgpack处理后大小:".strlen($msg_data);
$json_data = json_encode($newArr);
echo "<br>使用json处理后大小:".strlen($json_data);
echo "<br>msgpack处理后大小与json处理后大小比为1:".round((strlen($json_data)/strlen($msg_data)),2);

//计算1000次msgpack压缩用时
$time = microtime(true);
for($i = 1; $i<1000; $i++){
msgpack_pack($newArr);
}
echo '<br>1000次msgpack操作用时:'.(microtime(true)- $time);

//计算1000次json_encode压缩用时
$time1 = microtime(true);
for($i = 1; $i<1000; $i++){
json_encode($newArr);
}
echo '<br>1000次json_encode操作用时:'.(microtime(true)- $time1);
 程序过程没什么可说的了,就是先拼凑了一个数组数据(用$i来控制它的大小)。然后对比对这个数组的处理用时,如果$i很小,假如为2,得到的结果如下:
使用msgpack处理后大小:609
使用json处理后大小:751
msgpack处理后大小与json处理后大小比为1:1.23
1000次msgpack操作用时:0.05400013923645
1000次json_encode操作用时:0.03600001335144
 从上面的结果来看,msg_pack的效率根本不如json_encode的效率,只是msg_pack的压缩率大些而已。而当我把$i改大点,比如改到500后,结果就完全反转了:
使用msgpack处理后大小:152003
使用json处理后大小:187501
msgpack处理后大小与json处理后大小比为1:1.23
1000次msgpack操作用时:0.68599987030029
1000次json_encode操作用时:2.2000000476837
     经过多次测试,最后做两个总结如下:    1,msg_pack的压缩效率比json_encode大是毫无疑问,但压缩比我这只看到提高了20%左右,可能和数据类型有关系,这个值只供参考。


    2,在数据量较小的情况下,msg_pack的效率不如json_encode.而在数据量较大时,msg_pack的效率就远大于json_encode。

    3,和数据序列化一样,对数据的反序列化上,也是数量量大时,msg_unpack的效率远大于json_decode.

    下面是进行反序列化时的结果:
1000次msgpack操作用时:0.80399990081787
1000次json_encode操作用时:2.6389999389648

#原创#API接口设计要考虑的几个重要原则和方法总结

架构思想zkbhj 发表了文章 • 0 个评论 • 7651 次浏览 • 2018-11-12 11:04 • 来自相关话题

这里想和大家讨论的是在后台接口设计过程中,还有哪些方面需要考虑,以及还有哪些优秀的技术实践方案可以借鉴。

 【规范和最佳实践】

1.合理的接口命名;

接口的命名必须规范优雅,在未看到接口文档时,就可以根据接口的URL明白接口的功能是什么?

如下面的例子://好的接口命名示例
/customer/cert/search.json

//不好的接口命名示例
/customer/info/get.json
2.入参和出参的规范化定义,有统一的风格;

一个项目内的所有接口,必须有统一的风格,统一返回格式,约定业务层错误编码,每个编码可以携带明确的错误信息。出入参字段含义明确,采用统一的命名规范,如驼峰命名等。返回格式统一采用json格式。举一个例子:{
"status": "failure",
"error_code": 100003,
"error_message": "未获取到用户信息",
"data":
}status标识接口是否逻辑处理成功;error_code为不同类型错误信息对应的唯一错误码,error_message为错误信息的简要描述信息(注意某些数据或者信息是否可直接展示给用户),data则为需要返回给调用方的数据信息。
 
另外,每个参数一定要有明确且固定的数据格式,int就是int,string就是string,array就是array,object就是object,因为对于一些对数据类型要求比较严格的使用方,不明确的数据格式返回,可能会造成不可预知的错误。
 
下面给大家列一下Json里的六种基本数据格式: 

Number:整数或浮点数
String:字符串
Boolean:true 或 false
Array:数组包含在方括号中
Object:对象包含在大括号{}中
Null:空类型

 
3.接口的功能定义要具备单一性;

单一性是指接口要做的事情应该是一个比较单一的事情,比如登陆接口,登陆完成应该只是返回登陆成功以后一些用户信息如uid即可,但很多人为了减少接口交互,返回一大堆额外的数据。

但有时候对于一些内部系统接口来讲,为了实现通用性,可能会提供一些通用的查询接口,即在同一个接口内返回尽可能多的信息,但也不建议这么做,至少不是一个好的实践;


4.明确接口支持的协议;

接口要明确所支持的协议,是POST/GET/PUT/DELETE等的哪一个。一般来讲,同一个接口而言,尽量只支持一种协议,并且在接口被调用时,如果参数传递非接口定义协议,要明确提示返回错误信息,这样可以减少很多类似于“为啥我调接口参数都对还调不通”的问题的沟通成本。同时,严格的协议规范也可以避免一些意料之外的问题出现。例如:您请求的资源不支持 http 方法“POST
5.是否支持幂等;

这是作为一个接口而言,很需要明确的一点,尤其是在一些特殊的应用场景下,是否支持幂等是需要首先明确的。比如下面这个例子:

发放卡券的接口:/coupon/card/handOut.json POST

这是一个卡券系统里发放租金卡的接口,支持POST协议传参。由于很多种原因,同一类卡券被某个人领取时,都可能会产生接口被调用不支持一次的情形,比如网络抖动、用户快速点击、甚至是恶意刷接口等,我们希望,对于“同一个调用”,我们给用户返回的结果应该是一致的,这就是幂等。实现幂等的方式有几种,比如卡券系统就是通过生成订单号的形式完成的接口幂等;


6、充分考虑接口的可扩展性,避免做大而全的接口;

要根据实际业务场景定义接口,充分考虑接口的可扩展性。比如自如的APP首页数据接口,我们可以设计成整个首页就一个大接口,但是假如这样,未来再次改版APP,我们可能就需要完全重新写这个接口,但假如我们按模块区分接口,可能我们仅需要开发新增加的模块的接口,对于以前有的,在数据结构不调整只做样式改变的需求里,就可以减少工作量的开发。
另外这么做还有一个优点,尤其对移动端作用尤为突出,由于移动端对带宽有限,所以,接口中尽量不要返回无用的信息,只返回真正需要的数据,进而减少由于过多的数据量影响处理速度,最重要的是影响传输效率。


7、接口里尽量不做客户端可以处理的逻辑,减少服务端压力;

接口主要是提供给客户端数据的,对于能够在客端完成的逻辑处理,尽量由客端来处理(当然APP比较特殊,如果改动这部分逻辑需要发版,还是需要放在后端),进而减轻后端服务器的压力,让后端接口更加“专心”做好数据服务;


8、清晰的日志分类、记录以及归档规范;

接口日志很重要,无论对于追溯问题还是解决bug,都有着举足轻重的作用。所以,好的日志规范,是一个很好的习惯。日志主要有info和error两种(warning一般不做记录,或者很少用到),info日志一般用于记录现场,用来追溯问题;error日志一般用于协作我们查找bug,定位代码问题。

另外日志也需要定期做归档处理,防止机器磁盘被日志文件大量占用。

更高级的,可以接入一些日志搜集和分析工具,如ELK等,将日志信息持久化存储以及可视化展示,更加方便的对日志信息进行使用。


9、版本控制:
 
这一点,对于接口来讲,非常重要。在实际的场景中,维护多个版本是非常常见的事情,在系统的迭代升级过程中,无可避免的会增减返回参数及入参,修改返回数据的结构,甚至会废弃原有接口改为新的数据接口。所以,为了不影响老版本应用的正常使用,大部分应用后台都会针对性的维护多个接口版本。
一般来讲,有2中常用的方式:

1.每个接口有各自的版本,一般为接口添加个version的参数。 
2.整个接口系统有统一的版本,一般在URL中添加版本号,比如 http://api.zkbhj.com/v2。 

 

【性能和高可用】

1、接口的平均响应时长、支持的并发数、TPS;

这个很重要,无论是我们自己设计的接口,还是我们在使用第三方提供的接口时,我们都需要明确接口的平均响应时长,因为这直接关系到你系统的安全性问题!如果在接口调用时,没有设置合理的超时时间甚至都没有设置超时时间,那么一旦所以依赖的接口出现问题甚至服务不可用时,对你调用方系统来讲,将是致命的,雪崩式的系统崩溃,很大一部分就是由于这个原因造成的。所以为了不害人害己,设计以及最终提供的接口,一定要提供一个明确的接口平均响应时长,而且要在接口文档中写明并强烈建议接口调用方设置合理的超时时间,防止由于接口超时而造成雪崩式的连锁反应。


2、数据库和缓存的选择;

为了提高接口性能,合理的选择数据库和缓存很是总要。一般情况下,关系型存储我们一般都会选择MySQL数据库,缓存一般都选择Redis。当然,MySQL数据库的分库分表,加索引用事务、读写分离等,redis作为缓存使用时的缓存时长、缓存数据类型等,都有他们的使用原则和最佳实践,这里不做赘述。我们这里只讨论在何种场景下要使用缓存。比如查询类型的接口,如果要查询的数据并不是实时性要求很高的接口,那我们可以进行缓存处理,比如APP首页接口,一般都是CMS里面配置的一些图文信息,我们有必要做缓存处理。当然可能里面有一部分数据是需要实时的,比如自如寓的管家信息,那我们可以把这一部分内容做实时的处理。
 

3、限流、熔断和降级;

对于一些特殊的应用场景,比如抢红包、秒杀等,要对接口进行限流处理,方式短时间内的高并发请求将接口搞死;

接口熔断和降级,是为了解决系统不被拖死,不影响核心业务流程而采取的措施,比如获取用户信息列表,实时获取用户头像和昵称的接口暂时不可用(比如根据设置,10个请求里6个以上都超时,则判定为服务不可用,触发熔断机制),我们可以主动放弃调用(熔断),只返回核心数据uid等,昵称和头像暂时返回默认数据(降级);


4、消除单点,负载均衡;

对于任何一个接口服务,我们至少要有2台机器对外提供服务,禁止单点服务,单点一旦出问题,会直接造成服务不可用;

对于访问量很大的API服务,为了提供更加快速的接口响应,我们往往不是单台机器提供服务,而是有多台机器组成一个分布式集群对外提供服务。这个时候就会涉及到负载均衡,比如我们就会由nginx来做负载均衡,根据一定的策略机制,将接口请求平均的分发到不同的应用机器上进行处理和响应。进而提高接口的性能。


5、是否有第三方服务接口?

如果接口依赖了第三方服务接口,能用缓存就用缓存。这样可以进一步降低由于第三方接口不稳定给我们自己系统造成的波动。当然,也有一些第三方接口无法做缓存,比如就是要实时进行身份验证等,这个时候,超时时间的设置就尤为重要!


6、能异步处理的异步处理:

其实有很多场景下,一个接口里面的很多逻辑是可以异步处理的。举个例子:

比如用户注册场景,用户注册成功之后会给客户的邮箱发送一封激活邮件。常规的逻辑流程应该是,前段提交用户信息到注册接口,注册接口做各种校验,校验通过后,发送邮件,发送成功后,返回给前端告诉用户注册成功,请进入邮箱激活账号。其实,这个流程里的“发送邮件”就是可以拿出来异步处理的部分,当校验通过而且注册完成之后,我们把发送邮件这件事抛出去,交给另外一个就负责发邮件的任务进行处理(如我们现在有的补偿队列,或者是发一个MQ消息),然后直接返回给用户注册成功。这样,注册接口的平均响应时间一定会比第一种方案提高很多。


7、更高要求的高可用,可以采用异地机房部署;

在物理地域上就分开部署,两地同时崩溃的概率还是比较低的;


8、监控和报警;

在对接口建立高效的监控和报警机制,能够及时发现问题并通知到相应的人员进行第一时间的处理和跟进。


【稳定和安全】

1、身份验证;

在一些接口场景中,是要依赖于用户身份的,比如通过token还实现用户身份的验证;


2、接口防抓取和串改数据;

防止数据被轻易抓取到,我们可以采用https作为接口的网络传输协议,进而保证数据包不被轻易的就抓取和分析。即使这种情况下,依然被抓取到,我们还可以对传输的数据进行我们自己的加密处理,比如用对称加密算法AES或者非对称加密算法RSA,亦或是我们系统内部自己商定好的加密算法,对数据进行加密处理,这样,即使抓取到数据包,也很难分析出数据的原始信息。

对于防止数据被串改,可以使用sign验签,进一步防止接口参数被串改的可能性。


3、防刷;

接口防刷会有一些策略,根据实际的应用场景进行选择,比如增加图形验证码、接入智能验证码、时间戳限制单位时间内的调用次数、ip限制等;

另外监控很重要,及时发现异常的调用,进行封禁处理;


【其他】

1、是否需要支持跨域;

这一点是针对于H5提供接口时需要考虑的,因为一般情况下,实际的应用和接口所在的域并不是同一个域,基于浏览器的安全策略,对于XHR请求来讲,是不允许进行跨域请求的,所以,一般提供给H5的接口要支持跨域请求。当然解决跨域的方法也不在本次讨论的范围之内,目前主流的方式就是在服务器配置的header头信息中增加两项参数。



2、基于H5提供接口的一些安全性问题;

比如常见的CSRF攻击,我们可以在接口里验证 HTTP Referer字段、x-requested-with字段、header中增加token等,从一定程度上提高被CSRF攻击的门槛。 
 
3、在代码结构层面,尽量和其他部分分开;
 
API集中由同一个系统“模块”提供,尽量不要和页面等其他功能混合开发。例如下面的项目分层模式就是一个较好的实践方案:





 
即所有API接口均分布在api内部,不与pc(PC站页面)、mobile(M站页面)等混合在一起。 

4、文档:
 
好的接口,还有一项优点,就是会有为之配套的接口文档。如果希望降低接口文档的维护成本等,也可以使用开源的第三方自动化接口文档工具,比如swagger等。 查看全部
这里想和大家讨论的是在后台接口设计过程中,还有哪些方面需要考虑,以及还有哪些优秀的技术实践方案可以借鉴。

 【规范和最佳实践】

1.合理的接口命名;

接口的命名必须规范优雅,在未看到接口文档时,就可以根据接口的URL明白接口的功能是什么?

如下面的例子:
//好的接口命名示例
/customer/cert/search.json

//不好的接口命名示例
/customer/info/get.json

2.入参和出参的规范化定义,有统一的风格;

一个项目内的所有接口,必须有统一的风格,统一返回格式,约定业务层错误编码,每个编码可以携带明确的错误信息。出入参字段含义明确,采用统一的命名规范,如驼峰命名等。返回格式统一采用json格式。举一个例子:
{
"status": "failure",
"error_code": 100003,
"error_message": "未获取到用户信息",
"data":
}
status标识接口是否逻辑处理成功;error_code为不同类型错误信息对应的唯一错误码,error_message为错误信息的简要描述信息(注意某些数据或者信息是否可直接展示给用户),data则为需要返回给调用方的数据信息。
 
另外,每个参数一定要有明确且固定的数据格式,int就是int,string就是string,array就是array,object就是object,因为对于一些对数据类型要求比较严格的使用方,不明确的数据格式返回,可能会造成不可预知的错误。
 
下面给大家列一下Json里的六种基本数据格式: 


Number:整数或浮点数
String:字符串
Boolean:true 或 false
Array:数组包含在方括号中
Object:对象包含在大括号{}中
Null:空类型


 
3.接口的功能定义要具备单一性;

单一性是指接口要做的事情应该是一个比较单一的事情,比如登陆接口,登陆完成应该只是返回登陆成功以后一些用户信息如uid即可,但很多人为了减少接口交互,返回一大堆额外的数据。

但有时候对于一些内部系统接口来讲,为了实现通用性,可能会提供一些通用的查询接口,即在同一个接口内返回尽可能多的信息,但也不建议这么做,至少不是一个好的实践;


4.明确接口支持的协议;

接口要明确所支持的协议,是POST/GET/PUT/DELETE等的哪一个。一般来讲,同一个接口而言,尽量只支持一种协议,并且在接口被调用时,如果参数传递非接口定义协议,要明确提示返回错误信息,这样可以减少很多类似于“为啥我调接口参数都对还调不通”的问题的沟通成本。同时,严格的协议规范也可以避免一些意料之外的问题出现。例如:
您请求的资源不支持 http 方法“POST

5.是否支持幂等;

这是作为一个接口而言,很需要明确的一点,尤其是在一些特殊的应用场景下,是否支持幂等是需要首先明确的。比如下面这个例子:

发放卡券的接口:/coupon/card/handOut.json POST

这是一个卡券系统里发放租金卡的接口,支持POST协议传参。由于很多种原因,同一类卡券被某个人领取时,都可能会产生接口被调用不支持一次的情形,比如网络抖动、用户快速点击、甚至是恶意刷接口等,我们希望,对于“同一个调用”,我们给用户返回的结果应该是一致的,这就是幂等。实现幂等的方式有几种,比如卡券系统就是通过生成订单号的形式完成的接口幂等;


6、充分考虑接口的可扩展性,避免做大而全的接口;

要根据实际业务场景定义接口,充分考虑接口的可扩展性。比如自如的APP首页数据接口,我们可以设计成整个首页就一个大接口,但是假如这样,未来再次改版APP,我们可能就需要完全重新写这个接口,但假如我们按模块区分接口,可能我们仅需要开发新增加的模块的接口,对于以前有的,在数据结构不调整只做样式改变的需求里,就可以减少工作量的开发。
另外这么做还有一个优点,尤其对移动端作用尤为突出,由于移动端对带宽有限,所以,接口中尽量不要返回无用的信息,只返回真正需要的数据,进而减少由于过多的数据量影响处理速度,最重要的是影响传输效率。


7、接口里尽量不做客户端可以处理的逻辑,减少服务端压力;

接口主要是提供给客户端数据的,对于能够在客端完成的逻辑处理,尽量由客端来处理(当然APP比较特殊,如果改动这部分逻辑需要发版,还是需要放在后端),进而减轻后端服务器的压力,让后端接口更加“专心”做好数据服务;


8、清晰的日志分类、记录以及归档规范;

接口日志很重要,无论对于追溯问题还是解决bug,都有着举足轻重的作用。所以,好的日志规范,是一个很好的习惯。日志主要有info和error两种(warning一般不做记录,或者很少用到),info日志一般用于记录现场,用来追溯问题;error日志一般用于协作我们查找bug,定位代码问题。

另外日志也需要定期做归档处理,防止机器磁盘被日志文件大量占用。

更高级的,可以接入一些日志搜集和分析工具,如ELK等,将日志信息持久化存储以及可视化展示,更加方便的对日志信息进行使用。


9、版本控制:
 
这一点,对于接口来讲,非常重要。在实际的场景中,维护多个版本是非常常见的事情,在系统的迭代升级过程中,无可避免的会增减返回参数及入参,修改返回数据的结构,甚至会废弃原有接口改为新的数据接口。所以,为了不影响老版本应用的正常使用,大部分应用后台都会针对性的维护多个接口版本。
一般来讲,有2中常用的方式:


1.每个接口有各自的版本,一般为接口添加个version的参数。 
2.整个接口系统有统一的版本,一般在URL中添加版本号,比如 http://api.zkbhj.com/v2。 


 

【性能和高可用】

1、接口的平均响应时长、支持的并发数、TPS;

这个很重要,无论是我们自己设计的接口,还是我们在使用第三方提供的接口时,我们都需要明确接口的平均响应时长,因为这直接关系到你系统的安全性问题!如果在接口调用时,没有设置合理的超时时间甚至都没有设置超时时间,那么一旦所以依赖的接口出现问题甚至服务不可用时,对你调用方系统来讲,将是致命的,雪崩式的系统崩溃,很大一部分就是由于这个原因造成的。所以为了不害人害己,设计以及最终提供的接口,一定要提供一个明确的接口平均响应时长,而且要在接口文档中写明并强烈建议接口调用方设置合理的超时时间,防止由于接口超时而造成雪崩式的连锁反应。


2、数据库和缓存的选择;

为了提高接口性能,合理的选择数据库和缓存很是总要。一般情况下,关系型存储我们一般都会选择MySQL数据库,缓存一般都选择Redis。当然,MySQL数据库的分库分表,加索引用事务、读写分离等,redis作为缓存使用时的缓存时长、缓存数据类型等,都有他们的使用原则和最佳实践,这里不做赘述。我们这里只讨论在何种场景下要使用缓存。比如查询类型的接口,如果要查询的数据并不是实时性要求很高的接口,那我们可以进行缓存处理,比如APP首页接口,一般都是CMS里面配置的一些图文信息,我们有必要做缓存处理。当然可能里面有一部分数据是需要实时的,比如自如寓的管家信息,那我们可以把这一部分内容做实时的处理。
 

3、限流、熔断和降级;

对于一些特殊的应用场景,比如抢红包、秒杀等,要对接口进行限流处理,方式短时间内的高并发请求将接口搞死;

接口熔断和降级,是为了解决系统不被拖死,不影响核心业务流程而采取的措施,比如获取用户信息列表,实时获取用户头像和昵称的接口暂时不可用(比如根据设置,10个请求里6个以上都超时,则判定为服务不可用,触发熔断机制),我们可以主动放弃调用(熔断),只返回核心数据uid等,昵称和头像暂时返回默认数据(降级);


4、消除单点,负载均衡;

对于任何一个接口服务,我们至少要有2台机器对外提供服务,禁止单点服务,单点一旦出问题,会直接造成服务不可用;

对于访问量很大的API服务,为了提供更加快速的接口响应,我们往往不是单台机器提供服务,而是有多台机器组成一个分布式集群对外提供服务。这个时候就会涉及到负载均衡,比如我们就会由nginx来做负载均衡,根据一定的策略机制,将接口请求平均的分发到不同的应用机器上进行处理和响应。进而提高接口的性能。


5、是否有第三方服务接口?

如果接口依赖了第三方服务接口,能用缓存就用缓存。这样可以进一步降低由于第三方接口不稳定给我们自己系统造成的波动。当然,也有一些第三方接口无法做缓存,比如就是要实时进行身份验证等,这个时候,超时时间的设置就尤为重要!


6、能异步处理的异步处理:

其实有很多场景下,一个接口里面的很多逻辑是可以异步处理的。举个例子:

比如用户注册场景,用户注册成功之后会给客户的邮箱发送一封激活邮件。常规的逻辑流程应该是,前段提交用户信息到注册接口,注册接口做各种校验,校验通过后,发送邮件,发送成功后,返回给前端告诉用户注册成功,请进入邮箱激活账号。其实,这个流程里的“发送邮件”就是可以拿出来异步处理的部分,当校验通过而且注册完成之后,我们把发送邮件这件事抛出去,交给另外一个就负责发邮件的任务进行处理(如我们现在有的补偿队列,或者是发一个MQ消息),然后直接返回给用户注册成功。这样,注册接口的平均响应时间一定会比第一种方案提高很多。


7、更高要求的高可用,可以采用异地机房部署;

在物理地域上就分开部署,两地同时崩溃的概率还是比较低的;


8、监控和报警;

在对接口建立高效的监控和报警机制,能够及时发现问题并通知到相应的人员进行第一时间的处理和跟进。


【稳定和安全】

1、身份验证;

在一些接口场景中,是要依赖于用户身份的,比如通过token还实现用户身份的验证;


2、接口防抓取和串改数据;

防止数据被轻易抓取到,我们可以采用https作为接口的网络传输协议,进而保证数据包不被轻易的就抓取和分析。即使这种情况下,依然被抓取到,我们还可以对传输的数据进行我们自己的加密处理,比如用对称加密算法AES或者非对称加密算法RSA,亦或是我们系统内部自己商定好的加密算法,对数据进行加密处理,这样,即使抓取到数据包,也很难分析出数据的原始信息。

对于防止数据被串改,可以使用sign验签,进一步防止接口参数被串改的可能性。


3、防刷;

接口防刷会有一些策略,根据实际的应用场景进行选择,比如增加图形验证码、接入智能验证码、时间戳限制单位时间内的调用次数、ip限制等;

另外监控很重要,及时发现异常的调用,进行封禁处理;


【其他】

1、是否需要支持跨域;

这一点是针对于H5提供接口时需要考虑的,因为一般情况下,实际的应用和接口所在的域并不是同一个域,基于浏览器的安全策略,对于XHR请求来讲,是不允许进行跨域请求的,所以,一般提供给H5的接口要支持跨域请求。当然解决跨域的方法也不在本次讨论的范围之内,目前主流的方式就是在服务器配置的header头信息中增加两项参数。



2、基于H5提供接口的一些安全性问题;

比如常见的CSRF攻击,我们可以在接口里验证 HTTP Referer字段、x-requested-with字段、header中增加token等,从一定程度上提高被CSRF攻击的门槛。 
 
3、在代码结构层面,尽量和其他部分分开;
 
API集中由同一个系统“模块”提供,尽量不要和页面等其他功能混合开发。例如下面的项目分层模式就是一个较好的实践方案:

QQ截图20181112160646.jpg

 
即所有API接口均分布在api内部,不与pc(PC站页面)、mobile(M站页面)等混合在一起。 

4、文档:
 
好的接口,还有一项优点,就是会有为之配套的接口文档。如果希望降低接口文档的维护成本等,也可以使用开源的第三方自动化接口文档工具,比如swagger等。

JWT:完全前后端分离的项目如何做用户身份验证更安全?看这篇就够了!

前端开发zkbhj 发表了文章 • 0 个评论 • 4386 次浏览 • 2018-09-19 14:48 • 来自相关话题

在前后端分离开发时为什么需要用户认证呢?原因是由于HTTP协定是不储存状态的(stateless),这意味着当我们透过帐号密码验证一个使用者时,当下一个request请求时它就把刚刚的资料忘了。于是我们的程序就不知道谁是谁,就要再验证一次。所以为了保证系统安全,我们就需要验证用户否处于登录状态。

传统方式

前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个 token 和对应的用户id到数据库或Session中,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。

但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。

在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。

httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为 cookie 默认被发了出去。

另外,如果将验证信息保存在数据库中,后端每次都需要根据token查出用户id,这就增加了数据库的查询和存储开销。若把验证信息保存在session中,有加大了服务器端的存储压力。那我们可不可以不要服务器去查询呢?如果我们生成token遵循一定的规律,比如我们使用对称加密算法来加密用户id形成token,那么服务端以后其实只要解密该token就可以知道用户的id是什么了。不过呢,我只是举个例子而已,要是真这么做,只要你的对称加密算法泄露了,其他人可以通过这种加密方式进行伪造token,那么所有用户信息都不再安全了。恩,那用非对称加密算法来做呢,其实现在有个规范就是这样做的,就是我们接下来要介绍的 JWT。

Json Web Token(JWT)

JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:

简洁(Compact)

可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快

自包含(Self-contained)

负载中包含了所有用户所需要的信息,避免了多次查询数据库


JWT 组成





Header 头部

头部包含了两部分,token 类型和采用的加密算法
 {
"alg": "HS256",
"typ": "JWT"
}它会使用 Base64 编码组成 JWT 结构的第一部分,如果你使用Node.js,可以用Node.js的包base64url来得到这个字符串。

Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

Payload 负载

这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。
 {
"iss": "lion1ou JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "lion1ou@163.com"
}同样的,它会使用 Base64 编码组成 JWT 结构的第二部分

Signature 签名

前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

三个部分通过.连接在一起就是我们的 JWT 了,它可能长这个样子,长度貌似和你的加密算法和私钥有关系。eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s

其实到这一步可能就有人会想了,HTTP 请求总会带上 token,这样这个 token 传来传去占用不必要的带宽啊。如果你这么想了,那你可以去了解下 HTTP2,HTTP2 对头部进行了压缩,相信也解决了这个问题。

签名的目的

最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

信息暴露

在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?

是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。

因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。

JWT 使用




 
首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

和Session方式存储id的差异

Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。具体是否采用,需要在不同场景下用数据说话。

单点登录

Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:www.taobao.com,nv.taobao.com,nz.taobao.com,login.taobao.com。所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。

总结

JWT的主要作用在于(一)可附带用户信息,后端直接通过JWT获取相关信息。(二)使用本地保存,通过HTTP Header中的Authorization位提交验证。但其实关于JWT存放到哪里一直有很多讨论,有人说存放到本地存储,有人说存 cookie。个人偏向于放在本地存储,如果你有什么意见和看法欢迎提出。

参考文档:
https://segmentfault.com/a/1190000005783306 
https://ruiming.me/authentication-of-frontend-backend-separate-application/ 
 
总结和摘录自:
https://blog.csdn.net/kevin_lc ... 46723 查看全部
在前后端分离开发时为什么需要用户认证呢?原因是由于HTTP协定是不储存状态的(stateless),这意味着当我们透过帐号密码验证一个使用者时,当下一个request请求时它就把刚刚的资料忘了。于是我们的程序就不知道谁是谁,就要再验证一次。所以为了保证系统安全,我们就需要验证用户否处于登录状态。

传统方式

前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个 token 和对应的用户id到数据库或Session中,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。

但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。

在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。

httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为 cookie 默认被发了出去。

另外,如果将验证信息保存在数据库中,后端每次都需要根据token查出用户id,这就增加了数据库的查询和存储开销。若把验证信息保存在session中,有加大了服务器端的存储压力。那我们可不可以不要服务器去查询呢?如果我们生成token遵循一定的规律,比如我们使用对称加密算法来加密用户id形成token,那么服务端以后其实只要解密该token就可以知道用户的id是什么了。不过呢,我只是举个例子而已,要是真这么做,只要你的对称加密算法泄露了,其他人可以通过这种加密方式进行伪造token,那么所有用户信息都不再安全了。恩,那用非对称加密算法来做呢,其实现在有个规范就是这样做的,就是我们接下来要介绍的 JWT。

Json Web Token(JWT)

JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:


简洁(Compact)

可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快

自包含(Self-contained)

负载中包含了所有用户所需要的信息,避免了多次查询数据库



JWT 组成

006tNc79gy1fbv54tfilmj31120b2wl9.jpg

Header 头部

头部包含了两部分,token 类型和采用的加密算法
 
{
"alg": "HS256",
"typ": "JWT"
}
它会使用 Base64 编码组成 JWT 结构的第一部分,如果你使用Node.js,可以用Node.js的包base64url来得到这个字符串。


Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。


Payload 负载

这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。
 
{
"iss": "lion1ou JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "lion1ou@163.com"
}
同样的,它会使用 Base64 编码组成 JWT 结构的第二部分

Signature 签名

前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

三个部分通过.连接在一起就是我们的 JWT 了,它可能长这个样子,长度貌似和你的加密算法和私钥有关系。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s

其实到这一步可能就有人会想了,HTTP 请求总会带上 token,这样这个 token 传来传去占用不必要的带宽啊。如果你这么想了,那你可以去了解下 HTTP2,HTTP2 对头部进行了压缩,相信也解决了这个问题。

签名的目的

最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

信息暴露

在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?

是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。

因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。

JWT 使用

006tNc79gy1fbv63pzqocj30pj0h8t9m.jpg
 
  1. 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
  2. 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。
  3. 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
  4. 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
  5. 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
  6. 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。


和Session方式存储id的差异

Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。具体是否采用,需要在不同场景下用数据说话。

单点登录

Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:www.taobao.com,nv.taobao.com,nz.taobao.com,login.taobao.com。所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。

总结

JWT的主要作用在于(一)可附带用户信息,后端直接通过JWT获取相关信息。(二)使用本地保存,通过HTTP Header中的Authorization位提交验证。但其实关于JWT存放到哪里一直有很多讨论,有人说存放到本地存储,有人说存 cookie。个人偏向于放在本地存储,如果你有什么意见和看法欢迎提出。

参考文档:
https://segmentfault.com/a/1190000005783306 
https://ruiming.me/authentication-of-frontend-backend-separate-application/ 
 
总结和摘录自:
https://blog.csdn.net/kevin_lc ... 46723

PHP升级7.2之后需要注意的“坑”

PHPzkbhj 发表了文章 • 0 个评论 • 3109 次浏览 • 2018-09-12 20:25 • 来自相关话题

最近升级了PHP版本,从7.1升级到7.2,升级前版本:
PHP 7.1.14 (cli) (built: Feb 2 2018 08:42:59) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.1.14, Copyright (c) 1999-2018, by Zend Technologies
with Xdebug v2.6.0, Copyright (c) 2002-2018, by Derick Rethans升级后版本:
PHP 7.2.2 (cli) (built: Feb 24 2018 17:51:12) ( ZTS DEBUG )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.2.2, Copyright (c) 1999-2018, by Zend Technologies升级完成之后发现有几个框架在使用时都出现了问题,主要原因集中在7.2之后废弃了一些功能,下面列出几个常见的问题:

1、each函数已被废弃:

之前版本写法:<?php
$array = array();
each($array);

// Deprecated: The each() function is deprecated. This message will be suppressed on further calls在7.2版本中会提示过时,可以使用foreach替代each方法,也可以自己修改each方法替代:
<?php
function func_new_each(&$array){
$res = array();
$key = key($array);
if($key !== null){
next($array);
$res[1] = $res['value'] = $array[$key];
$res[0] = $res['key'] = $key;
}else{
$res = false;
}
return $res;
}
2、当传递一个无效参数时,count()函数将抛出warning警告:

之前版本写法<?php
count('');

// Warning: count(): Parameter must be an array or an object that implements Countable在7.2版本中将严格执行类型区分,参数类型不正确,将会出现警告,所以需要在使用count方法时注意参数的值,不过也可以通过自己修改方法来替代(不建议):
<?php
function func_new_count($array_or_countable,$mode = COUNT_NORMAL){
if(is_array($array_or_countable) || is_object($array_or_countable)){
return count($array_or_countable, $mode);
}else{
return 0;
}
}3、create_function被废弃,可以用匿名函数来代替:

之前版本写法:<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New anonymous function: $newfunc\n";
echo $newfunc(2, M_E) . "\n";
// outputs
// New anonymous function: lambda_1
// ln(2) + ln(2.718281828459) = 1.6931471805599

// Warning This function has been DEPRECATED as of PHP 7.2.0. Relying on this function is highly discouraged.在7.2版本中会有警告提示,可修改为匿名函数来替代:
<?php
$newfunc = function ($a,$b){
return "ln($a) + ln($b) = " . log($a * $b);
};
echo $newfunc(2, M_E) . "\n";以上就是升级之后暂时遇到的几个问题,其它相关修改可详看链家产品技术团队做的翻译及整理:PHP7.2 版本指南
https://mp.weixin.qq.com/s/60pohj2n7Pxba3G9vY92yg 查看全部
最近升级了PHP版本,从7.1升级到7.2,升级前版本:
PHP 7.1.14 (cli) (built: Feb  2 2018 08:42:59) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.1.14, Copyright (c) 1999-2018, by Zend Technologies
with Xdebug v2.6.0, Copyright (c) 2002-2018, by Derick Rethans
升级后版本:
PHP 7.2.2 (cli) (built: Feb 24 2018 17:51:12) ( ZTS DEBUG )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.2.2, Copyright (c) 1999-2018, by Zend Technologies
升级完成之后发现有几个框架在使用时都出现了问题,主要原因集中在7.2之后废弃了一些功能,下面列出几个常见的问题:

1、each函数已被废弃:

之前版本写法:
<?php
$array = array();
each($array);

// Deprecated: The each() function is deprecated. This message will be suppressed on further calls
在7.2版本中会提示过时,可以使用foreach替代each方法,也可以自己修改each方法替代:
<?php
function func_new_each(&$array){
$res = array();
$key = key($array);
if($key !== null){
next($array);
$res[1] = $res['value'] = $array[$key];
$res[0] = $res['key'] = $key;
}else{
$res = false;
}
return $res;
}

2、当传递一个无效参数时,count()函数将抛出warning警告:

之前版本写法
<?php
count('');

// Warning: count(): Parameter must be an array or an object that implements Countable
在7.2版本中将严格执行类型区分,参数类型不正确,将会出现警告,所以需要在使用count方法时注意参数的值,不过也可以通过自己修改方法来替代(不建议):
<?php
function func_new_count($array_or_countable,$mode = COUNT_NORMAL){
if(is_array($array_or_countable) || is_object($array_or_countable)){
return count($array_or_countable, $mode);
}else{
return 0;
}
}
3、create_function被废弃,可以用匿名函数来代替:

之前版本写法:
<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New anonymous function: $newfunc\n";
echo $newfunc(2, M_E) . "\n";
// outputs
// New anonymous function: lambda_1
// ln(2) + ln(2.718281828459) = 1.6931471805599

// Warning This function has been DEPRECATED as of PHP 7.2.0. Relying on this function is highly discouraged.
在7.2版本中会有警告提示,可修改为匿名函数来替代:
<?php
$newfunc = function ($a,$b){
return "ln($a) + ln($b) = " . log($a * $b);
};
echo $newfunc(2, M_E) . "\n";
以上就是升级之后暂时遇到的几个问题,其它相关修改可详看链家产品技术团队做的翻译及整理:PHP7.2 版本指南
https://mp.weixin.qq.com/s/60pohj2n7Pxba3G9vY92yg

PHP函数的参数里,前面三个点的形式...$args是什么含义和用法?

PHPzkbhj 发表了文章 • 0 个评论 • 4981 次浏览 • 2018-09-07 13:30 • 来自相关话题

这是PHP5.6添加的功能(一种语法糖): 
可以通过...将函数参数存储在紧接的可遍历的变量中。
 
function add($a, $b, $c)
{
return $a + $b + $c;
}

$num=[2, 3];
echo add(1, ...$num); //6 
上面的第二个和第三个参数将会从$num中循环赋值($b为2,$c为3)。
 
手册地址:http://php.net/manual/zh/migra ... s.php 查看全部
这是PHP5.6添加的功能(一种语法糖): 
可以通过...将函数参数存储在紧接的可遍历的变量中。
 
function add($a, $b, $c)
{
return $a + $b + $c;
}

$num=[2, 3];
echo add(1, ...$num); //6
 
上面的第二个和第三个参数将会从$num中循环赋值($b为2,$c为3)。
 
手册地址:http://php.net/manual/zh/migra ... s.php

PHP获取远程文件的mime类型的方法

PHPzkbhj 发表了文章 • 0 个评论 • 3739 次浏览 • 2018-09-06 19:45 • 来自相关话题

 /**
* 获取远程或本地文件信息
* @param string $strUrl 远程文件或本地文件地址
* @param integer $intType 调用方式(1:get_headers 2:fsocketopen 3:curl 4:本地文件)
* @param array $arrOptional
* @return array
* @author mengdj<mengdj#outlook.com>
*/
function remote_filesize($strUrl,$intType=1,$arrOptional=array()){
$arrRet=array(
"length"=>0, //大小,字节为单位
"mime"=>"", //mime类型
"filename"=>"", //文件名
"status"=>0 //状态码
);
switch($intType){
case 1:
//利用get_headers函数
if(($arrTmp=get_headers($strUrl,true))){
$arrRet=array("length"=>$arrTmp['Content-Length'],"mime"=>$arrTmp['Content-Type']);
if(preg_match('/filename=\"(.*)\"/si',$arrTmp['Content-Disposition'],$arr)){
$arrRet["filename"]=$arr[1];
}
if(preg_match('/\s(\d+)\s/',$arrTmp[0],$arr)){
$arrRet["status"]=$arr[1];
}
}
break;
case 2:
//利用fsocket
if(($arrUrl=parse_url($strUrl))){
if($fp=@fsockopen($arrUrl['host'],empty($arrUrl['port'])?80:$arrUrl['port'],$error)){
@fputs($fp,"GET ".(empty($arrUrl['path'])?'/':$arrUrl['path'])." HTTP/1.1\r\n");
@fputs($fp,"Host: $arrUrl[host]\r\n");
@fputs($fp,"Connection: Close\r\n\r\n");
while(!feof($fp)){
$tmp=fgets($fp);
if(trim($tmp)==''){
//此行代码只读到头信息即可
break;
}else{
(preg_match('/(HTTP.*)(\s\d{3}\s)/',$tmp,$arr))&&$arrRet['status']=trim($arr[2]);
(preg_match('/Content-Length:(.*)/si',$tmp,$arr))&&$arrRet['length']=trim($arr[1]);
(preg_match('/Content-Type:(.*)/si',$tmp,$arr))&&$arrRet['mime']=trim($arr[1]);
(preg_match('/filename=\"(.*)\"/si',$tmp,$arr))&&$arrRet['filename']=trim($arr[1]);
}
}
@fclose($fp);
}
}
break;
case 3:
//利用curl
if(($ch=curl_init($strUrl))){
curl_setopt($ch,CURLOPT_HEADER,1);
curl_setopt($ch,CURLOPT_NOBODY,1);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
if(isset($arrOptional['user'])&&isset($arrOptional['password'])){
$headers=array('Authorization: Basic '.base64_encode($arrOptional['user'].':'.$arrOptional['password']));
curl_setopt($ch,CURLOPT_HTTPHEADER,$headers);
}
$tmp=curl_exec($ch);
curl_close($ch);
(preg_match('/Content-Length:\s([0-9].+?)\s/',$tmp,$arr))&&$arrRet['length']=trim($arr[1]);
(preg_match('/Content-Type:\s(.*)\s/',$tmp,$arr))&&$arrRet['mime']=trim($arr[1]);
(preg_match('/filename=\"(.*)\"/i',$tmp,$arr))&&$arrRet['filename']=trim($arr[1]);
(preg_match('/(HTTP.*)(\s\d{3}\s)/',$tmp,$arr))&&$arrRet['status']=trim($arr[2]);
}
break;
case 4:
//本地处理
if(file_exists($strUrl)) {
$arrRet=array(
"length"=>filesize($strUrl),
"mime" =>mime_content_type($strUrl),
"filename"=>basename($strUrl),
"status"=>200
);
}else{
$arrRet=array(
"length"=>0,
"mime" =>'',
"filename"=>basename($strUrl),
"status"=>404
);
}
break;
}
if(isset($arrOptional['getimagesize'])&&$arrRet['status']=='200'){
if(($arrTmp=@getimagesize($strUrl))){
$arrRet['width']=$arrTmp[0];
$arrRet['height']=$arrTmp[1];
$arrRet['type']=$arrTmp[2];
$arrRet['tag']=$arrTmp[3];
$arrRet['bits']=$arrTmp['bits'];
$arrRet['channels']=$arrTmp['channels'];
!isset($arrRet['mime'])&&$arrRet['mime']=$arrTmp['mime'];
}
}
return $arrRet;
} 查看全部
 
/**
* 获取远程或本地文件信息
* @param string $strUrl 远程文件或本地文件地址
* @param integer $intType 调用方式(1:get_headers 2:fsocketopen 3:curl 4:本地文件)
* @param array $arrOptional
* @return array
* @author mengdj<mengdj#outlook.com>
*/
function remote_filesize($strUrl,$intType=1,$arrOptional=array()){
$arrRet=array(
"length"=>0, //大小,字节为单位
"mime"=>"", //mime类型
"filename"=>"", //文件名
"status"=>0 //状态码
);
switch($intType){
case 1:
//利用get_headers函数
if(($arrTmp=get_headers($strUrl,true))){
$arrRet=array("length"=>$arrTmp['Content-Length'],"mime"=>$arrTmp['Content-Type']);
if(preg_match('/filename=\"(.*)\"/si',$arrTmp['Content-Disposition'],$arr)){
$arrRet["filename"]=$arr[1];
}
if(preg_match('/\s(\d+)\s/',$arrTmp[0],$arr)){
$arrRet["status"]=$arr[1];
}
}
break;
case 2:
//利用fsocket
if(($arrUrl=parse_url($strUrl))){
if($fp=@fsockopen($arrUrl['host'],empty($arrUrl['port'])?80:$arrUrl['port'],$error)){
@fputs($fp,"GET ".(empty($arrUrl['path'])?'/':$arrUrl['path'])." HTTP/1.1\r\n");
@fputs($fp,"Host: $arrUrl[host]\r\n");
@fputs($fp,"Connection: Close\r\n\r\n");
while(!feof($fp)){
$tmp=fgets($fp);
if(trim($tmp)==''){
//此行代码只读到头信息即可
break;
}else{
(preg_match('/(HTTP.*)(\s\d{3}\s)/',$tmp,$arr))&&$arrRet['status']=trim($arr[2]);
(preg_match('/Content-Length:(.*)/si',$tmp,$arr))&&$arrRet['length']=trim($arr[1]);
(preg_match('/Content-Type:(.*)/si',$tmp,$arr))&&$arrRet['mime']=trim($arr[1]);
(preg_match('/filename=\"(.*)\"/si',$tmp,$arr))&&$arrRet['filename']=trim($arr[1]);
}
}
@fclose($fp);
}
}
break;
case 3:
//利用curl
if(($ch=curl_init($strUrl))){
curl_setopt($ch,CURLOPT_HEADER,1);
curl_setopt($ch,CURLOPT_NOBODY,1);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
if(isset($arrOptional['user'])&&isset($arrOptional['password'])){
$headers=array('Authorization: Basic '.base64_encode($arrOptional['user'].':'.$arrOptional['password']));
curl_setopt($ch,CURLOPT_HTTPHEADER,$headers);
}
$tmp=curl_exec($ch);
curl_close($ch);
(preg_match('/Content-Length:\s([0-9].+?)\s/',$tmp,$arr))&&$arrRet['length']=trim($arr[1]);
(preg_match('/Content-Type:\s(.*)\s/',$tmp,$arr))&&$arrRet['mime']=trim($arr[1]);
(preg_match('/filename=\"(.*)\"/i',$tmp,$arr))&&$arrRet['filename']=trim($arr[1]);
(preg_match('/(HTTP.*)(\s\d{3}\s)/',$tmp,$arr))&&$arrRet['status']=trim($arr[2]);
}
break;
case 4:
//本地处理
if(file_exists($strUrl)) {
$arrRet=array(
"length"=>filesize($strUrl),
"mime" =>mime_content_type($strUrl),
"filename"=>basename($strUrl),
"status"=>200
);
}else{
$arrRet=array(
"length"=>0,
"mime" =>'',
"filename"=>basename($strUrl),
"status"=>404
);
}
break;
}
if(isset($arrOptional['getimagesize'])&&$arrRet['status']=='200'){
if(($arrTmp=@getimagesize($strUrl))){
$arrRet['width']=$arrTmp[0];
$arrRet['height']=$arrTmp[1];
$arrRet['type']=$arrTmp[2];
$arrRet['tag']=$arrTmp[3];
$arrRet['bits']=$arrTmp['bits'];
$arrRet['channels']=$arrTmp['channels'];
!isset($arrRet['mime'])&&$arrRet['mime']=$arrTmp['mime'];
}
}
return $arrRet;
}

PHP如何实现对图片指定位置(可多个)进行打码处理

PHPzkbhj 发表了文章 • 0 个评论 • 3174 次浏览 • 2018-09-05 16:43 • 来自相关话题

<?php
/**
* 图片通用处理逻辑类
*/
namespace common\models\logics;

use Yii;
use yii\helpers\Json;


class ImageLogic
{
/**
* PHP将网页上的图片攫取到本地存储
* @param $imgUrl 图片url地址
* @param string $saveDir 本地存储路径 默认存储在当前路径
* @param null $fileName 图片存储到本地的文件名
* @return mix
*/
public static function crabImage($imgUrl, $fileName=null, $saveDir='/tmp/remote/'){
if(empty($imgUrl)){
return false;
}

//获取图片信息大小
$imgSize = getImageSize($imgUrl);
if(!in_array($imgSize['mime'],array('image/jpg', 'image/gif', 'image/png', 'image/jpeg'),true)){
return false;
}

//获取后缀名
$_mime = explode('/', $imgSize['mime']);
$_ext = '.'.end($_mime);

if(empty($fileName)){ //生成唯一的文件名
$fileName = uniqid(time(),true).$_ext;
}else{
$fileName .= $_ext;
}

//判断你是否已经本地有该图片,如果有则直接返回
if(file_exists($saveDir.$fileName)){
return $saveDir.$fileName;
}

//开始攫取
ob_start();
readfile($imgUrl);
$imgInfo = ob_get_contents();
ob_end_clean();

if(!file_exists($saveDir)){
mkdir($saveDir,0777,true);
}
$fp = fopen($saveDir.$fileName, 'a');
$imgLen = strlen($imgInfo); //计算图片源码大小
$_inx = 204800; //每次写入200k
$_time = ceil($imgLen/$_inx);
for($i=0; $i<$_time; $i++){
fwrite($fp,substr($imgInfo, $i*$_inx, $_inx));
}
fclose($fp);

return $saveDir.$fileName;
}


/**
* 将百度AI返回的位置信息转化为大码方法可以是别的位置坐标信息
* 百度:位置数组(坐标0点为左上角)
* left:表示定位位置的长方形左上顶点的水平坐标
* top:表示定位位置的长方形左上顶点的垂直坐标
* width:表示定位位置的长方形的宽度
* height:表示定位位置的长方形的高度
*
* 大码要求的坐标信息(坐标0点为左上角)
* x1:起点横坐标
* y1:起点纵坐标
* x2:终点横坐标
* y2:终点纵坐标
*/
public static function getLocationReal($location = )
{
if(empty($location))
$position = ;

//内容周围padding
$padding = 5;

//循环计算每个位置的坐标数据
foreach($location as $key => $value){
$position[$key]['x1'] = $value['left'] - $padding;
$position[$key]['y1'] = $value['top'] - $padding;
$position[$key]['x2'] = $value['left'] + $value['width'] + $padding;
$position[$key]['y2'] = $value['top'] + $value['height'] + $padding;
}

return $position;

}


/** 图片局部打马赛克
* @param String $source 原图
* @param Stirng $target 生成的图片
* @param int $x1 起点横坐标
* @param int $y1 起点纵坐标
* @param int $x2 终点横坐标
* @param int $y2 终点纵坐标
* @param int $deep 深度,数字越大越模糊
* @return boolean
*/
public static function imageMosaics($source, $target, $x1, $y1, $x2, $y2, $deep = 6){

// 判断原图是否存在
if(!file_exists($source)){
return false;
}

// 获取原图信息
list($o_width, $o_height, $o_type) = getimagesize($source);

// 判断区域是否超出图片
if($x1>$o_width || $x1<0 || $x2>$o_width || $x2<0 || $y1>$o_height || $y1<0 || $y2>$o_height || $y2<0){
return false;
}

switch($o_type){
case 1: $source_img = imagecreatefromgif($source); break;
case 2: $source_img = imagecreatefromjpeg($source); break;
case 3: $source_img = imagecreatefrompng($source); break;
default:
return false;
}

// 打马赛克
for($x=$x1; $x<$x2; $x=$x+$deep){
for($y=$y1; $y<$y2; $y=$y+$deep){
$color = imagecolorat($source_img, $x+round($deep/2), $y+round($deep/2));
imagefilledrectangle($source_img, $x, $y, $x+$deep, $y+$deep, $color);
}
}

// 生成图片
switch($o_type){
case 1: imagegif($source_img, $target); break;
case 2: imagejpeg($source_img, $target); break;
case 3: imagepng($source_img, $target); break;
}

return is_file($target)? true : false;

}

/**
* 在图片固定位置(支持多个位置)打码
* @param String $source 原图
* @param Stirng $target 生成的图片
* @param array $position 大码位置信息,可能有多个需要打码的位置
* @param int $position->x1 起点横坐标
* @param int $position->y1 起点纵坐标
* @param int $position->x2 终点横坐标
* @param int $position->y2 终点纵坐标
* @param int $deep 深度,数字越大越模糊
* return string 返回处理后的本地图片地址,如 /tmp/convert/1a987608924ce098fc8ccd3deaac8d09.png
*/
public static function imageAddMosaics($source, $target, $position, $deep = 6)
{
//默认生成临时文件地址
$target = empty($target) ? '/tmp/
convert/'.md5(Json::encode($position)).'.png' : $target;

//将要打码的位置信息从百度模式转为本地模式
$position = self::getLocationReal($position);

//判断图片是否为远程图片
if(strpos($source,'http') !==false){
$source = self::crabImage($source,md5(Json::encode($position)));
}

//循环处理图片打码
foreach ($position as $key => $value) {

//如果是第一次进行打码操作
if($key == 0){
self::imageMosaics($source, $target, $value['x1'], $value['y1'], $value['x2'], $value['y2'], 6);
}else{
self::imageMosaics($target, $target, $value['x1'], $value['y1'], $value['x2'], $value['y2'], 6);
}
}

return $target;

}

}


//使用
use common\models\logics\ImageLogic;
$source = 'https://qiniu.zkbhj.com/common/images/jiagou/%E5%87%AF%E5%86%B0%E7%A7%91%E6%8A%80%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84.png';
$target = '';
$position = [
0 => [
'width'=>656,
'top'=>452,
'left'=>110,
'height'=>35

],
1 => [
'width'=>748,
'top'=>1400,
'left'=>188,
'height'=>39

]
];

$img = ImageLogic::imageAddMosaics($source, $target, $position);




需要用到百度AI图片文本内容识别接口,参考:http://ai.baidu.com/docs#/OCR-API/0d9adafa

请求百度图片识别接口代码示例:
<?php

/**
* 发起http post请求(REST API), 并获取REST请求的结果
* @param string $url
* @param string $param
* @return - http response body if succeeds, else false.
*/
function request_post($url = '', $param = '')
{
if (empty($url) || empty($param)) {
return false;
}

$postUrl = $url;
$curlPost = $param;
// 初始化curl
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $postUrl);
curl_setopt($curl, CURLOPT_HEADER, 0);
// 要求结果为字符串且输出到屏幕上
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
// post提交方式
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $curlPost);
// 运行curl
$data = curl_exec($curl);
curl_close($curl);

return $data;
}
//token需要通过自己的appid和secretkey获取
$token = '24.6a792a76b5edaf7b8d48f65a8843c3c5.2592000.1538639572.282335-11769895';
$url = 'https://aip.baidubce.com/rest/2.0/ocr/v1/general?access_token=' . $token;
$img = file_get_contents('source.png');
$img = base64_encode($img);
$bodys = array(
"image" => $img
);
$res = request_post($url, $bodys);
echo $res;




  查看全部
<?php
/**
* 图片通用处理逻辑类
*/
namespace common\models\logics;

use Yii;
use yii\helpers\Json;


class ImageLogic
{
/**
* PHP将网页上的图片攫取到本地存储
* @param $imgUrl 图片url地址
* @param string $saveDir 本地存储路径 默认存储在当前路径
* @param null $fileName 图片存储到本地的文件名
* @return mix
*/
public static function crabImage($imgUrl, $fileName=null, $saveDir='/tmp/remote/'){
if(empty($imgUrl)){
return false;
}

//获取图片信息大小
$imgSize = getImageSize($imgUrl);
if(!in_array($imgSize['mime'],array('image/jpg', 'image/gif', 'image/png', 'image/jpeg'),true)){
return false;
}

//获取后缀名
$_mime = explode('/', $imgSize['mime']);
$_ext = '.'.end($_mime);

if(empty($fileName)){ //生成唯一的文件名
$fileName = uniqid(time(),true).$_ext;
}else{
$fileName .= $_ext;
}

//判断你是否已经本地有该图片,如果有则直接返回
if(file_exists($saveDir.$fileName)){
return $saveDir.$fileName;
}

//开始攫取
ob_start();
readfile($imgUrl);
$imgInfo = ob_get_contents();
ob_end_clean();

if(!file_exists($saveDir)){
mkdir($saveDir,0777,true);
}
$fp = fopen($saveDir.$fileName, 'a');
$imgLen = strlen($imgInfo); //计算图片源码大小
$_inx = 204800; //每次写入200k
$_time = ceil($imgLen/$_inx);
for($i=0; $i<$_time; $i++){
fwrite($fp,substr($imgInfo, $i*$_inx, $_inx));
}
fclose($fp);

return $saveDir.$fileName;
}


/**
* 将百度AI返回的位置信息转化为大码方法可以是别的位置坐标信息
* 百度:位置数组(坐标0点为左上角)
* left:表示定位位置的长方形左上顶点的水平坐标
* top:表示定位位置的长方形左上顶点的垂直坐标
* width:表示定位位置的长方形的宽度
* height:表示定位位置的长方形的高度
*
* 大码要求的坐标信息(坐标0点为左上角)
* x1:起点横坐标
* y1:起点纵坐标
* x2:终点横坐标
* y2:终点纵坐标
*/
public static function getLocationReal($location = )
{
if(empty($location))
$position = ;

//内容周围padding
$padding = 5;

//循环计算每个位置的坐标数据
foreach($location as $key => $value){
$position[$key]['x1'] = $value['left'] - $padding;
$position[$key]['y1'] = $value['top'] - $padding;
$position[$key]['x2'] = $value['left'] + $value['width'] + $padding;
$position[$key]['y2'] = $value['top'] + $value['height'] + $padding;
}

return $position;

}


/** 图片局部打马赛克
* @param String $source 原图
* @param Stirng $target 生成的图片
* @param int $x1 起点横坐标
* @param int $y1 起点纵坐标
* @param int $x2 终点横坐标
* @param int $y2 终点纵坐标
* @param int $deep 深度,数字越大越模糊
* @return boolean
*/
public static function imageMosaics($source, $target, $x1, $y1, $x2, $y2, $deep = 6){

// 判断原图是否存在
if(!file_exists($source)){
return false;
}

// 获取原图信息
list($o_width, $o_height, $o_type) = getimagesize($source);

// 判断区域是否超出图片
if($x1>$o_width || $x1<0 || $x2>$o_width || $x2<0 || $y1>$o_height || $y1<0 || $y2>$o_height || $y2<0){
return false;
}

switch($o_type){
case 1: $source_img = imagecreatefromgif($source); break;
case 2: $source_img = imagecreatefromjpeg($source); break;
case 3: $source_img = imagecreatefrompng($source); break;
default:
return false;
}

// 打马赛克
for($x=$x1; $x<$x2; $x=$x+$deep){
for($y=$y1; $y<$y2; $y=$y+$deep){
$color = imagecolorat($source_img, $x+round($deep/2), $y+round($deep/2));
imagefilledrectangle($source_img, $x, $y, $x+$deep, $y+$deep, $color);
}
}

// 生成图片
switch($o_type){
case 1: imagegif($source_img, $target); break;
case 2: imagejpeg($source_img, $target); break;
case 3: imagepng($source_img, $target); break;
}

return is_file($target)? true : false;

}

/**
* 在图片固定位置(支持多个位置)打码
* @param String $source 原图
* @param Stirng $target 生成的图片
* @param array $position 大码位置信息,可能有多个需要打码的位置
* @param int $position->x1 起点横坐标
* @param int $position->y1 起点纵坐标
* @param int $position->x2 终点横坐标
* @param int $position->y2 终点纵坐标
* @param int $deep 深度,数字越大越模糊
* return string 返回处理后的本地图片地址,如 /tmp/convert/1a987608924ce098fc8ccd3deaac8d09.png
*/
public static function imageAddMosaics($source, $target, $position, $deep = 6)
{
//默认生成临时文件地址
$target = empty($target) ? '/tmp/
convert/'.md5(Json::encode($position)).'.png' : $target;

//将要打码的位置信息从百度模式转为本地模式
$position = self::getLocationReal($position);

//判断图片是否为远程图片
if(strpos($source,'http') !==false){
$source = self::crabImage($source,md5(Json::encode($position)));
}

//循环处理图片打码
foreach ($position as $key => $value) {

//如果是第一次进行打码操作
if($key == 0){
self::imageMosaics($source, $target, $value['x1'], $value['y1'], $value['x2'], $value['y2'], 6);
}else{
self::imageMosaics($target, $target, $value['x1'], $value['y1'], $value['x2'], $value['y2'], 6);
}
}

return $target;

}

}


//使用
use common\models\logics\ImageLogic;
$source = 'https://qiniu.zkbhj.com/common/images/jiagou/%E5%87%AF%E5%86%B0%E7%A7%91%E6%8A%80%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84.png';
$target = '';
$position = [
0 => [
'width'=>656,
'top'=>452,
'left'=>110,
'height'=>35

],
1 => [
'width'=>748,
'top'=>1400,
'left'=>188,
'height'=>39

]
];

$img = ImageLogic::imageAddMosaics($source, $target, $position);




需要用到百度AI图片文本内容识别接口,参考:http://ai.baidu.com/docs#/OCR-API/0d9adafa

请求百度图片识别接口代码示例:
<?php

/**
* 发起http post请求(REST API), 并获取REST请求的结果
* @param string $url
* @param string $param
* @return - http response body if succeeds, else false.
*/
function request_post($url = '', $param = '')
{
if (empty($url) || empty($param)) {
return false;
}

$postUrl = $url;
$curlPost = $param;
// 初始化curl
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $postUrl);
curl_setopt($curl, CURLOPT_HEADER, 0);
// 要求结果为字符串且输出到屏幕上
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
// post提交方式
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $curlPost);
// 运行curl
$data = curl_exec($curl);
curl_close($curl);

return $data;
}
//token需要通过自己的appid和secretkey获取
$token = '24.6a792a76b5edaf7b8d48f65a8843c3c5.2592000.1538639572.282335-11769895';
$url = 'https://aip.baidubce.com/rest/2.0/ocr/v1/general?access_token=' . $token;
$img = file_get_contents('source.png');
$img = base64_encode($img);
$bodys = array(
"image" => $img
);
$res = request_post($url, $bodys);
echo $res;

QQ截图20180905165443.jpg

 

PHP给图片选定区域打上马赛克的方法

PHPzkbhj 发表了文章 • 0 个评论 • 2473 次浏览 • 2018-09-04 21:11 • 来自相关话题

原理:

对图片中选定区域的每一像素,增加若干宽度及高度,生成矩型。而每一像素的矩型重叠在一起,就形成了马赛克效果。

本例使用GD库的imagecolorat获取像素颜色,使用imagefilledrectangle画矩型。<?php
/** 图片局部打马赛克
* @param String $source 原图
* @param Stirng $dest 生成的图片
* @param int $x1 起点横坐标
* @param int $y1 起点纵坐标
* @param int $x2 终点横坐标
* @param int $y2 终点纵坐标
* @param int $deep 深度,数字越大越模糊
* @return boolean
*/
function imageMosaics($source, $dest, $x1, $y1, $x2, $y2, $deep){

// 判断原图是否存在
if(!file_exists($source)){
return false;
}

// 获取原图信息
list($owidth, $oheight, $otype) = getimagesize($source);

// 判断区域是否超出图片
if($x1>$owidth || $x1<0 || $x2>$owidth || $x2<0 || $y1>$oheight || $y1<0 || $y2>$oheight || $y2<0){
return false;
}

switch($otype){
case 1: $source_img = imagecreatefromgif($source); break;
case 2: $source_img = imagecreatefromjpeg($source); break;
case 3: $source_img = imagecreatefrompng($source); break;
default:
return false;
}

// 打马赛克
for($x=$x1; $x<$x2; $x=$x+$deep){
for($y=$y1; $y<$y2; $y=$y+$deep){
$color = imagecolorat($source_img, $x+round($deep/2), $y+round($deep/2));
imagefilledrectangle($source_img, $x, $y, $x+$deep, $y+$deep, $color);
}
}

// 生成图片
switch($otype){
case 1: imagegif($source_img, $dest); break;
case 2: imagejpeg($source_img, $dest); break;
case 3: imagepng($source_img, $dest); break;
}

return is_file($dest)? true : false;

}

$source = 'source.jpg';
$dest = 'dest.jpg';

$flag = imageMosaics($source, $dest, 176, 98, 273, 197, 4);
echo '<img src="'.$source.'">';
echo '<img src="'.$dest.'">';

?> 查看全部
原理:

对图片中选定区域的每一像素,增加若干宽度及高度,生成矩型。而每一像素的矩型重叠在一起,就形成了马赛克效果。

本例使用GD库的imagecolorat获取像素颜色,使用imagefilledrectangle画矩型。
<?php  
/** 图片局部打马赛克
* @param String $source 原图
* @param Stirng $dest 生成的图片
* @param int $x1 起点横坐标
* @param int $y1 起点纵坐标
* @param int $x2 终点横坐标
* @param int $y2 终点纵坐标
* @param int $deep 深度,数字越大越模糊
* @return boolean
*/
function imageMosaics($source, $dest, $x1, $y1, $x2, $y2, $deep){

// 判断原图是否存在
if(!file_exists($source)){
return false;
}

// 获取原图信息
list($owidth, $oheight, $otype) = getimagesize($source);

// 判断区域是否超出图片
if($x1>$owidth || $x1<0 || $x2>$owidth || $x2<0 || $y1>$oheight || $y1<0 || $y2>$oheight || $y2<0){
return false;
}

switch($otype){
case 1: $source_img = imagecreatefromgif($source); break;
case 2: $source_img = imagecreatefromjpeg($source); break;
case 3: $source_img = imagecreatefrompng($source); break;
default:
return false;
}

// 打马赛克
for($x=$x1; $x<$x2; $x=$x+$deep){
for($y=$y1; $y<$y2; $y=$y+$deep){
$color = imagecolorat($source_img, $x+round($deep/2), $y+round($deep/2));
imagefilledrectangle($source_img, $x, $y, $x+$deep, $y+$deep, $color);
}
}

// 生成图片
switch($otype){
case 1: imagegif($source_img, $dest); break;
case 2: imagejpeg($source_img, $dest); break;
case 3: imagepng($source_img, $dest); break;
}

return is_file($dest)? true : false;

}

$source = 'source.jpg';
$dest = 'dest.jpg';

$flag = imageMosaics($source, $dest, 176, 98, 273, 197, 4);
echo '<img src="'.$source.'">';
echo '<img src="'.$dest.'">';

?>
    PHP(外文名:PHP: Hypertext Preprocessor,中文名:“超文本预处理器”)是一种通用开源脚本语言。后端必备世界上最好的语言~