这篇文章主要为大家详细介绍了php集成动态口令认证,动态口令采用一次一密、用过密码作废的方式来提高安全性能,感兴趣的小伙伴们可以参考一下
大多数系统目前均使用的静态密码进行身份认证登录,但由于静态密码容易被窃取,其安全性无法满足安全要求。
动态口令采用一次一密、用过密码作废的方式防止了密码被窃取带来的安全问题。
动态口令分为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 pisible 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)、开通动态口令
在原有系统的修改密码页面,加上认证方式的选择,例如:
如果用户选择动态口令方式,则会生成一张二维码显示在页面,用于用户开通动态口令。为了兼容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获取post数据的三种方法详解
以上就是php集成动态口令认证(推荐)的详细内容。