이 글에서는 주로 PHP 통합 동적 비밀번호 인증에 대해 자세히 소개합니다. 동적 비밀번호는 보안 성능을 높이기 위해 일회용 비밀번호와 무효화된 비밀번호를 사용합니다. 관심 있는 친구들은 이를 참고할 수 있습니다.
현재 대부분의 시스템에서는 신원 인증 로그인에 정적 비밀번호가 사용됩니다. 그러나 정적 비밀번호는 도난당하기 쉽기 때문에 보안 요구 사항을 충족할 수 없습니다.
동적 비밀번호는 일회용 비밀번호를 사용하며, 비밀번호 도용으로 인한 보안 문제를 방지하기 위해 사용한 비밀번호를 무효화합니다.
동적 비밀번호는 HOTP(이벤트 계산 기반 동적 비밀번호, RFC4226), TOTP(시간 계산 기반 동적 비밀번호, RFC6238), OCRA(챌린지 응답 동적 비밀번호, RFC6287) 및 기타 방법으로 구분됩니다.
이 글에서는 TOTP 방식을 통합한 동적 비밀번호 인증 솔루션을 소개합니다. PHP 프레임워크는 Thinkphp3.2.3을 사용하고, 동적 비밀번호 생성기는 Google 인증을 사용합니다.
1 Thinkphp 프레임워크에 맹세 알고리즘 클래스 추가
맹세 알고리즘 캡슐화 클래스 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가 필요합니다. 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 프레임워크의 ThinkPHPLibraryVendoroath 디렉터리에 넣으세요. oath 디렉터리는 직접 만든 것입니다.
2. 데이터베이스 필드 추가
사용자 테이블에 다음 필드를 추가합니다:
auth_type(0-정적 비밀번호, 1-동적 비밀번호)
seed(시드 키)
temp_seed(임시 시드 키)
last_logintime(마지막) time 로그인 성공 시간)
last_otp (마지막으로 사용한 비밀번호)
여기서 auth_type은 사용자가 어떤 인증 방법을 사용하는지를 나타내며, seed는 사용자의 시드 키, temp_seed는 사용자가 계정을 활성화하기 전에 임시로 저장되는 시드 키입니다. 동적 비밀번호 인증이 성공적으로 활성화되면 이 필드의 내용이 시드 필드에 채워집니다. last_logintime과 last_otp는 마지막으로 성공한 인증의 시간과 동적 비밀번호로, 사용자가 동일한 비밀번호를 재사용하는 것을 방지하는 데 사용됩니다.
3. 코드 통합
1) 동적 비밀번호 활성화
원본 시스템의 비밀번호 변경 페이지에서 인증 방법 선택을 추가합니다. 예:
사용자가 동적 비밀번호 방법을 선택하면 QR 코드가 생성되어 사용자가 동적 비밀번호를 활성화할 수 있는 페이지에 표시됩니다. Google 인증과 호환되도록 QR 코드 형식은 Google과 동일합니다. QR 코드를 생성하는 방법은 내 다른 기사 "Thinkphp3.2.3은 phpqrcode를 통합하여 로고가 있는 QR 코드 생성"을 참조하세요.
키 QR코드를 생성하는 코드는 다음과 같습니다.
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) 코드는 Google 모바일 토큰의 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 인증을 열고 QR 코드를 스캔하면 동적 비밀번호가 표시됩니다.
콘텐츠를 저장하고 동적 비밀번호를 활성화하는 데 성공했습니다!
그러면 고급 동적 비밀번호로 시스템에 로그인할 수 있습니다!
요약: 위 내용은 이 글의 전체 내용입니다. 모든 사람의 학습에 도움이 되기를 바랍니다.
관련 권장 사항:
PHP 2차원 배열 중복 제거 알고리즘 그래픽 및 텍스트 설명
phpPOST 데이터를 얻는 세 가지 방법에 대한 자세한 설명
위 내용은 PHP 통합 동적 비밀번호 인증(권장)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!