Browse Source

first commit

mapleSnow 1 year ago
parent
commit
7ce38a8fed
4 changed files with 307 additions and 2 deletions
  1. 38 0
      Dockerfile
  2. 120 2
      README.md
  3. 16 0
      composer.json
  4. 133 0
      index.php

+ 38 - 0
Dockerfile

@@ -0,0 +1,38 @@
+FROM php:7.4-apache
+
+# 更换 Debian 镜像源为阿里云
+RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
+    sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list
+
+# 安装依赖并清理缓存
+RUN apt-get update && apt-get install -y \
+    git \
+    zip \
+    unzip \
+    libpng-dev \
+    libjpeg-dev \
+    && docker-php-ext-configure gd --with-jpeg \
+    && docker-php-ext-install -j$(nproc) gd \
+    && rm -rf /var/lib/apt/lists/*
+
+# 安装 Composer v2,并使用阿里云镜像源
+RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer  && \
+    composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
+
+# 在容器中创建一个工作目录
+WORKDIR /var/www/html
+
+# 复制 PHP 代码和 Composer 配置文件
+COPY composer.json /var/www/html/
+COPY index.php /var/www/html/
+
+# 使用 Composer 安装 PHP QR Code 库
+RUN composer install --no-dev --no-scripts --no-autoloader --no-interaction && \
+    composer dump-autoload --optimize
+
+# 清理不需要的文件
+RUN rm -rf /root/.composer/cache/*
+
+# 定义容器启动命令
+CMD ["apache2-foreground"]
+

+ 120 - 2
README.md

@@ -1,3 +1,121 @@
-# dynamic-qrcode
+# 动态验证码部署
+
+## 快速使用 
+
+```bash
+docker load -i dynamic-qrcode.tar.gz  && docker run -d --name dq -p 8027:80 dynamic-qrcode:v1.0  
+```
+
+
+
+##编译
+
+```bash
+docker build -t dynamic-qrcode:v1.0 .
+```
+
+
+
+## 安装
+
+```bash
+docker load -i dynamic-qrcode.tar.gz 
+```
+
+
+
+## 运行
+
+```bash
+docker run -d --name dq -p PORT:80 dynamic-qrcode:v1.0  // PORT 按需修改
+```
+
+
+
+# 服务端PHP验证码校验
+
+## 工具类
+
+```php
+<?php
+
+namespace App\Utils;
+
+class TOTPAuthenticator
+{
+    // Base32 解码函数
+    public static function base32_decode($str)
+    {
+        $base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+        $base32charsFlipped = array_flip(str_split($base32chars));
+        $paddingCharCount = substr_count($str, '=');
+
+        if ($paddingCharCount > 0) {
+            $str = substr($str, 0, -($paddingCharCount));
+        }
+
+        $str = str_split($str);
+        $binaryString = "";
+
+        for ($i = 0; $i < count($str); $i = $i + 8) {
+            $x = "";
+            for ($j = 0; $j < 8; $j++) {
+                $x .= str_pad(base_convert(@$base32charsFlipped[@$str[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
+            }
+            $eightBits = str_split($x, 8);
+            for ($z = 0; $z < count($eightBits); $z++) {
+                $binaryString .= ($eightBits[$z] == '') ? '' : chr(base_convert($eightBits[$z], 2, 10));
+            }
+        }
+        return $binaryString;
+    }
+
+    // TOTP 令牌生成函数
+    public static function generateTOTPToken($secretKey)
+    {
+        if (empty($secretKey)) return false;
+
+        $timestamp = floor(time() / 30);
+        $validityPeriod = 30 - (time() % 30);
+        $secretKey = self::base32_decode($secretKey);
+        $timestamp = pack('N*', 0) . pack('N*', $timestamp);
+        $hash = hash_hmac('sha1', $timestamp, $secretKey, true);
+        $offset = ord(substr($hash, -1)) & 0x0F;
+        $token = (
+            (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, 6);
+        return array($token, $validityPeriod);
+    }
+}
+```
+
+## 测试用例
+
+```php
+<?php
+
+namespace Tests\Unit;
+
+use PHPUnit\Framework\TestCase;
+use App\Utils\TOTPAuthenticator;
+
+class TOTPAuthenticatorTest extends TestCase
+{
+    // 测试 TOTP 令牌生成函数
+    public function testGenerateTOTPToken()
+    {
+        $secretKey = 'JBSWY3DPEHPK3PXP';
+        $tokenInfo = TOTPAuthenticator::generateTOTPToken($secretKey);
+
+        $this->assertIsArray($tokenInfo);
+        $this->assertCount(2, $tokenInfo);
+        $this->assertIsInt($tokenInfo[0]);
+        $this->assertIsInt($tokenInfo[1]);
+        $this->assertGreaterThan(0, $tokenInfo[1]);
+    }
+}
+```
 
-动态验证码,用于双因子登录

+ 16 - 0
composer.json

@@ -0,0 +1,16 @@
+{
+    "name": "maplesnow/dynamic-captcha",
+    "description": "A basic dynamic captcha project",
+    "type": "project",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "maplesnow"
+        }
+    ],
+    "require": {
+        "php": "^7.3",
+        "chillerlan/php-qrcode": "^2.0"
+    }
+}
+

+ 133 - 0
index.php

@@ -0,0 +1,133 @@
+<?php
+// Base32 解码函数
+function base32_decode($str) {
+    $base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+    $base32charsFlipped = array_flip(str_split($base32chars));
+    $paddingCharCount = substr_count($str, '=');
+
+    if ($paddingCharCount > 0) {
+        $str = substr($str, 0, -($paddingCharCount));
+    }
+
+    $str = str_split($str);
+    $binaryString = "";
+
+    for ($i = 0; $i < count($str); $i = $i + 8) {
+        $x = "";
+        for ($j = 0; $j < 8; $j++) {
+            $x .= str_pad(base_convert(@$base32charsFlipped[@$str[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
+        }
+        $eightBits = str_split($x, 8);
+        for ($z = 0; $z < count($eightBits); $z++) {
+            $binaryString .= ( $eightBits[$z] == '' ) ? '' : chr(base_convert($eightBits[$z], 2, 10));
+        }
+    }
+    return $binaryString;
+}
+
+// TOTP 令牌生成函数
+function generateTOTPToken($secretKey) {
+    if(empty($secretKey)) return false; // 检查密钥是否为空
+
+    $timestamp = floor(time() / 30); // 时间戳以 30 秒为单位
+    $validityPeriod = 30 - (time() % 30); // 计算当前时间距离下一个 30 秒的时间差,作为有效期
+    $secretKey = base32_decode($secretKey);
+    $timestamp = pack('N*', 0) . pack('N*', $timestamp); // 将时间戳转换为字节流
+    $hash = hash_hmac('sha1', $timestamp, $secretKey, true); // 使用 HMAC-SHA1 算法生成哈希
+    $offset = ord(substr($hash, -1)) & 0x0F; // 获取哈希的最后一个字节的低四位作为偏移量
+    $token = (
+        (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, 6); // 取哈希结果的四个字节生成一个 6 位动态验证码
+    return array($token, $validityPeriod); // 返回 6 位验证码 和 有效期
+}
+
+// 示例用户令牌密钥
+$userSecretKey = 'JBSWY3DPEHPK3PXP'; // 这是一个示例密钥,实际应用中应该是随机生成的
+
+// 生成动态验证码和有效期
+list($token, $validityPeriod) = generateTOTPToken($userSecretKey);
+if($token === false) {
+    echo "Failed to generate TOTP token."; // 在生成动态验证码失败时输出错误消息
+    exit(); // 出错时停止执行后续代码
+}
+
+// 导入 QR Code 生成库
+require_once 'vendor/autoload.php';
+
+use chillerlan\QRCode\QRCode;
+use chillerlan\QRCode\QROptions;
+
+// 生成二维码
+$options = new QROptions;
+
+//$options->version             = 7;
+$options->outputInterface     = QRGdImagePNG::class;
+$options->scale               = 20;
+$options->bgColor             = [200, 150, 200];
+$options->imageTransparent    = true;
+$qrCode = new QRCode($options);
+
+// 生成二维码数据
+$qrCodeData = $qrCode->render($token);
+
+// 输出HTML页面
+echo "<!DOCTYPE html>
+<html lang=\"en\">
+<head>
+    <meta charset=\"UTF-8\">
+    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
+    <title>动态验证码</title>
+    <style>
+      body {
+            text-align: center;
+            margin: 0;
+            padding: 0;
+        }
+        #countdown {
+            font-size: 20px;
+            font-weight: bold;
+            margin-top: 20px;
+        }
+        img {
+            width: 300px;
+            height: 300px;
+            display: block;
+            margin: 0 auto;
+        }
+    </style>
+</head>
+<body>
+    <h1>Dynamic QR Code</h1>
+    <div id=\"countdown\"></div>
+    <img src=\"$qrCodeData\" alt=\"Dynamic QR Code\">
+    <script>
+        // 获取倒计时容器和倒计时文本
+        const countdownContainer = document.getElementById('countdown');
+        // 设置初始倒计时时间
+        let timeLeft = $validityPeriod;
+        // 更新倒计时显示
+        function updateCountdown() {
+            countdownContainer.textContent = 'Expires in ' + timeLeft + ' seconds';
+            // 如果倒计时为0,则重新请求动态口令
+            if (timeLeft === 0) {
+                clearInterval(timer);
+                // 重新加载页面或者请求新的动态口令
+                location.reload();
+            }
+            // 否则,减少剩余时间
+            else {
+                timeLeft--;
+            }
+        }
+        // 初始更新倒计时显示
+        updateCountdown();
+        // 每秒更新倒计时
+        const timer = setInterval(updateCountdown, 1000);
+    </script>
+</body>
+</html>";
+?>
+