Pārlūkot izejas kodu

‘小程序/APP-短信验证码增加非对称校验’

sunxbiao 1 gadu atpakaļ
vecāks
revīzija
d9ccaff1d9

+ 2 - 1
.gitignore

@@ -2,4 +2,5 @@ public/
2 2
 .idea/
3 3
 .env
4 4
 runtime/
5
-nohup.out
5
+nohup.out
6
+*.pem

+ 60 - 0
app/common/middleware/RateLimitMiddleware.php

@@ -0,0 +1,60 @@
1
+<?php
2
+
3
+namespace app\common\middleware;
4
+
5
+use think\facade\Cache;
6
+use think\Response;
7
+
8
+class RateLimitMiddleware
9
+{
10
+    // 频率限制配置(单位:秒)
11
+    protected $rules = [
12
+        'ip' => ['limit' => 10, 'window' => 60],    // 每IP每分钟10次
13
+        'mobile' => ['limit' => 5, 'window' => 3600] // 每手机号每小时5次
14
+    ];
15
+
16
+    public function handle($request, \Closure $next)
17
+    {
18
+        $mobile = $request->param('mobile');
19
+        $ip = $request->ip();
20
+
21
+        // IP频率检查
22
+        $ipKey = "rate:ip:{$ip}";
23
+        if (!$this->checkRate($ipKey, $this->rules['ip'])) {
24
+            return $this->tooManyRequests('IP请求过于频繁');
25
+        }
26
+
27
+        // 手机号频率检查
28
+        $mobileKey = "rate:mobile:{$mobile}";
29
+        if (!$this->checkRate($mobileKey, $this->rules['mobile'])) {
30
+            return $this->tooManyRequests('该手机号请求过于频繁');
31
+        }
32
+
33
+        return $next($request);
34
+    }
35
+
36
+    protected function checkRate(string $key, array $rule): bool
37
+    {
38
+        $count = Cache::get($key, 0);
39
+
40
+        if ($count >= $rule['limit']) {
41
+            return false;
42
+        }
43
+
44
+        Cache::inc($key);
45
+        if ($count === 0) {
46
+            Cache::expire($key, $rule['window']);
47
+        }
48
+
49
+        return true;
50
+    }
51
+
52
+    protected function tooManyRequests(string $message): Response
53
+    {
54
+        return json([
55
+            'code' => 429,
56
+            'msg' => $message,
57
+            'data' => null
58
+        ])->code(429);
59
+    }
60
+}

+ 98 - 0
app/common/middleware/SignatureMiddleware.php

@@ -0,0 +1,98 @@
1
+<?php
2
+
3
+namespace app\common\middleware;
4
+
5
+use think\facade\Cache;
6
+use think\facade\Log;
7
+use think\Request;
8
+use think\Response;
9
+
10
+class SignatureMiddleware
11
+{
12
+    public function handle(Request $request, \Closure $next)
13
+    {
14
+        // 获取必要参数
15
+        $essential = $request->only(['timestamp', 'nonce', 'signature']);
16
+
17
+        // 验证必要参数
18
+        if (count(array_filter($essential)) !== 3) {
19
+            return $this->errorResponse('参数不完整', 400);
20
+        }
21
+
22
+        // 验证时间戳(5分钟内有效)
23
+        if (abs(time() - (int)$essential['timestamp']) > 300) {
24
+            return $this->errorResponse('请求已过期', 400);
25
+        }
26
+
27
+        // 验证nonce唯一性
28
+        $nonceKey = 'nonce:' . $essential['nonce'];
29
+        if (Cache::has($nonceKey)) {
30
+            return $this->errorResponse('重复请求', 400);
31
+        }
32
+
33
+        // 构造签名数据(与前端完全一致)
34
+        $signData = $request->param();
35
+        unset($signData['signature']);
36
+
37
+        // 按键名排序
38
+        ksort($signData);
39
+
40
+        // 生成签名字符串(与前端相同格式)
41
+        $signContent = '';
42
+        foreach ($signData as $key => $value) {
43
+            $signContent .= "{$key}={$value}&";
44
+        }
45
+        $signContent = rtrim($signContent, '&');
46
+
47
+        // 记录原始签名内容(用于调试)
48
+        Log::debug("Sign Content: " . $signContent);
49
+
50
+        // 获取公钥
51
+        $publicKey = openssl_pkey_get_public(
52
+            file_get_contents(env('RSA_PUBLIC_KEY_PATH'))
53
+        );
54
+
55
+        if (!$publicKey) {
56
+            Log::error("公钥加载失败");
57
+            return $this->errorResponse('系统错误', 500);
58
+        }
59
+
60
+        // 计算签名的MD5值(与前端一致)
61
+        $md5Hash = md5($signContent);
62
+        Log::debug("MD5 Hash: " . $md5Hash);
63
+
64
+        // 解码前端签名(前端使用公钥加密)
65
+        $signature = base64_decode($essential['signature']);
66
+
67
+        // 使用公钥解密签名
68
+        $decrypted = '';
69
+        $success = openssl_public_decrypt($signature, $decrypted, $publicKey);
70
+
71
+        if (!$success) {
72
+            Log::error("签名解密失败: " . openssl_error_string());
73
+            return $this->errorResponse('签名验证失败', 403);
74
+        }
75
+
76
+        Log::debug("解密结果: " . $decrypted);
77
+
78
+        // 比较解密后的值与MD5哈希
79
+        if ($decrypted !== $md5Hash) {
80
+            Log::error("签名验证失败: 期望 {$md5Hash}, 实际 {$decrypted}");
81
+            return $this->errorResponse('签名验证失败', 403);
82
+        }
83
+
84
+        // 记录已使用的nonce(5分钟过期)
85
+        Cache::set($nonceKey, 1, 300);
86
+
87
+        return $next($request);
88
+    }
89
+
90
+    private function errorResponse(string $message, int $code): Response
91
+    {
92
+        return json([
93
+            'code' => $code,
94
+            'msg' => $message,
95
+            'data' => null
96
+        ])->code($code);
97
+    }
98
+}

+ 37 - 0
app/controller/api/Auth.php

@@ -48,12 +48,14 @@ use think\App;
48 48
 use think\db\exception\DataNotFoundException;
49 49
 use think\db\exception\DbException;
50 50
 use think\db\exception\ModelNotFoundException;
51
+use think\Env;
51 52
 use think\Exception;
52 53
 use think\exception\HttpResponseException;
53 54
 use think\facade\Cache;
54 55
 use think\facade\Db;
55 56
 use think\facade\Log;
56 57
 use think\Filesystem;
58
+use think\Response;
57 59
 
58 60
 /**
59 61
  * Class Auth
@@ -1322,6 +1324,41 @@ class Auth extends BaseController
1322 1324
         return $res;
1323 1325
     }
1324 1326
 
1327
+
1328
+    /**
1329
+     * 获取验签公钥
1330
+     * @return Response
1331
+     */
1332
+    public function getPublicKey(): Response
1333
+    {
1334
+        $path = env('RSA_PUBLIC_KEY_PATH');
1335
+
1336
+        if (empty($path)) {
1337
+            return json([
1338
+                'code' => 500,
1339
+                'msg' => '公钥路径未配置',
1340
+                'data' => null
1341
+            ]);
1342
+        }
1343
+
1344
+        if (!file_exists($path)) {
1345
+            return json([
1346
+                'code' => 500,
1347
+                'msg' => '公钥文件不存在: ' . $path,
1348
+                'data' => null
1349
+            ]);
1350
+        }
1351
+
1352
+        $publicKey = file_get_contents($path);
1353
+        return json([
1354
+            'code' => 200,
1355
+            'msg' => 'success',
1356
+            'data' => [
1357
+                'public_key' => $publicKey
1358
+            ]
1359
+        ]);
1360
+    }
1361
+
1325 1362
     public function verify(UserAuthValidate $validate)
1326 1363
     {
1327 1364
         $data = $this->request->params(['phone', 'code', 'key',"type"]);

+ 11 - 1
route/api.php

@@ -6,6 +6,8 @@
6 6
  * Copyright (c) http://crmeb.net
7 7
  */
8 8
 
9
+use app\common\middleware\RateLimitMiddleware;
10
+use app\common\middleware\SignatureMiddleware;
9 11
 use think\facade\Route;
10 12
 Route::miss(function () {
11 13
     $DB = DIRECTORY_SEPARATOR;
@@ -969,8 +971,16 @@ Route::group('api/', function () {
969 971
     Route::get('auth/wechat', 'api.Auth/auth');
970 972
     //小程序授权
971 973
     Route::post('auth/mp', 'api.Auth/mpAuth');
974
+
975
+    // 获取验签公钥
976
+    // openssl genrsa -out request_private_key.pem 2048
977
+    // openssl rsa -in request_private_key.pem -pubout -out request_public_key.pem
978
+    Route::get('auth/getPublicKey', 'api.Auth/getPublicKey');
979
+
972 980
     //验证码
973
-    Route::post('auth/verify', 'api.Auth/verify');
981
+    Route::post('auth/verify', 'api.Auth/verify')
982
+        ->middleware(SignatureMiddleware::class)
983
+        ->middleware(RateLimitMiddleware::class);
974 984
     //微信配置
975 985
     Route::get('wechat/config', 'api.Wechat/jsConfig');
976 986
     //图片验证码