php集成动态口令认证

时间:2021-07-28 08:24:48

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

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

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

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
<?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内容如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<?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的二维码》
生成密钥二维码代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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解码算法并没有填充的‘='号。

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

?
1
2
3
4
5
6
从数据库读取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字段。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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集成动态口令认证

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。