thinkphp前后端分离集成极验验证码(geetest) [TOCM] # 前言 公司项目以前一直使用4位数字验证码做登录验证,被老板吐槽,都什么年代了,还用这么low的东西,像xxx都用滑动验证了,巴拉巴拉... 一开始接到这个被吐槽的需求,想着实在太简单了,让前端找jquery插件,随随便便就改了。 # 分析 1. 公司项目采用react做独立前端,如果引入jquery,对项目的维护和代码结构不友好,建议使用react的插件库。 2. 验证码的本质的防止爬虫等攻击行为,如果不能和后端联动,那么将失去作为验证码的本身存在的意义。 3. 项目为前后端分离,属于无状态连接,如何验证正确性显得尤为重要。 结合以上三点,该需求变得复杂起来,挑选许多例子最终选用极验验证码作为我们的验证插件。 https://www.geetest.com/ 主要是由于极验验证码在但时间段有500次免费请求,对于我们这种内部项目错错有余,而且无需考虑各种被破解的风险。 相反,这点网易云盾就不是很人性化了。 ok, # 实现(前后端分离) ## 申请账号 在官网申请极验验证码的账号。 得到配置信息,填写在tp框架的配置文件中 ~~~ 'geetest'=> [ 'captcha_id' =>'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'private_key'=>'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', ], ~~~ ## 引入sdk库 讲下列代码保存为GeetestLib.php放置在extend下的geetest文件夹下。 ```php captcha_id = empty($config['captcha_id']) ? 1 : $config['captcha_id']; $this->private_key = empty($config['private_key']) ? 1 : $config['private_key']; $this->domain = "http://api.geetest.com"; } /** * 判断极验服务器是否down机 * * @param array $data * @return int */ public function pre_process($param, $new_captcha = true) { $data = array('gt' => $this->captcha_id, 'new_captcha' => $new_captcha ); $data = array_merge((array)$data, (array)$param); $query = http_build_query($data); $url = $this->domain . "/register.php?" . $query; $challenge = $this->send_request($url); if (strlen($challenge) != 32) { $this->failback_process(); return 0; } $this->success_process($challenge); return 1; } /** * @param $challenge */ private function success_process($challenge) { $challenge = md5($challenge . $this->private_key); $result = array( 'success' => 1, 'gt' => $this->captcha_id, 'challenge' => $challenge, 'new_captcha' => true ); $this->response = $result; } /** * */ private function failback_process() { $rnd1 = md5(rand(0, 100)); $rnd2 = md5(rand(0, 100)); $challenge = $rnd1 . substr($rnd2, 0, 2); $result = array( 'success' => 0, 'gt' => $this->captcha_id, 'challenge' => $challenge, 'new_captcha' => true ); $this->response = $result; } /** * @return mixed */ public function get_response_str() { return $this->response; } /** * 返回数组方便扩展 * * @return mixed */ public function get_response() { return $this->response; } /** * 正常模式获取验证结果 * * @param string $challenge * @param string $validate * @param string $seccode * @param array $param * @return int */ public function success_validate($challenge, $validate, $seccode, $param, $json_format = 1) { if (!$this->check_validate($challenge, $validate)) { return 0; } $query = array( "seccode" => $seccode, "timestamp" => time(), "challenge" => $challenge, "captchaid" => $this->captcha_id, "json_format" => $json_format, "sdk" => self::GT_SDK_VERSION ); $query = array_merge((array)$query, (array)$param); $url = $this->domain . "/validate.php"; $codevalidate = $this->post_request($url, $query); $obj = json_decode($codevalidate, true); if ($obj === false) { return 0; } if ($obj['seccode'] == md5($seccode)) { return 1; } else { return 0; } } /** * 宕机模式获取验证结果 * * @param $challenge * @param $validate * @param $seccode * @return int */ public function fail_validate($challenge, $validate, $seccode) { if (md5($challenge) == $validate) { return 1; } else { return 0; } } /** * @param $challenge * @param $validate * @return bool */ private function check_validate($challenge, $validate) { if (strlen($validate) != 32) { return false; } if (md5($this->private_key . 'geetest' . $challenge) != $validate) { return false; } return true; } /** * GET 请求 * * @param $url * @return mixed|string */ private function send_request($url) { if (function_exists('curl_exec')) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::$connectTimeout); curl_setopt($ch, CURLOPT_TIMEOUT, self::$socketTimeout); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $data = curl_exec($ch); $curl_errno = curl_errno($ch); curl_close($ch); if ($curl_errno > 0) { return 0; } else { return $data; } } else { $opts = array( 'http' => array( 'method' => "GET", 'timeout' => self::$connectTimeout + self::$socketTimeout, ) ); $context = stream_context_create($opts); $data = @file_get_contents($url, false, $context); if ($data) { return $data; } else { return 0; } } } /** * * @param $url * @param array $postdata * @return mixed|string */ private function post_request($url, $postdata = '') { if (!$postdata) { return false; } $data = http_build_query($postdata); if (function_exists('curl_exec')) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::$connectTimeout); curl_setopt($ch, CURLOPT_TIMEOUT, self::$socketTimeout); //不可能执行到的代码 if (!$postdata) { curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); } else { curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); } $data = curl_exec($ch); if (curl_errno($ch)) { $err = sprintf("curl[%s] error[%s]", $url, curl_errno($ch) . ':' . curl_error($ch)); $this->triggerError($err); } curl_close($ch); } else { if ($postdata) { $opts = array( 'http' => array( 'method' => 'POST', 'header' => "Content-type: application/x-www-form-urlencoded\r\n" . "Content-Length: " . strlen($data) . "\r\n", 'content' => $data, 'timeout' => self::$connectTimeout + self::$socketTimeout ) ); $context = stream_context_create($opts); $data = file_get_contents($url, false, $context); } } return $data; } /** * @param $err */ private function triggerError($err) { trigger_error($err); } } ``` ## 后端生成验证码配置信息 ~~~ public function geetest() { $param = $this->param; if (empty($param['t'])) { return ajaxReturn(-1, '参数错误'); } $geetest = new GeetestLib((array)Config::get('geetest')); Cache::set('geetest_userid' . $param['t'], $param['t']); Cache::set('geetest_status' . $param['t'], $geetest->pre_process(Cache::get('geetest_userid' . $param['t']))); return ajaxReturn(0, 'success', $geetest->get_response_str()); } ~~~ ## 后端登录验证是否成功 ```php //判断验证码是否正确,带入原始数据 $data = Request::param(false); if (!is_null($data) && !geetest_check($data)) { return ajaxReturn(-1, '验证码错误'); } /** * 极验验证码验证是否正确 */ function geetest_check($post, $config = []) { $config = empty($config) ? Config::get('geetest') : $config; $geetest = new GeetestLib($config); if (Cache::get('geetest_status' . $post['t']) == 1) { if ($geetest->success_validate($post['geetest_challenge'], $post['geetest_validate'], $post['geetest_seccode'], Cache::get('geetest_userid' . $post['t']))) { return true; } else { return false; } } else { if ($geetest->fail_validate($post['geetest_challenge'], $post['geetest_validate'], $post['geetest_seccode'])) { return true; } else { return false; } } } ``` # 实现(前后端不分离) ## 模板里的调用 ~~~ CSS样式参照demo.html的style样式48-144行 行为验证™ 安全组件加载中 ~~~ ## 控制器里验证 ~~~ $data = Request::param(false); //传入请求数据,使用false参数以获取原始数据 if(!is_null($data) && !geetest_check($data)){ //验证失败 return json()->data(false)->code(403); // 自定义 } ~~~ 附实现效果 