CSRF

Cross Site Request Forgery, 即跨站请求伪造。

原理

前提是向目标网站注入恶意代码并获得了用户身份信息(Cookie),该恶意代码的作用是在用户主动或不知情(如img标签)的情况下请求欺诈网站,然后在欺诈网站上带上获得的用户身份信息请求目标网站,从而达到攻击的目的。简单来说就是:攻击者盗用用户身份发起恶意请求。

CSRF因为需要注入代码、获得cookie,所以是在XSS攻击基础上发起的。

防御

  • 表单提交生成表单令牌(Token), 服务端每次提交需要校验令牌。
  • 使用验证码
  • 验证referer头

XSS

Cross-site Scripting(XSS) 跨站脚本攻击,主要利用HTML和JavaScript代码注入。

主要原理

网站程序(前端和后端)未对用户提交/请求的数据进行检查和过滤,将包含JavaScript代码的数据直接输出到浏览器解释执行。

类型

  • 反射型XSS

反射型XSS只是简单的把用户输入的数据“反射”给浏览器,也就是说需要诱使用户“点击”一个恶意链接,才能攻击成功。反射型XSS也叫作“非持久型XSS”。

  • 存储型XSS

主动提交恶意数据到服务器,攻击者在数据中嵌入代码,这样当其他用户请求后,服务器从数据库中查询数据并发给用户,用户浏览此类页面时就可能受到攻击。

  • DOM-based XSS

基于DOM的XSS,通过对具体DOM代码进行分析,根据实际情况构造DOM节点进行XSS跨站脚本攻击。

防御

  • 输入过滤,对用户提交/请求的数据进行转义或过滤HTML标签
  • 输出转义
  • 考虑设置HTTP Only Cookie

参考资料

PHP运行模式和(非)线程安全

运行模式

  • CGI模式

HTTP服务器收到用户请求后,启动一个子进程(php / php.exe)去处理,处理完后结束该子进程。

  • FastCGI模式

FastCGI作为HTTP服务器和动态脚本语言(PHP)的通信接口。PHP启动FastCGI进程管理器PHP-FPM,HTTP服务器收到用户请求后转发给PHP-FPM处理。

  • Apache模块模式

 mod_php以Apache模块的形式,在Apache启动时加载进去,Apache服务器收到用户请求后交由mod_php模块处理。

  • CLI

命令行模式,直接执行 php 命令。

  • ISAPI模式

Windows服务器下作为IIS的WEB服务扩展运行。PHP 5.3版本后丢弃 ISAPI 模块支持,使用改进的 FastCGI SAPI模块替代[Link]。

线程安全和非线程安全

PHP以Apache模块模式和ISAPI模式运行时,用Thread Safe版本;其它模式用Non-Thread Safe版。

 

HTTP状态码

1xx消息


这一类型的状态码,代表请求已被接受,需要继续处理。

101 Switching Protocols

服务器已经理解了客户端的请求,并将通过Upgrade消息头通知客户端采用不同的协议来完成这个请求。如从 HTTP1.1 切换到 HTTP/2 或 Websocket

2xx成功


这一类型的状态码,代表请求已成功被服务器接收、理解、并接受

200 OK

请求已成功

206 Partial Content(RFC 7233

服务器已经成功处理了部分GET请求,如断点续传、分段下载。

3xx重定向


301 Moved Permanently

永久重定向

302 Moved Temporarily

临时重定向

4xx客户端错误


这类的状态码代表了客户端看起来可能发生了错误,妨碍了服务器的处理

400 Bad Request

The request could not be understood by the server due to malformed syntax

401 Unauthorized

未通过 HTTP Auth 认证

403 Forbidden

服务器拒绝执行

404 Not Found

服务器找不到请求资源

405 Method Not Allowed

请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow头信息用以表示出当前资源能够接受的请求方法的列表。例如,需要通过POST呈现数据的表单上的GET请求,或只读资源上的PUT请求。

5xx服务器错误


这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生

500 Internal Server Error

通用错误消息,服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。

502 Bad Gateway

后端服务器无响应,如nginx+php-fpm架构中php-fpm没有启动

503 Service Unavailable

由于临时的服务器维护或者过载,服务器当前无法处理请求。

504 Gateway Timeout

后端处理超时,如php执行时间超过nginx的fastcgi_read_timeout时间

 

Notes


301/302 区别

用户通过浏览器访问并无感知区别,主要作用于搜索引擎,302告诉搜索引擎此处跳转是临时的,应当收录原来的地址,有可能会被搜索引擎认为作弊;301告诉搜索引擎原地址已永久转移,应当收录新地址。引用一段StackOverflow的答案:

Status 301 means that the resource (page) is moved permanently to a new location. The client/browser should not attempt to request the original location but use the new location from now on.

Status 302 means that the resource is temporarily located somewhere else, and the client/browser should continue requesting the original url.

PHP和HTTP Authentication

# 获取认证用户名和密码

$_SERVER['PHP_AUTH_USER'];
$_SERVER['PHP_AUTH_PW'];

参考资料:

    https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication
    https://www.nginx.com/resources/admin-guide/restricting-access-auth-basic/
    http://php.net/manual/fa/features.http-auth.php

小内存环境下使用PHP Composer

最近Composer爆出了PHP资源回收机制的一个梗(https://github.com/composer/composer/commit/ac676f47f7bbc619678a29deae097b6b0710b799), 也由此才知道原来Composer在安装和更新包的时候会这么耗资源的。

最近刚好在DigitalOcean有台512M RAM的VPS要用到Composer, 执行composer install的时候才发现内存不够,把其他服务(nginx, mysql, memcached)停掉后问题依旧。在快认为小内存环境不能用composer的时候,在DO的社区找到了个帖子(https://www.digitalocean.com/community/questions/composer-install-not-enough-memory),试了下,居然可以了。解决办法居然是给VPS加上swap分区。一直以为Linux的swap分区是必须的,不知道DO的VPS默认是没有swap分区的。

最近看到陈皓在一篇关于C语言的文章里提到的几个理论(http://coolshell.cn/articles/5761.html),觉得印象很深刻,记录下。

1)相信程序员,不阻止程序员做他们想做的事。

2)保持语言的简洁,以及概念上的简单。

3)保证性能,就算牺牲移植性。

根据经纬度实现附近查找

网上找到的几种办法:

  1. 范围查找: blog.charlee.li/location-search/
  2. geohash算法: www.cnblogs.com/dengxinglin/archive/2012/12/14/2817761.html
  3. Mongodb的Geospatial indexes: docs.mongodb.org/manual/applications/geospatial-indexes/
  4. mysql扩展: github.com/lucasepe/lib_mysqludf_haversine

mongodb的Geospatial indexes应该是最强大的,提供了丰富的查询方法。缺点是增加了运维成本,对于不是专门做LBS的应用来说,也是一个问题。

第1,4种方法原理是一样的, 用到了haversine 公式:

  • 第1种算出来的是经纬度范围,然后再以该范围为条件查询数据库,把公式计算放在了应用层。
  • 第4种得出的两点的距离计算结果,可以用该结果来作为返回字段、过滤条件和排序, 但是数据量大的时候无法使用索引也是一个问题。
  • 这里还有haversine 公式各种语言的实现: www.codecodex.com/wiki/Calculate_Distance_Between_Two_Points_on_a_Globe

私信系统的设计

需求:

  • 最基本的写、看、删
  • 删除效果只限于操作者本人,即本人删除私信对方仍可以看到私信记录
  • 会话合并显示,将与同一个人的私信记录合并成一条记录显示

 

数据结构:

mail-db图1.私信数据结构

三个表分别是: 会话表(mail_session)、私信-会话关系表(mail_map)、私信表(mail)

 

 说明:

mail-session图2.效果图

  1. 会话: 用户与用户的关系。
  • 当A发送一条私信给B时会产生两条会话记录:

A=>B (from_uid=A, to_uid=B)

B=>A (from_uid=B, to_uid=A)

  • 用户查看自己的私信列表时,只需查询 会话表mail_session(from_uid=:uid)即可
  • 用户删除自己的会话时,不会影响到对方
  • 删除会话时应该把 会话-私信表 mail_map 的关系也删除,避免冗余数据
  1. 信息:用户产生的文字
  • 当A发送一条私信给B时会产生两条会话-私信关系记录,分别与用户A、B自己的会话关联
  • 用户查看某个会话时,查询 mail_map LEFT JOIN mail (sess_id=:sess_id)
  • 用户删除某条私信时,在会话-私信表 mail_map 上删除相应记录 (map_id=:map_id) 或者(sess_id=:sess_id AND mail_id=:mail_id)即可
  • 发送私信时将最新私信内容更新到会话表的 last_message 字段中,适当的数据冗余简化私信会话列表的查询,可利用程序或触发器实现

php扩展中使用动态链接库

整理下大概知识点:

  • C的编译、链接、安装过程
  • autoconf基本语法
  • config.m4中的PHP_XX系列宏(在aclocal.m4中)

具体例子:
以使用taglib(https://github.com/taglib/taglib)编写一个操作mp3的id3标签的PHP扩展为例,按照默认编译安装taglib,头文件在/usr/local/include/taglib/tag_c.h,共享库文件在/usr/local/lib/libtag_c.so
我用到了taglib这个库的c语言版,安装后taglib-config –libs命令得到的是它的c++版的库,所以要在config.m4里自己写。

config.m4的关键代码:

PHP_ARG_WITH(taglib, for taglib support, [  --with-taglib             Include taglib support])

if test "$PHP_TAGLIB" != "no"; then

SEARCH_PATH="/usr/local /usr"
SEARCH_FOR="/include/taglib/tag_c.h"
if test -r $PHP_TAGLIB/$SEARCH_FOR; then # path given as parameter
TAGLIB_DIR=$PHP_TAGLIB
else # search default path list
AC_MSG_CHECKING([for taglib files in default path])
for i in $SEARCH_PATH ; do
if test -r $i/$SEARCH_FOR; then
TAGLIB_DIR=$i
AC_MSG_RESULT(found in $i)
fi
done
fi

if test -z "$TAGLIB_DIR"; then
AC_MSG_RESULT([not found])
AC_MSG_ERROR([Please reinstall the taglib distribution])
fi

PHP_ADD_INCLUDE($TAGLIB_DIR/include)

dnl #库文件 libtag_c.so
LIBNAME=tag_c

dnl #库函数
LIBSYMBOL=taglib_file_tag

dnl 第三个参数要设置成EXTRA_LDFLAGS时,链接的时候才会加上 -ltag_c 参数...这个PHP_ADD_LIBRARY_WITH_PATH的原型还没看明白...
dnl 貌似还有好几种解决方案,PHP_EVAL_LIBLINE?
PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $TAGLIB_DIR/lib, EXTRA_LDFLAGS)

PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
[
AC_DEFINE(HAVE_TAGLIB,1,[ ])
],[
AC_MSG_ERROR()
],[
-L$TAGLIB_DIR/lib -lm
])

PHP_NEW_EXTENSION(php_taglib, php_taglib.c, $ext_shared)
PHP_SUBST(TAGLIB_SHARED_LIBADD)
fi

接下来是

configure [--with-taglib[=/path]]
make
make install

顺利的话可以到php.ini里面加载扩展。

期间可能会出现tag_c.h文件找不到的问题,注意下头文件的路径是由 config.m4 文件里的 PHP_ADD_INCLUDE($TAGLIB_DIR/include) 加上 扩展源码里 #include “taglib/tag_c.h” 组成的。

然后,即使扩展编译安装成功,php_info()里看到加载成功,但是cli执行php 扩展测试代码的时候出现 symbol lookup error的话,应该是扩展链接的时候没有找到libtag_c.so, 注意下PHP_ADD_LIBRARY_WITH_PATH() 的第三个参数即可。

JS+PHP实现图片裁剪

用到几个插件:
1.Jquery上传插件: Fine Uploader
2.JS图片裁剪:imgAreaSelect
3.PHP上传/PHP GD库

原理:
PHP的imagecopy()函数可以将一张图片的一部分复制到另一张图片中,而imgAreaSelect可以获得用户裁剪图片的坐标及高宽,利用imgAreaSelect将需裁剪的x轴起点、y轴起点、宽、高四个参数传给PHP,PHP完成复制图片工作。

要点:
1.图片预览/截图。如果浏览器兼容FileApi ,问题就简单多,直接读取本地文件完成预览、获得截图的坐标和宽高等参数。考虑到IE10之前都不支持的情况,可以利用AJAX上传的方法,先将图片传到服务器,然后读取出来进行预览和裁剪。
2.PHP处理图片。PHP进行图片复制需要获得以下几个参数:原图片路径、x轴起点、y轴起点、宽、高。如果服务器支持ImageMagick扩展,可以直接利用Imagick::cropImage函数进行裁剪。不支持的话,可以采用最原始的方法,先用imagecreatetruecolor函数生成一副和截图区域等宽等高的空白图片,再用imagecopy将原图的截图复制过来。

关键代码:
1.PHP的处理


class Pic{
	public $picInfo;

	public function get_pic_info( $file ){
		if( ($im = getimagesize($file)) == false ){
			$this->error = "$file is not a valid image file";
			return false;
		}

		$fileInfo = pathinfo( $file );
		$info = array(
			'width'	=> $im[0],
			'height'=> $im[1],
			'name'	=> $fileInfo['filename'],
			'type'	=> strtolower($fileInfo['extension']),
			'mime'	=> $im['mime'],
			'path'	=> $fileInfo['dirname'].'/'
			);
		$this->picInfo = $info;
		//return $info;
      	}

	/**
	 * 截取图片
	 * 
	 * @param $source_file 源文件地址
	 * @param $x 截取x轴起点
	 * @param $y 截取y轴起点
	 * @param $width 截取宽度
	 * @param $height 截取高度
	 * @return bool | resource
	 * @since v1.0
	 * @create 2012-12-20
	 */
	public function cut( $source_file, $x, $y, $width, $height ){
		$this->get_pic_info( $source_file );	
		$info = $this->picInfo;
		if( !$info ){
			$this->error = "failed to get $source_file";
			return false;
		}
		// 判断截取是否有效
		if( $x>=$info['width'] || $y>=$info['height'] || $width>$info['width'] || $height>$info['height'] || !$width || !$height ){
			$this->error = "invalid crop params";
			return false;
		}

		$type = $info['type'] == 'jpg' ? 'jpeg' : $info['type'];
		// 根据源文件类型创建对象
		$func = "imagecreatefrom".$type;
		if( ($im = $func( $source_file )) ==  false ){
			$this->error = "create gd resource failed";
			return false;		
		}

		// 创建一个目标文件
		$dst = imagecreatetruecolor( $width, $height );
		// 设定背景色为白色
		$color = imagecolorallocatealpha($dst, 255, 255, 255, 0);	
		imagefill( $dst, 0, 0, $color );
		// 复制源文件的指定区域
		if( !imagecopy( $dst, $im, 0, 0, $x, $y, $width, $height ) ){
			$this->error = "copy image fail";
			return false;
		}

		// 根据文件类型保存对象
		$func = "image".$type;

		ob_start();
		if( $type=='jpeg' ){
			$func( $dst, NULL, 85);
		}else{
			$func( $dst, NULL ); 
		}
		$newIm = ob_get_clean();

		if( !$newIm ){
			$this->error = "create image fail";
			return false;
		}
		imagedestroy( $dst );

		return $newIm;
	}
}

2. JS的处理:


var picEditor;
var imguploader = $(‘#fileuploadbtn’).fineUploader({
request: {
endpoint: ‘upload.php’,
},validation: {
allowedExtensions: [‘jpeg’, ‘jpg’, ‘png’, ‘gif’],
sizeLimit: 2097152
},
text: {
uploadButton: ‘点击上传’
},
dragAndDrop: {
extraDropzones:[$(‘#dropzone’)],
hideDropzones: true,
disableDefaultDropzone: false,
},
template: ‘

‘ +

{uploadButtonText}

‘ +

    ‘ +


    }).on(‘complete’, function(event, id, filename, json){
    // ajax上传返回json数据
    if (json.status) {
    //初始化js裁剪插件,设置参数请参看官网文档
    picEditor = $(‘#topfileupload img’).imgAreaSelect({
    handles: true,
    aspectRatio: ‘{$aspectRatio}’,
    instance: true,
    imageHeight: json.height,
    imageWidth: json.width,
    classPrefix: ‘imgareaselect’,
    onSelectEnd: function(img, sel){
    change_preview(sel);
    }
    });
    } else {
    if( json.info ) alert(‘上传失败:’+json.info);
    }
    }).on(‘error’,function(id,fileName,error){
    alert(‘只允许上传小于2M的图片’);
    });

    // 将裁剪参数传给PHP处理
    $(‘#btnSubmit’).click(function(){
    if( !picEditor ){
    $.bigpu.alert(‘请先上传图片’);
    return;
    }
    var s = picEditor.getSelection();
    var f = $(‘#topfileupload img’).attr(‘src’);
    $.ajax({
    url: ‘crop.php’,
    data: ‘file=’+f+’&x=’+s.x1+’&y=’+s.y1+’&width=’+s.width+’&height=’+s.height,
    dataType: ‘json’,
    success: function(json){
    if( json.status ){
    picEditor.remove();
    // PHP完成任务,返回Json供Js回调
    pic_editor_success( json );
    }else{
    alert(‘图片保存失败%>_<%。'); } } }); }) // 截图预览 function change_preview(selection){ //预览宽高 var prevWidth = 100; var prevHeight = 100; if ( selection.width == 0 || selection.height == 0 || !prevWidth || !prevHeight ) { return false;} // 缩放比例 var scaleX = prevWidth / selection.width; var scaleY = prevHeight / selection.height; // 预览定位 $('#imgPreview').css({ width: Math.round(scaleX * picInfo['oldWidth']) + 'px', height: Math.round(scaleY * picInfo['oldHeight']) + 'px', marginLeft: '-' + Math.round(scaleX * selection.x1) + 'px', marginTop: '-' + Math.round(scaleY * selection.y1) + 'px' }); // 记录位置 $('#x').val( selection.x1 ); $('#y').val( selection.y1 ); $('#w').val( selection.width ); $('#h').val( selection.height ); return; } [/javascript]