Discuz!X2验证码的产生和验证及随机数产生探讨
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Discuz!X2验证码的产⽣和验证及随机数产⽣探讨
⼀、验证码的产⽣
1、如何在模板中添加⼀个验证码
在X2中验证码的模板部分独⽴为⼀个模板⽂件(template/default/common/seccheck.htm),供各个地⽅调⽤。
在模板中可以添加如下代码来调⽤验证码模板部分:
复制代码
代码如下:
<!--{eval $seccodecheck = 1;}-->
<!--{eval $sectpl = '<tr><th><sec></th><td><sec><p class="d"><sec></p></td>';}-->
<!--{subtemplate common/seccheck}-->
解释下这三句话:
第⼀句的意思为,我要开启验证码,即 $seccodecheck
变量
必须为真,就表⽰当前页⾯要开启验证码。
第⼆句的意思为,给要显⽰出来的验证码设置⼀个显⽰的模板格式,$sectpl 这个变量对应的就是模板,设置 $sectpl 可以让验证码的显⽰与当前页⾯的格式更好的结合。
从⽰例的模板代码中可以看出,只有 <sec> 不属于 HMTL 标准代码,⽽且出现了 3 次,这 3 次分别代表:“验证码”⽂字、验证码输⼊框、验证码图⽚,如下图所⽰:
这样就可以把验证码不同的部分合理的安放在您的页⾯中了。
第三句的意思为,将独⽴的验证码模板合并到当前页⾯中,与当前页⾯的模板⼀同输出。
在模板中添加上如上的代码后,刷新页⾯就可以看到验证码部分了。
2、验证码的⽣成流程
(以X2默认设置的“英⽂图⽚验证码”为例)
1)刚出现的验证码会默认执⾏⼀段 JS 代码
复制代码
代码如下:
<script type="text/javascript" reload="1">updateseccode('SQq29j20');</script>
执⾏的 JS 主要就是执⾏了 updateseccode 这个函数,直接点击验证码图⽚执⾏的也是这个函数。
函数中的 'SQq29j20' 是当前页⾯验证码的唯⼀字符串 idhash,他是由是否为Ajax请求、session id、⾃增数字组成,此处不必深究其含义。
2)updateseccode 函数在 static/js/common.js 中
复制代码
代码如下:
function updateseccode(idhash, play) {
$F('_updateseccode', arguments);
}
通过上⾯代码可以看到,updateseccode ⼜调⽤了 _updateseccode 私有函数,_updateseccode 函数在
static/js/common_extra.js ⽂件中
复制代码
代码如下:
function _updateseccode(idhash, play) {
if(isUndefined(play)) {
if($('seccode_' + idhash)) {
$('seccodeverify_' + idhash).value = '';
if(secST['code_' + idhash]) {
clearTimeout(secST['code_' + idhash]);
}
$('checkseccodeverify_' + idhash).innerHTML = '<img src="'+ IMGDIR + '/none.gif" width="16" height="16" class="vm" />'; ajaxget('misc.php?mod=seccode&action=update&idhash=' + idhash, 'seccode_' + idhash, null, '', '', function() {
secST['code_' + idhash] = setTimeout(function() {$('seccode_' + idhash).innerHTML = '<span class="xi2 cur1"
onclick="updateseccode(''+idhash+'')">刷新验证码</span>';}, 180000);
});
}
} else {
eval('window.document.seccodeplayer_' + idhash + '.SetVariable("isPlay", "1")');
}
}
这段 JS 代码有两个含义:
⼀是通过 ajaxget 请求了 misc.php?mod=seccode&action=update&idhash=xxxx 这样⼀个地址
⼆是设定了⼀个
定时器
,从显⽰了验证码开始,3分钟后⾃动将验证码图⽚换为“刷新验证码”的⽂字,点击该⽂字就执⾏ updateseccode 这个函数,重新更新验证码。
由此可以看出,此种⽅式可以很好的解决验证码过期的问题。
3)找到通过 ajaxget 请求的程序 source/module/misc/misc_seccode.php
通过 url 中的 action=update 可以看出,应该查看 if($_G['gp_action'] == 'update') { …… } 中的⼀段
复制代码
代码如下:
if($_G['gp_action'] == 'update') {
$message = '';
if($_G['setting']['seccodestatus']) {
$rand = random(5, 1);
$flashcode = '';
$idhash = isset($_G['gp_idhash']) ? $_G['gp_idhash'] : '';
$ani = $_G['setting']['seccodedata']['animator'] ? '_ani' : '';
if($_G['setting']['seccodedata']['type'] == 2) {
……
} elseif($_G['setting']['seccodedata']['type'] == 3) {
…...
} else {
$message = lang('core', 'seccode_image'.$ani.'_tips').'<img onclick="updateseccode(''.$idhash.'')" width="'.$_G['setting'] ['seccodedata']['width'].'" height="'.$_G['setting']['seccodedata']['height'].'" src="misc.php?
mod=seccode&update='.$rand.'&idhash='.$idhash.'" class="vm" alt="" />';
}
}
include template('common/header_ajax');
echo lang('message', $message, array('flashcode' => $flashcode, 'idhash' => $idhash));
include template('common/footer_ajax');
}
默认设置的“英⽂图⽚验证码”的 $_G['setting']['seccodedata']['type'] 为 0,所以看 else 的部分。
仔细看这⾥就是按照 ajax 的格式返回了⼀个验证码的图⽚,但是图⽚的 src 为 misc.php?mod=seccode&update=$rand&idhash=$idhash 这样⼀个动态链接,所以是通过这个链接动态⽣成的图⽚,此时⼜产⽣了⼀个新的请求。
4)找到通过图⽚链接请求的程序 source/module/misc/misc_seccode.php(和上⾯是同⼀个⽂件)
通过 url 可以看出,应该查看 if($_G['gp_action'] == 'update') { …… } else { …… } 中的⼀段
复制代码
代码如下:
} else {
$refererhost = parse_url($_SERVER['HTTP_REFERER']);
$refererhost['host'] .= !empty($refererhost['port']) ? (':'.$refererhost['port']) : '';
if($_G['setting']['seccodedata']['type'] < 2 && ($refererhost['host'] != $_SERVER['HTTP_HOST'] || !$_G['setting']
['seccodestatus']) || $_G['setting']['seccodedata']['type'] == 2 && !extension_loaded('ming') && $_POST['fromFlash'] != 1 || $_G['setting']['seccodedata']['type'] == 3 && $_GET['fromFlash'] != 1) {
exit('Access Denied');
}
$seccode = make_seccode($_G['gp_idhash']);
if(!$_G['setting']['nocacheheaders']) {
@header("Expires: -1");
@header("Cache-Control: no-store, private, post-check=0, pre-check=0, max-age=0", FALSE);
@header("Pragma: no-cache");
}
require_once libfile('class/seccode');
$code = new seccode();
$code->code = $seccode;
$code->type = $_G['setting']['seccodedata']['type'];
$code->width = $_G['setting']['seccodedata']['width'];
$code->height = $_G['setting']['seccodedata']['height'];
$code->background = $_G['setting']['seccodedata']['background'];
$code->adulterate = $_G['setting']['seccodedata']['adulterate'];
$code->ttf = $_G['setting']['seccodedata']['ttf'];
$code->angle = $_G['setting']['seccodedata']['angle'];
$code->warping = $_G['setting']['seccodedata']['warping'];
$code->scatter = $_G['setting']['seccodedata']['scatter'];
$code->color = $_G['setting']['seccodedata']['color'];
$code->size = $_G['setting']['seccodedata']['size'];
$code->shadow = $_G['setting']['seccodedata']['shadow'];
$code->animator = $_G['setting']['seccodedata']['animator'];
$code->fontpath = DISCUZ_ROOT.'./static/image/seccode/font/';
$code->datapath = DISCUZ_ROOT.'./static/image/seccode/';
$code->includepath = DISCUZ_ROOT.'./source/class/';
$code->display();
}
这部分开始是先做了⼀些安全性的验证,最后是根据给定的参数和由 make_seccode ⽣成的验证码字符串,⽣成验证码的图⽚,所以中间是重点。
make_seccode($_G['gp_idhash']) 这个函数传⼊了当前页⾯验证码的唯⼀字符串 idhash,⽣成了⽤于验证码的字符串。
5)make_seccode 函数在 source/function/function_seccode.php ⽂件
复制代码
代码如下:
function make_seccode($idhash){
global $_G;
$seccode = random(6, 1);
$seccodeunits = '';
if($_G['setting']['seccodedata']['type'] == 1) {
$lang = lang('seccode');
$len = strtoupper(CHARSET) == 'GBK' ? 2 : 3;
$code = array(substr($seccode, 0, 3), substr($seccode, 3, 3));
$seccode = '';
for($i = 0; $i < 2; $i++) {
$seccode .= substr($lang['chn'], $code[$i] * $len, $len);
}
} elseif($_G['setting']['seccodedata']['type'] == 3) {
$s = sprintf('%04s', base_convert($seccode, 10, 20));
$seccodeunits = 'CEFHKLMNOPQRSTUVWXYZ';
} else {
$s = sprintf('%04s', base_convert($seccode, 10, 24));
$seccodeunits = 'BCEFGHJKMPQRTVWXY2346789';
}
if($seccodeunits) {
$seccode = '';
for($i = 0; $i < 4; $i++) {
$unit = ord($s{$i});
$seccode .= ($unit >= 0x30 && $unit <= 0x39) ? $seccodeunits[$unit - 0x30] : $seccodeunits[$unit - 0x57];
}
}
dsetcookie('seccode'.$idhash, authcode(strtoupper($seccode)."t".(TIMESTAMP - 180)."t".$idhash."t".FORMHASH,
'ENCODE', $_G['config']['security']['authkey']), 0, 1, true);
return $seccode;
}
从函数中可以看到,验证码 $seccode ⾸先来⾃⼀个6位的随机数字 random(6, 1) (此函数如何⼯作,最后讲解)。
默认设置的“英⽂图⽚验证码”的 $_G['setting']['seccodedata']['type'] 为 0,所以看 else 的部分。
将 $seccode 的数字通过base_convert 函数由 10
进制
转为 24 进制,然后设定可以在验证码出现的字符串
复制代码
代码如下:
'BCEFGHJKMPQRTVWXY2346789'。
最后将 24 进制的验证码在 $seccodeunits 中取得真正的 4 位验证码字符串 $seccode ,最后将 $seccode 通过 authcode 加密函数进⾏加密,写⼊ cookie 中,并返回,cookie 的名字是 seccode 连上 $idhash 的值(例如:seccodeSQq29j20)。
加密时使⽤的是在 config/config_global.php 中设置的 $_G['config']['security']['authkey'] 的值。
⾄此验证码及图⽚⽣成完毕,⽣成的验证码到⽬前为⽌只以加密的⽅式存在于 cookie 中。
⼆、验证码的验证
1、JS ⽅式的验证
1)这种验证就是在⽂本框中输⼊验证码后,及时的验证。
这个验证是由⽂本框的 onblur 失去焦点事件触发 checksec('code', 'SQq29j20') JS 函数进⾏验证的。
2)checksec 函数在 static/js/common.js 中
复制代码
代码如下:
function checksec(type, idhash, showmsg, recall) {
$F('_checksec', arguments);
}
通过上⾯代码可以看到,checksec ⼜调⽤了 _checksec 私有函数,_checksec 函数在 static/js/common_extra.js ⽂件中
复制代码
代码如下:
function _checksec(type, idhash, showmsg, recall) {
var showmsg = !showmsg ? 0 : showmsg;
var secverify = $('sec' + type + 'verify_' + idhash).value;
if(!secverify) {
return;
}
var x = new Ajax('XML', 'checksec' + type + 'verify_' + idhash);
x.loading = '';
$('checksec' + type + 'verify_' + idhash).innerHTML = '<img src="'+ IMGDIR + '/loading.gif" width="16" height="16"
class="vm" />';
x.get('misc.php?mod=sec' + type + '&action=check&inajax=1&&idhash=' + idhash + '&secverify=' + (BROWSER.ie && document.charset == 'utf-8' ? encodeURIComponent(secverify) : secverify), function(s){
var obj = $('checksec' + type + 'verify_' + idhash);
obj.style.display = '';
if(s.substr(0, 7) == 'succeed') {
obj.innerHTML = '<img src="'+ IMGDIR + '/check_right.gif" width="16" height="16" class="vm" />';
if(showmsg) {
recall(1);
}
} else {
obj.innerHTML = '<img src="'+ IMGDIR + '/check_error.gif" width="16" height="16" class="vm" />';
if(showmsg) {
if(type == 'code') {
showError('验证码错误,请重新填写');
} else if(type == 'qaa') {
showError('验证问答错误,请重新填写');
}
recall(0);
}
}
});
}
这个函数⾸先验证下,输⼊框内填写的验证码的值 $('sec' + type + 'verify_' + idhash).value 是否存在(type 就是传⼊的code)。
然后通过 ajax 请求访问 misc.php?mod=seccode&action=check&inajax=1&&idhash=xxxx&secverify=xxxx 这样⼀个地址,这个地址会返回验证的结果字符串。
如果返回结果的前 7 个字符是 succeed 则验证通过,显⽰对勾;否则提⽰“验证码错误,请重新填写”,并显⽰红叉。
3)找到通过 ajax 请求的程序 source/module/misc/misc_seccode.php
通过 url 中的 action=check 可以看出,应该查看 elseif($_G['gp_action'] == 'check') { …… } 中的⼀段
复制代码
代码如下:
} elseif($_G['gp_action'] == 'check') {
include template('common/header_ajax');
echo check_seccode($_G['gp_secverify'], $_G['gp_idhash']) ? 'succeed' : 'invalid';
include template('common/footer_ajax');
} else {
这⾥将通过 url 传⼊的 secverify 和 idhash 两个值传递给 check_seccode 函数,通过代码看到 check_seccode 返回布尔值,故结果为真,则通过验证,返回 succeed 字符串,结果为假,则验证失败,返回 invalid 字符串。
4)check_seccode 函数在 source/function/function_core.php ⽂件
复制代码
代码如下:
function check_seccode($value, $idhash) {
global $_G;
if(!$_G['setting']['seccodestatus']) {
return true;
}
if(!isset($_G['cookie']['seccode'.$idhash])) {
return false;
}
list($checkvalue, $checktime, $checkidhash, $checkformhash) = explode("t", authcode($_G['cookie']['seccode'.$idhash],
'DECODE', $_G['config']['security']['authkey']));
return $checkvalue == strtoupper($value) && TIMESTAMP - 180 > $checktime && $checkidhash == $idhash && FORMHASH == $checkformhash;
}
此函数⾸先根据缓存中的设定验证验证码的开启状态,如果未开启,此处验证直接返回真,既然没有开启验证码⾃然如何验证均为真。
然后验证 cookie 中是否存在⽣成验证码时写⼊ cookie 的值(例如:seccodeSQq29j20),如果 cookie 没有此值,则此次验证失效,需要重新⽣成验证码,重新验证。
最后从 cookie 取出值,使⽤ $_G['config']['security']['authkey'] 加密串,通过 authcode 函数对值进⾏解密,解密后获取到验证码、⽣成时间、idhash、formhash 四个值。
然后需要同时满⾜以下四个条件才可以通过验证:
- 输⼊的验证码等于解密出来的验证码
- 验证码的⽣成时间距当前时间⼩于 180 秒
- 传⼊的 idhash 等于解密出来的 idhash
- 当前系统⽣成的 formhash 等于解密出来的 formhash
⾄此通过 JS ⽅式的验证码验证完成。
2、PHP ⽅式的验证
1)这种⽅式就是在验证码所在的表单提交后,对输⼊的验证码进⾏的验证。
例如在修改⽤户密码时开启了验证码,则会在其处理的 PHP 程序中发现(source/include/spacecp/spacecp_profile.php)这样⼀句代码
submitcheck('passwordsubmit', 0, $seccodecheck, $secqaacheck)
submitcheck 函数就是对提交的表单进⾏验证的。
2)submitcheck 函数在 source/function/function_core.php ⽂件
复制代码
代码如下:
function submitcheck($var, $allowget = 0, $seccodecheck = 0, $secqaacheck = 0) {
if(!getgpc($var)) {
return FALSE;
} else {
global $_G;
if($allowget || ($_SERVER['REQUEST_METHOD'] == 'POST' && !empty($_G['gp_formhash']) && $_G['gp_formhash'] == formhash() && empty($_SERVER['HTTP_X_FLASH_VERSION']) && (empty($_SERVER['HTTP_REFERER']) ||
preg_replace("/https?://([^:/]+).*/i", "1", $_SERVER['HTTP_REFERER']) == preg_replace("/([^:]+).*/", "1",
$_SERVER['HTTP_HOST'])))) {
if(checkperm('seccode')) {
if($secqaacheck && !check_secqaa($_G['gp_secanswer'], $_G['gp_sechash'])) {
showmessage('submit_secqaa_invalid');
}
if($seccodecheck && !check_seccode($_G['gp_seccodeverify'], $_G['gp_sechash'])) {
showmessage('submit_seccode_invalid');
}
}
return TRUE;
} else {
showmessage('submit_invalid');
}
}
}
submitcheck 函数⼀般只填写前两个参数即可,第⼀个参数表⽰要验证的表单元素的名字,此表单元素不存在则验证失败;第⼆个参数表⽰是否允许通过 GET ⽅式提交的数据通过验证,0 为不允许,1 为允许,⼀般为 0 即可。
后两个参数⽤于表⽰提交的表单中是否需要对验证码和验证问答做验证,第三个参数 $seccodecheck 代表验证码,第四个参数 $secqaacheck 代表验证问答,参数值都是 0 为不验证,1 为验证。
所以如果需要在提交后验证验证码,则⾄少要填写 3 个参数,即 submitcheck('passwordsubmit', 0, 1) 。
进⼊函数中会现对提交表单的提交⽅式、formhash、访问来源 referer 等数据进⾏安全性验证,通过后则会调⽤
check_seccode 函数对提交过来的验证码进⾏验证了,根据 check_seccode 的返回值,来给予不同的提⽰。
check_seccode 函数如何⼯作的参看 JS 验证中的 4)即可。
⾄此通过 PHP ⽅式的验证码验证完成。
三、随机数如何产⽣的
Discuz! X的随机数是通过 random 函数产⽣的,函数在 source/function/function_core.php ⽂件
复制代码
代码如下:
function random($length, $numeric = 0) {
$seed = base_convert(md5(microtime().$_SERVER['DOCUMENT_ROOT']), 16, $numeric ? 10 : 35);
$seed = $numeric ? (str_replace('0', '', $seed).'012340567890') : ($seed.'zZ'.strtoupper($seed));
$hash = '';
$max = strlen($seed) - 1;
for($i = 0; $i < $length; $i++) {
$hash .= $seed{mt_rand(0, $max)};
}
return $hash;
}
此函数有两个参数,$length 表⽰要获取的随机数的位数,$numeric 表⽰是否要获取纯数字的随机数,取值 0 或 1。
函数⾸先使⽤ microtime 函数获取当前的微秒级时间戳字符串,然后在后⾯拼接上单前⽹站的根⽬录路径,然后进⾏ MD5 加密,获得 32 位长的字符串。
之后对其进⾏转进制,如果要获取纯数字的随机数,则从 16 进制转为 10 进制,如果要获得数字和英⽂混杂的随机数,则从 16 进制转为 35 进制。
之后再将转进制后获得的字符串,根据是否要获取纯数字随机数的区别,进⾏拼接。
最后从拼接后的字符串中随机抽取随机数的第⼀位、第⼆位以此类推,直⾄获取满⾜要求的随机数的位置为⽌。
⾄此⽣成了随机数。