php集成动态口令认证


Posted in PHP onJuly 21, 2016

大多数系统目前均使用的静态密码进行身份认证登录,但由于静态密码容易被窃取,其安全性无法满足安全要求。

动态口令采用一次一密、用过密码作废的方式防止了密码被窃取带来的安全问题。
动态口令分为HOTP(基于事件计数的动态口令,RFC4226)、TOTP(基于时间计数的动态口令,RFC6238)、OCRA(挑战应答式动态口令,RFC6287)等方式。

本文介绍了集成TOTP方式的动态口令认证的方案,PHP框架采用Thinkphp3.2.3,动态口令生成器使用的是google authtication。

1、为Thinkphp框架添加oath算法类

oath算法封装类oath.php代码如下:

<?PHP
/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * PHP Google two-factor authentication module.
 *
 * See http://www.idontplaydarts.com/2011/07/google-totp-two-factor-authentication-for-php/
 * for more details
 *
 * @author Phil
 **/

class Google2FA {

 const keyRegeneration  = 30; // Interval between key regeneration
 const otpLength  = 6; // Length of the Token generated

 private static $lut = array( // Lookup needed for Base32 encoding
  "A" => 0, "B" => 1,
  "C" => 2, "D" => 3,
  "E" => 4, "F" => 5,
  "G" => 6, "H" => 7,
  "I" => 8, "J" => 9,
  "K" => 10, "L" => 11,
  "M" => 12, "N" => 13,
  "O" => 14, "P" => 15,
  "Q" => 16, "R" => 17,
  "S" => 18, "T" => 19,
  "U" => 20, "V" => 21,
  "W" => 22, "X" => 23,
  "Y" => 24, "Z" => 25,
  "2" => 26, "3" => 27,
  "4" => 28, "5" => 29,
  "6" => 30, "7" => 31
 );

 /**
  * Generates a 16 digit secret key in base32 format
  * @return string
  **/
 public static function generate_secret_key($length = 16) {
  $b32  = "234567QWERTYUIOPASDFGHJKLZXCVBNM";
  $s  = "";

  for ($i = 0; $i < $length; $i++)
   $s .= $b32[rand(0,31)];

  return $s;
 }

 /**
  * Returns the current Unix Timestamp devided by the keyRegeneration
  * period.
  * @return integer
  **/
 public static function get_timestamp() {
  return floor(microtime(true)/self::keyRegeneration);
 }

 /**
  * Decodes a base32 string into a binary string.
  **/
 public static function base32_decode($b32) {

  $b32  = strtoupper($b32);

  if (!preg_match('/^[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]+$/', $b32, $match))
   throw new Exception('Invalid characters in the base32 string.');

  $l  = strlen($b32);
  $n = 0;
  $j = 0;
  $binary = "";

  for ($i = 0; $i < $l; $i++) {

   $n = $n << 5;     // Move buffer left by 5 to make room
   $n = $n + self::$lut[$b32[$i]];  // Add value into buffer
   $j = $j + 5;    // Keep track of number of bits in buffer

   if ($j >= 8) {
    $j = $j - 8;
    $binary .= chr(($n & (0xFF << $j)) >> $j);
   }
  }

  return $binary;
 }
 /*by tang*/  
 public static function base32_encode($data, $length){
  $basestr = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  $count = 0;
  if ($length > 0) {
   $buffer = $data[0];
   $next = 1;
   $bitsLeft = 8;

   while (($bitsLeft > 0 || $next < $length)) {
    if ($bitsLeft < 5) {
    if ($next < $length) {
     $buffer <<= 8;
     $buffer |= $data[$next++] & 0xFF;
     $bitsLeft += 8;
    } else {
     $pad = 5 - $bitsLeft;
     $buffer <<= $pad;
     $bitsLeft += $pad;
    }
    }
    $index = 0x1F & ($buffer >> ($bitsLeft - 5));
    $bitsLeft -= 5;
    $result .= $basestr[$index];
    $count++;
   }
   }
   return $result;  
 }
 /**
  * Takes the secret key and the timestamp and returns the one time
  * password.
  *
  * @param binary $key - Secret key in binary form.
  * @param integer $counter - Timestamp as returned by get_timestamp.
  * @return string
  **/
 public static function oath_hotp($key, $counter)
 {
  if (strlen($key) < 8)
  throw new Exception('Secret key is too short. Must be at least 16 base 32 characters');

  $bin_counter = pack('N*', 0) . pack('N*', $counter);  // Counter must be 64-bit int
  $hash  = hash_hmac ('sha1', $bin_counter, $key, true);

  return str_pad(self::oath_truncate($hash), self::otpLength, '0', STR_PAD_LEFT);
 }

 /**
  * Verifys a user inputted key against the current timestamp. Checks $window
  * keys either side of the timestamp.
  *
  * @param string $b32seed
  * @param string $key - User specified key
  * @param integer $window
  * @param boolean $useTimeStamp
  * @return boolean
  **/
 public static function verify_key($b32seed, $key, $window = 5, $useTimeStamp = true) {

  $timeStamp = self::get_timestamp();

  if ($useTimeStamp !== true) $timeStamp = (int)$useTimeStamp;

  $binarySeed = self::base32_decode($b32seed);

  for ($ts = $timeStamp - $window; $ts <= $timeStamp + $window; $ts++)
   if (self::oath_hotp($binarySeed, $ts) == $key)
    return true;

  return false;

 }

 /**
  * Extracts the OTP from the SHA1 hash.
  * @param binary $hash
  * @return integer
  **/
 public static function oath_truncate($hash)
 {
  $offset = ord($hash[19]) & 0xf;

  return (
   ((ord($hash[$offset+0]) & 0x7f) << 24 ) |
   ((ord($hash[$offset+1]) & 0xff) << 16 ) |
   ((ord($hash[$offset+2]) & 0xff) << 8 ) |
   (ord($hash[$offset+3]) & 0xff)
  ) % pow(10, self::otpLength);
 }


}
/*
$InitalizationKey = "LFLFMU2SGVCUIUCZKBMEKRKLIQ";     // Set the inital key

$TimeStamp  = Google2FA::get_timestamp();
$secretkey  = Google2FA::base32_decode($InitalizationKey); // Decode it into binary
$otp    = Google2FA::oath_hotp($secretkey, $TimeStamp); // Get current token

echo("Init key: $InitalizationKey\n");
echo("Timestamp: $TimeStamp\n");
echo("One time password: $otp\n");

// Use this to verify a key as it allows for some time drift.

$result = Google2FA::verify_key($InitalizationKey, "123456");

var_dump($result);
*/
?>

由于google的动态口令算法中种子密钥使用了base32编码,因此需要base32算法,base32.php内容如下:

<?php
//namespace Base32;
/**
 * Base32 encoder and decoder
 *
 * Last update: 2012-06-20
 *
 * RFC 4648 compliant
 * @link http://www.ietf.org/rfc/rfc4648.txt
 *
 * Some groundwork based on this class
 * https://github.com/NTICompass/PHP-Base32
 *
 * @author Christian Riesen <chris.riesen@gmail.com>
 * @link http://christianriesen.com
 * @license MIT License see LICENSE file
 */
class Base32
{
 /**
  * Alphabet for encoding and decoding base32
  *
  * @var array
  */
 private static $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
 /**
  * Creates an array from a binary string into a given chunk size
  *
  * @param string $binaryString String to chunk
  * @param integer $bits Number of bits per chunk
  * @return array
  */
 private static function chunk($binaryString, $bits)
 {
  $binaryString = chunk_split($binaryString, $bits, ' ');
  if (substr($binaryString, (strlen($binaryString)) - 1) == ' ') {
   $binaryString = substr($binaryString, 0, strlen($binaryString)-1);
  }
  return explode(' ', $binaryString);
 }
 /**
  * Encodes into base32
  *
  * @param string $string Clear text string
  * @return string Base32 encoded string
  */
 public static function encode($string)
 {
  if (strlen($string) == 0) {
   // Gives an empty string
   return '';
  }
  // Convert string to binary
  $binaryString = '';
  foreach (str_split($string) as $s) {
   // Return each character as an 8-bit binary string
   $binaryString .= sprintf('%08b', ord($s));
  }
  // Break into 5-bit chunks, then break that into an array
  $binaryArray = self::chunk($binaryString, 5);
  // Pad array to be divisible by 8
  while (count($binaryArray) % 8 !== 0) {
   $binaryArray[] = null;
  }
  $base32String = '';
  // Encode in base32
  foreach ($binaryArray as $bin) {
   $char = 32;
   if (!is_null($bin)) {
    // Pad the binary strings
    $bin = str_pad($bin, 5, 0, STR_PAD_RIGHT);
    $char = bindec($bin);
   }
   // Base32 character
   $base32String .= self::$alphabet[$char];
  }
  return $base32String;
 }
 /**
  * Decodes base32
  *
  * @param string $base32String Base32 encoded string
  * @return string Clear text string
  */
 public static function decode($base32String)
 {
  // Only work in upper cases
  $base32String = strtoupper($base32String);
  // Remove anything that is not base32 alphabet
  $pattern = '/[^A-Z2-7]/';
  $base32String = preg_replace($pattern, '', $base32String);
  if (strlen($base32String) == 0) {
   // Gives an empty string
   return '';
  }
  $base32Array = str_split($base32String);
  $string = '';
  foreach ($base32Array as $str) {
   $char = strpos(self::$alphabet, $str);
   // Ignore the padding character
   if ($char !== 32) {
    $string .= sprintf('%05b', $char);
   }
  }
  while (strlen($string) %8 !== 0) {
   $string = substr($string, 0, strlen($string)-1);
  }
  $binaryArray = self::chunk($string, 8);
  $realString = '';
  foreach ($binaryArray as $bin) {
   // Pad each value to 8 bits
   $bin = str_pad($bin, 8, 0, STR_PAD_RIGHT);
   // Convert binary strings to ASCII
   $realString .= chr(bindec($bin));
  }
  return $realString;
 }
}

?>

将这两个文件放到Thinkphp框架的ThinkPHP\Library\Vendor\oath目录下,oath目录是自己创建的。

2、添加数据库字段

用户表添加如下字段:
auth_type(0-静态密码,1-动态口令)
seed(种子密钥)
temp_seed(临时种子密钥)
last_logintime(上次登录成功时间)
last_otp(上次使用密码)
其中auth_type是为了标明用户使用的哪种认证方式,seed为用户的种子密钥,temp_seed为用户未开通前临时保存的一个种子密钥,如果用户开通动态口令认证成功,该字段内容会填到seed字段。last_logintime和last_otp为上次认证成功的时间和动态口令,用于避免用户同一个口令重复使用。

3、代码集成

1)、开通动态口令

在原有系统的修改密码页面,加上认证方式的选择,例如:

php集成动态口令认证

如果用户选择动态口令方式,则会生成一张二维码显示在页面,用于用户开通动态口令。为了兼容google authtication,其二维码格式与谷歌一样。生成二维码的方法见我的另一篇《Thinkphp3.2.3整合phpqrcode生成带logo的二维码》 。
生成密钥二维码代码如下:

public function qrcode()
 { 
  Vendor('oath.base32');
  $base32 = new \Base32();
  $rand = random(16);//生成随机种子
  $rand = $base32->encode($rand);
  $rand=str_replace('=','',$rand);//去除填充的‘='

  $errorCorrectionLevel =intval(3) ;//容错级别 
  $matrixPointSize = intval(8);//生成图片大小

  //生成二维码图片 
  Vendor('phpqrcode.phpqrcode');
  $object = new \QRcode();
  $text = sprintf("otpauth://totp/%s?secret=%s", $user, $rand);
  $object->png($text, false, $errorCorrectionLevel, $matrixPointSize, 2);

  生成的种子$rand保存到数据库的temp_seed字段
 }

random是生成随机字符串函数。$rand=str_replace('=','',$rand)这句代码是因为谷歌手机令牌中base32解码算法并没有填充的‘='号。

验证用户动态口令的代码如下:

从数据库读取temp_seed
Vendor('oath.oath');
$object = new \Google2FA();
if($object->verify_key($temp_seed, $otp)){
 验证成功,将数据库更新seed为temp_seed,auth_type为1,last_otp为otp
}

2)、动态口令登录

用户动态口令登录验证的代码:

从数据库读取auth_type,seed,last_otp字段。

if($auth_type==1){//动态口令
 //防止重复认证    
 if($lat_otp == $otp) {
  动态口令重复使用返回    
 }
 Vendor('oath.oath');
 $object = new \Google2FA();
 if(!$object->verify_key($seed, $otp))
 {
  动态口令不正确
 }
 else
 {
  登录成功,将数据库更新last_otp为$otp,last_logintime为time()
 }    
}

4、测试验证

下载google authtication,使用静态密码登录系统,进入修改密码页面。
打开google authtication,扫描二维码,会显示动态口令。

php集成动态口令认证

php集成动态口令认证

保存内容,开通动态口令成功!
然后你就可以用高大上的动态口令登录系统了!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
十天学会php之第四天
Oct 09 PHP
利用js调用后台php进行数据处理原码
Oct 09 PHP
抓取YAHOO股票报价的类
May 15 PHP
php HtmlReplace输入过滤安全函数
Jul 03 PHP
ThinkPHP自动验证失败的解决方法
Jun 09 PHP
php笔记之:有规律大文件的读取与写入的分析
Apr 26 PHP
php调用C代码的实现方法
Mar 11 PHP
PHP实现自动登入google play下载app report的方法
Sep 23 PHP
php中get_cfg_var()和ini_get()的用法及区别
Mar 04 PHP
PHP 数据结构队列(SplQueue)和优先队列(SplPriorityQueue)简单使用实例
May 12 PHP
yii2.0整合阿里云oss上传单个文件的示例
Sep 19 PHP
php实现微信分享朋友链接功能
Feb 18 PHP
Thinkphp3.2.3整合phpqrcode生成带logo的二维码
Jul 21 #PHP
微信随机生成红包金额算法php版
Jul 21 #PHP
PHP简单读取PDF页数的实现方法
Jul 21 #PHP
基于PHP微信红包的算法探讨
Jul 21 #PHP
php中preg_replace_callback函数简单用法示例
Jul 21 #PHP
PHP获取客户端及服务器端IP的封装类
Jul 21 #PHP
thinkPHP多域名情况下使用memcache方式共享session数据的实现方法
Jul 21 #PHP
You might like
php不用内置函数对数组排序的两个算法代码
2010/02/08 PHP
Smarty foreach控制循环次数的实现详解
2013/07/03 PHP
如何使用FireFox插件FirePHP调试PHP
2013/07/23 PHP
PHP中unset,array_splice删除数组中元素的区别
2014/07/28 PHP
php打造智能化的柱状图程序,用于报表等
2015/06/19 PHP
PHP使用pear自带的mail类库发邮件的方法
2015/07/08 PHP
php如何控制用户对图片的访问 PHP禁止图片盗链
2016/03/25 PHP
ExtJS4给Combobox设置列表中的默认值示例
2014/05/02 Javascript
纯JS打造网页中checkbox和radio的美化效果
2016/10/13 Javascript
js对字符串进行编码的方法总结(推荐)
2016/11/10 Javascript
AngularJS路由实现页面跳转实例
2017/03/03 Javascript
小程序视频列表中视频的播放与停止的示例代码
2018/07/20 Javascript
NodeJS 将文件夹按照存放路径变成一个对应的JSON的方法
2018/10/17 NodeJs
JS中使用react-tooltip插件实现鼠标悬浮显示框
2019/05/15 Javascript
vue 弹出遮罩层样式实例
2020/07/22 Javascript
图文讲解选择排序算法的原理及在Python中的实现
2016/05/04 Python
python实现用户登录系统
2016/05/21 Python
深入浅析Python的类
2018/06/22 Python
python3 正则表达式基础廖雪峰
2020/03/25 Python
Python脚本如何在bilibili中查找弹幕发送者
2020/06/04 Python
python支持多继承吗
2020/06/19 Python
你可能不熟练的十个前端HTML5经典面试题
2018/07/03 HTML / CSS
记一次高分屏下canvas模糊问题
2020/02/17 HTML / CSS
英国护发和美妆在线商店:Klip Shop
2019/03/24 全球购物
我们在web应用开发过程中经常遇到输出某种编码的字符,如iso8859-1等,如何输出一个某种编码的字符串?
2014/03/30 面试题
三个Unix的命令面试题
2015/04/12 面试题
社区精神文明建设汇报材料
2014/08/17 职场文书
学生会竞选演讲稿学习部
2014/08/25 职场文书
党员个人整改方案及措施
2014/10/25 职场文书
学习保证书100字
2015/02/26 职场文书
2015社区精神文明建设工作总结
2015/04/21 职场文书
房屋租赁意向书范本
2015/05/09 职场文书
幼儿园老师新年寄语
2015/08/17 职场文书
javaScript Array api梳理
2021/03/31 Javascript
golang日志包logger的用法详解
2021/05/05 Golang
浅谈MySQL next-key lock 加锁范围
2021/06/07 MySQL