CSRF是一个很普遍的问题,这里不对CSRF做过多的介绍,简单提一下,说一下自己在实际工作中的解决方案。
copy图(来自于网上):
核心问题
CSRF的核心问题在于:
1.目标网站不确定请求是否来自与指定的网站
2.目标网站不确定请求是否是用户的真实意愿,不安全网站是通过间接欺诈的手段在提交请求。
通用解决方案:
1.referer 校验。(问题:容易模仿)
2.token 校验。(问题:维护token麻烦)
3.签名认证。(问题:签名算法容易泄漏)
我们想要的效果:
1.我们需要对用户的每一个请求做校验来验证这是用户的本意操作。
2.我们需要大批量的验证不想维护token。
3.我们要预防CSRF。
我们的基本条件:
1.假设用户cookie不会泄露。
2.通过refer校验+签名认证(前端和后端维护同一个算法)+过期时间 的方法进行CSRF攻击防范。
3.我们的校验依赖于后台的名单,而不应该依赖于前台js的签名,所以我们需要在后台(服务端)维护一个校验名单。
具体思路:
1.js端通过cookie中的session id + 当天时间 按照一定的算法/规则做运算,在请求中将当前时间戳作为明文,运算出来的值作为秘文传给服务端。
2.服务端通过读取后台校验名单对符合规则的接口用同样的算法对session id +时间戳(前台传输)进行校验 并于结果进行对比,完成签名认证的过程。
3.在后台校验之前需要先进行refer校验。
4.每一个时间戳的有效期为2s,也就是从发起请求到收到请求不应该超过2s。(此步骤可以舍去或放开,未了防止重放攻击)
code:
对应的js算法
function getDefaultToken(type){
var data = {};
var obj = {};
var str = '';
var cookie = document.cookie;
var arr=cookie.split(";");
for(var i=0;i<arr.length;i++){
var arr2 = arr[i].split('=');
obj[arr2[0]] = arr2[1];
if( $.trim(arr2[0])=='PHPSESSID'){
str += $.trim(arr2[1]);
break;
}
}
var time = parseInt( (new Date).getTime(),10);
str+=time;
var len = str.length;
var k=0,j=0,l=0;
for(k;k<len;k++){
l = str.charCodeAt(k);
l = (l<<5) ;
l = (l>0)?l:k;
j += l;
}
j= j+time;
if(!type){
data['time_csrf'] = time;
data['token_csrf'] = j;
return data;
}
return time+'::'+j;
}
对应的js请求方法:
function updateAdminPasswordSend(){
var _data = {};
_data.uid = user_id;
_data.password = $("#newpassword").val();
_data.comfirmpassword = $("#newpasswordcomfirm").val();
var sign = $.getDefaultToken();
_data.num = Math.random();
//console.log(_data);
if(_data.password != _data.comfirmpassword ){
art.dialog({title:"修改密码",lock: true, content: "两次密码不一致",time:3});
return;
}
if(_data.password == ''){
art.dialog({title:"修改密码",lock: true, content: "密码不能为空",time:3});
return;
}
$.post("{:U('Admin/updateAdminPassword')}",{"data":_data,"csrf_sign":sign},function(e){
if(e.status == 1){
$(".send_hide_none").removeClass("send_hide_none").addClass("send");
art.dialog.get("show_auth").close();
}else{
art.dialog({title:"出错了",lock: true, content: e.info,time:3});
}
},'json');
}
对应的服务端校验名单(php 以 define作为校验名单)
<?php
/**
* Created by PhpStorm.
* User: wangyf
* Date: 16-1-4
* Time: 下午3:33
*/
/*
* 校验名单命名规则:prefix_action_function(prefix 前缀用来标识所在系统如:管理后台Admin 也可以根据需求自己定义),对应常量值,表示的是传输过来后sign校验所在的下标
* 白名单命名规则:white_prefix_action_function(white 为固定前缀,prefix,action,function同上),白名单后的值可以随意给,不做判断。
* 每个系统可以自己维护自己的配置文件也可以维护在公共配置文件,建议各自维护各自的配置。白名单和黑名单只是为了更好的控制校验范围,具体问题具体分析
* */
/*小二后台校验名单*/
//define('Admin_Admin_admin_list','csrf_sign');
define('Admin_Admin_add_admin_name','csrf_sign');
define('Admin_Admin_updateAdminPassword','csrf_sign');
define('Admin_Admin_delete_admin','csrf_sign');
define('Admin_Admin_ajax_upPhoneNum','csrf_sign');
/*小二后台校验白名单*/
//define('white_Admin_Admin_updateAdminPassword','csrf_sign');
对应的服务端构造函数中加入校验:
abstract class CommonAction extends Action
{
private $admin_dict = array(
'15833627275' => 0
);
/**
* 基类初始化操作
*/
protected function _initialize()
{
$this->CSRFCheck($_REQUEST['_URL_'][0],$_REQUEST['_URL_'][1],'Admin'); //csrf 校验
$this->AdminAuth();
$this->CheckAdminAuth();
}
//csrf 统一验证
protected function CSRFCheck($class,$function,$prefix){
$CSRFModel = new CSRFBascModel();
$CSRFModel->crsfCheck($class,$function,$prefix);
}
....
具体的CSRF校验类:
/**
* Created by PhpStorm.
* User: wangyf
* Date: 16-1-4
* Time: 上午11:39
* bref:此类为CRSF验证类,主要功能是对crsf进行验证,此类目前是根据define配置来判断接口 并将验证收拢与此类中,方便后续迁移
*/
class CSRFBascModel extends BaseModel
{
//默认的校验名称
protected $signKey = 'time_csrf';
protected $signVal = 'token_csrf';
//crsf校验,包括refer和签名认证。
public function crsfCheck($class,$function,$prefix){
$key =$prefix.'_'.$class.'_'.$function;
$whiteDefined =$this->definedCheck('white_'.$key);
//验证名单,是为了兼容系统,后期可以去除,全部采用统一的传输方式,此处第一兼容白名单,第二兼容目前已有的ajax调用
$defined = $this->definedCheck($key);
$sign = array();
//排除白名单
if($whiteDefined) return true;
//校验 校验名单
if($defined){
$define = constant($key);
$sign = isset($_REQUEST[$define])?$_REQUEST[$define]:'';
}else{
//如果没有 校验 不再验证
return true;
}
//校验refer
$referOk = $this->referCheck();
if(!$referOk) ApiAjaxReturnFail('refer error');
//校验sign
$signOk = $this->signCheck($sign,$key);
if(!$signOk) ApiAjaxReturnFail('sign error');
}
//根据key返回是否有defined定义
public function definedCheck($key){
$whiteDefined = defined($key);
return $whiteDefined;
}
//校验签名 对sign自动解析并校验
public function signCheck($sign,$key){
//所有的sign 数据统一按照类中的定义取出
if(!is_array($sign)){
$array = explode('::',$sign);
$sign = array();
if(isset($array[0]))
$sign[$this->signKey] = $array[0];
if(isset($array[1]))
$sign[$this->signVal] = $array[1];
}
if(!isset($sign[$this->signKey]) || !isset($sign[$this->signVal])) return false;
$sign_time = $sign[$this->signKey];
$sign_token = $sign[$this->signVal];
/*$time = microtime(true)*1000;
$timeLimit = ($time-$sign_time);
//超过2000毫秒 就是超时 主要为了防止get请求、重放攻击
if($timeLimit>2000) {
LogItil::write('sign check error: --request interface--'.$key.'--sign--'.json_encode($sign).'--check time--'.$time.'--time difference--'. $timeLimit .'--time--'. time().' this is error time out ---end',LogItil::LEVEL_FLOW);
return false;
}*/
$cookie = $_COOKIE;
$str =trim( $cookie['PHPSESSID']);
$str.=$sign_time;
$result = 0;
$k = strlen($str);
for($i=0;$i<$k;$i++){
$r = $this->charCodeAt($str,$i);
$r = $r<<5;
$r = ($r>0)?$r:$i;
$result +=$r;
}
$result = $result+$sign_time;
if(strval($result) != strval($sign_token)){
LogItil::write('sign check error: --request interface--'.$key.'--sign--'.json_encode($sign).'--PHPSESSID--'. $str .'--time--'. time().' this is error check error ---end',LogItil::LEVEL_FLOW);
return false;
}
return true;
}
//校验refer 公共函数,但此处考录之后重构 以及测试环境 兼容 迁移在此处实现并收拢
public function referCheck(){
$referer = $_SERVER['HTTP_REFERER'];
$matchUrl = "/^https?\:\/\/([a-zA-Z0-9\-]{0,16}\.){0,3}test\.com(:8080){0,1}(\/|$)/";
# 线上环境才验证 长度为16 其余为32 为了兼容测试环境
if(!isonline() ){
$matchUrl = "/^https?\:\/\/([a-zA-Z0-9\-]{0,32}\.){0,3}test\.com(:8080){0,1}(\/|$)/";
}
if (!preg_match($matchUrl, $referer))
{
LogItil::write('refer check error: --refer--'. $referer .'--match--'.$matchUrl.'--time--'. time().' this is error refer prefix/port ---end',LogItil::LEVEL_FLOW);
return false;
}
/**
* 检查非法的后缀的refer
*/
$illegal_refer = "/((.bmp)|(.png)|(.jpeg)|(.jpg))$/";
if (preg_match($illegal_refer, $referer)) {
LogItil::write('refer check error: --refer--'. $referer .'--match--'.$illegal_refer.'--time--'. time().' this is error refer postfix ---end',LogItil::LEVEL_FLOW);
return false;
}
return true;
}
/*
* charCodeAt 函数实现 ,获取某个字符串某位置的Unicode 编码 用于签名算法
* @author wangyf
* @time 20151213
* */
public function charCodeAt($str,$index)
{
$char = mb_substr($str, $index, 1, 'UTF-8');
if (mb_check_encoding($char, 'UTF-8'))
{
$ret = mb_convert_encoding($char, 'UTF-32BE', 'UTF-8');
return hexdec(bin2hex($ret));
}
else
{
return null;
}
}
}
具体需求可以根据具体情况,对其中的refer校验部分和对应的线上线下规则进行修改,2s超时可以适当放开或改进(也许不同的服务器时间会有出入,客户端的时间也会不同),整体思路就是以上部分 此处签名算法过于简单,后续可以根据自己的需要进行改进。
js可以封装在jquery扩展中自动提交,后台依赖于黑白名单校验,我们就可以通过配置黑白名单的方法进行快速大批量的页面CSRF校验。黑白名单只是便于控制校验范围,并不是必须的。
以上只是思路,和大家分享,共同进步。