Forráskód Böngészése

feat:账号ip白名单检测

aiden 4 hónapja
szülő
commit
49ea767eae

+ 63 - 4
app/common.php

@@ -130,13 +130,18 @@ if(!function_exists('checkUserLogin')){
  * @param array $user 用户信息数组
  * @param string $controller 控制器名称
  * @param string $action 操作名称
+ * @param bool $checkIp 是否检查IP白名单,默认false
  * @return bool 有权限返回true,无权限返回false
  */
 if(!function_exists('checkPermission')){
-    function checkPermission($user, $controller, $action) {
+    function checkPermission($user, $controller, $action, $checkIp = false) {
         // 超级管理员拥有所有权限
         $superAdminRoleId = \think\facade\Config::get('permission.super_admin_role_id', 1);
         if ($user['user_role'] == $superAdminRoleId) {
+            // 即使是超级管理员,如果需要检查IP,也要验证
+            if ($checkIp) {
+                return checkUserIpWhiteList($user);
+            }
             return true;
         }
         
@@ -149,8 +154,62 @@ if(!function_exists('checkPermission')){
         $privileges = $role->privileges;
         
         // 检查是否有对应权限
-        return isset($privileges[$controller]) && 
-               is_array($privileges[$controller]) && 
-               in_array($action, $privileges[$controller]);
+        $hasPermission = isset($privileges[$controller]) && 
+                        is_array($privileges[$controller]) && 
+                        in_array($action, $privileges[$controller]);
+        
+        // 如果有权限且需要检查IP,则进一步验证IP白名单
+        if ($hasPermission && $checkIp) {
+            return checkUserIpWhiteList($user);
+        }
+        
+        return $hasPermission;
+    }
+}
+
+/**
+ * 检查用户IP白名单
+ *
+ * @param array $user 用户信息数组
+ * @return bool IP在白名单中返回true,否则返回false
+ */
+if(!function_exists('checkUserIpWhiteList')){
+    function checkUserIpWhiteList($user) {
+        // 获取用户完整信息
+        $userModel = \app\model\UserModel::where('user_id', $user['user_id'])
+            ->where('merchant_id', $user['merchant_id'])
+            ->find();
+            
+        if (!$userModel) {
+            return false;
+        }
+        
+        // 获取客户端IP
+        $clientIp = \app\service\IpWhiteListService::getRealIp();
+        
+        // 检查IP白名单
+        return \app\service\IpWhiteListService::checkIpWhiteList($clientIp, $userModel->white_list_ip);
+    }
+}
+
+/**
+ * 检查用户登录状态并验证IP白名单
+ *
+ * @param bool $checkIp 是否检查IP白名单,默认false
+ * @return array|null 验证通过返回用户信息数组,失败返回null
+ */
+if(!function_exists('checkUserLoginWithIp')){
+    function checkUserLoginWithIp($checkIp = false) {
+        $user = checkUserLogin();
+        if (!$user) {
+            return null;
+        }
+        
+        // 如果需要检查IP白名单
+        if ($checkIp && !checkUserIpWhiteList($user)) {
+            return null;
+        }
+        
+        return $user;
     }
 }

+ 111 - 2
app/controller/User.php

@@ -10,6 +10,7 @@ use think\facade\Config;
 use app\model\UserModel;
 use app\model\UserRoleModel;
 use app\validate\UserValidate;
+use app\service\IpWhiteListService;
 
 class User extends BaseController
 {
@@ -22,7 +23,8 @@ class User extends BaseController
         'create_suc' => '创建用户成功',
         'empty' => '用户不存在',
         'suc' => '操作成功',
-        'res' => '获取成功'
+        'res' => '获取成功',
+        'ip_denied' => 'IP地址不在白名单中,禁止登录'
     ];
 
     /**
@@ -47,6 +49,17 @@ class User extends BaseController
         // 查询用户
         $user = UserModel::where('user_name', $userName)->find();
         if ($user && password_verify($password, $user->password)) {
+            
+            // 检查IP白名单
+            $clientIp = IpWhiteListService::getRealIp();
+            if (!IpWhiteListService::checkIpWhiteList($clientIp, $user->white_list_ip)) {
+                // 记录IP限制登录日志
+                trace("用户 {$userName} 尝试从IP {$clientIp} 登录,但不在白名单 {$user->white_list_ip} 中", 'info');
+                return json_error([
+                    'client_ip' => $clientIp,
+                    'white_list_ip' => $user->white_list_ip
+                ], $this->message['ip_denied']);
+            }
 
             $token = generateToken([
                 'user_id' => $user->user_id,
@@ -58,12 +71,16 @@ class User extends BaseController
             $user->login_time = time();
             $user->save();
 
+            // 记录成功登录日志
+            trace("用户 {$userName} 从IP {$clientIp} 登录成功", 'info');
+
             return json_success([
                 'user_name' => $user->user_name,
                 'nick_name' => $user->nick_name,
                 'user_role' => $user->user_role,
                 'login_time' => $user->login_time,
-                'token' => $token
+                'token' => $token,
+                'client_ip' => $clientIp
             ], $this->message['login']);
         } else {
             return json_error([], $this->message['error']);
@@ -341,6 +358,98 @@ class User extends BaseController
         }
     }
     
+    /**
+     * 验证IP白名单格式
+     */
+    public function validateIpWhiteList()
+    {
+        $loginInfo = checkUserLogin();
+        if (!$loginInfo) {
+            return json_error([], '请先登录');
+        }
+        
+        $whiteListIp = Request::post('white_list_ip', '', 'trim');
+        
+        try {
+            list($isValid, $message, $parsedList) = IpWhiteListService::validateWhiteListFormat($whiteListIp);
+            
+            return json_success([
+                'valid' => $isValid,
+                'message' => $message,
+                'parsed_list' => $parsedList,
+                'current_ip' => IpWhiteListService::getRealIp()
+            ], '验证完成');
+        } catch (\Exception $e) {
+            return json_error([], '验证失败:' . $e->getMessage());
+        }
+    }
+    
+    /**
+     * 获取当前访问IP信息
+     */
+    public function getCurrentIp()
+    {
+        $loginInfo = checkUserLogin();
+        if (!$loginInfo) {
+            return json_error([], '请先登录');
+        }
+        
+        try {
+            $currentIp = IpWhiteListService::getRealIp();
+            $ipInfo = IpWhiteListService::getIpInfo($currentIp);
+            
+            return json_success([
+                'current_ip' => $currentIp,
+                'ip_info' => $ipInfo,
+                'timestamp' => time(),
+                'datetime' => date('Y-m-d H:i:s')
+            ], '获取当前IP成功');
+        } catch (\Exception $e) {
+            return json_error([], '获取IP信息失败:' . $e->getMessage());
+        }
+    }
+    
+    /**
+     * 检查IP是否在用户白名单中
+     */
+    public function checkIpWhiteList()
+    {
+        $loginInfo = checkUserLogin();
+        if (!$loginInfo) {
+            return json_error([], '请先登录');
+        }
+        
+        $userId = Request::get('user_id', $loginInfo['user_id'], 'intval');
+        $testIp = Request::get('test_ip', '', 'trim');
+        
+        // 获取用户信息
+        $user = UserModel::where('user_id', $userId)
+            ->where('merchant_id', $loginInfo['merchant_id'])
+            ->find();
+            
+        if (!$user) {
+            return json_error([], '用户不存在');
+        }
+        
+        $currentIp = IpWhiteListService::getRealIp();
+        $checkIp = !empty($testIp) ? $testIp : $currentIp;
+        
+        try {
+            $isAllowed = IpWhiteListService::checkIpWhiteList($checkIp, $user->white_list_ip);
+            
+            return json_success([
+                'user_id' => $userId,
+                'user_name' => $user->user_name,
+                'check_ip' => $checkIp,
+                'white_list_ip' => $user->white_list_ip,
+                'is_allowed' => $isAllowed,
+                'current_ip' => $currentIp,
+                'is_current_ip' => $checkIp === $currentIp
+            ], $isAllowed ? 'IP在白名单中' : 'IP不在白名单中');
+        } catch (\Exception $e) {
+            return json_error([], '检查IP白名单失败:' . $e->getMessage());
+        }
+    }
 
     /**
      * 验证输入数据

+ 70 - 0
app/middleware/IpWhiteListMiddleware.php

@@ -0,0 +1,70 @@
+<?php
+declare (strict_types=1);
+
+namespace app\middleware;
+
+use app\service\IpWhiteListService;
+use app\model\UserModel;
+use think\Response;
+
+/**
+ * IP白名单中间件
+ * 用于在需要的控制器或方法中验证用户IP白名单
+ */
+class IpWhiteListMiddleware
+{
+    /**
+     * 处理请求
+     *
+     * @param \think\Request $request
+     * @param \Closure       $next
+     * @return Response
+     */
+    public function handle($request, \Closure $next)
+    {
+        // 获取当前用户登录信息
+        $loginInfo = checkUserLogin();
+        
+        // 如果未登录,跳过IP检查(登录检查由其他中间件处理)
+        if (!$loginInfo) {
+            return $next($request);
+        }
+        
+        // 获取用户信息
+        $user = UserModel::where('user_id', $loginInfo['user_id'])
+            ->where('merchant_id', $loginInfo['merchant_id'])
+            ->find();
+            
+        if (!$user) {
+            return json([
+                'state' => 0,
+                'code' => 401,
+                'message' => '用户信息不存在',
+                'data' => []
+            ]);
+        }
+        
+        // 获取客户端IP
+        $clientIp = IpWhiteListService::getRealIp();
+        
+        // 检查IP白名单
+        if (!IpWhiteListService::checkIpWhiteList($clientIp, $user->white_list_ip)) {
+            // 记录IP限制访问日志
+            trace("用户 {$user->user_name} 尝试从IP {$clientIp} 访问 {$request->pathinfo()},但不在白名单 {$user->white_list_ip} 中", 'warning');
+            
+            return json([
+                'state' => 0,
+                'code' => 403,
+                'message' => 'IP地址不在白名单中,禁止访问',
+                'data' => [
+                    'client_ip' => $clientIp,
+                    'white_list_ip' => $user->white_list_ip,
+                    'requested_url' => $request->pathinfo()
+                ]
+            ]);
+        }
+        
+        // IP检查通过,继续执行
+        return $next($request);
+    }
+}

+ 326 - 0
app/service/IpWhiteListService.php

@@ -0,0 +1,326 @@
+<?php
+declare (strict_types=1);
+
+namespace app\service;
+
+class IpWhiteListService
+{
+    /**
+     * 验证IP是否在白名单中
+     * 
+     * @param string $clientIp 客户端IP
+     * @param string $whiteListIp 白名单IP配置
+     * @return bool
+     */
+    public static function checkIpWhiteList(string $clientIp, string $whiteListIp): bool
+    {
+        // 如果白名单为空,表示不限制IP
+        if (empty($whiteListIp)) {
+            return true;
+        }
+        
+        // 解析白名单IP配置
+        $whiteList = self::parseWhiteListIp($whiteListIp);
+        
+        foreach ($whiteList as $allowedIp) {
+            if (self::matchIp($clientIp, $allowedIp)) {
+                return true;
+            }
+        }
+        
+        return false;
+    }
+    
+    /**
+     * 解析白名单IP配置
+     * 支持格式:
+     * - 单个IP: 192.168.1.1
+     * - IP段: 192.168.1.0/24
+     * - IP范围: 192.168.1.1-192.168.1.100
+     * - 多个配置用逗号分隔: 192.168.1.1,10.0.0.0/8,172.16.0.1-172.16.0.100
+     * 
+     * @param string $whiteListIp
+     * @return array
+     */
+    public static function parseWhiteListIp(string $whiteListIp): array
+    {
+        $whiteList = [];
+        $items = array_filter(array_map('trim', explode(',', $whiteListIp)));
+        
+        foreach ($items as $item) {
+            if (!empty($item)) {
+                $whiteList[] = $item;
+            }
+        }
+        
+        return $whiteList;
+    }
+    
+    /**
+     * 匹配IP地址
+     * 
+     * @param string $clientIp 客户端IP
+     * @param string $allowedIp 允许的IP配置
+     * @return bool
+     */
+    private static function matchIp(string $clientIp, string $allowedIp): bool
+    {
+        // 精确匹配
+        if ($clientIp === $allowedIp) {
+            return true;
+        }
+        
+        // CIDR格式匹配 (例: 192.168.1.0/24)
+        if (strpos($allowedIp, '/') !== false) {
+            return self::matchCidr($clientIp, $allowedIp);
+        }
+        
+        // IP范围匹配 (例: 192.168.1.1-192.168.1.100)
+        if (strpos($allowedIp, '-') !== false) {
+            return self::matchRange($clientIp, $allowedIp);
+        }
+        
+        // 通配符匹配 (例: 192.168.1.*)
+        if (strpos($allowedIp, '*') !== false) {
+            return self::matchWildcard($clientIp, $allowedIp);
+        }
+        
+        return false;
+    }
+    
+    /**
+     * CIDR格式IP匹配
+     * 
+     * @param string $clientIp
+     * @param string $cidr
+     * @return bool
+     */
+    private static function matchCidr(string $clientIp, string $cidr): bool
+    {
+        list($subnet, $mask) = explode('/', $cidr);
+        
+        if (!filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ||
+            !filter_var($clientIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
+            return false;
+        }
+        
+        $mask = intval($mask);
+        if ($mask < 0 || $mask > 32) {
+            return false;
+        }
+        
+        $clientIpLong = ip2long($clientIp);
+        $subnetLong = ip2long($subnet);
+        $maskLong = (-1 << (32 - $mask));
+        
+        return ($clientIpLong & $maskLong) === ($subnetLong & $maskLong);
+    }
+    
+    /**
+     * IP范围匹配
+     * 
+     * @param string $clientIp
+     * @param string $range
+     * @return bool
+     */
+    private static function matchRange(string $clientIp, string $range): bool
+    {
+        list($startIp, $endIp) = explode('-', $range, 2);
+        $startIp = trim($startIp);
+        $endIp = trim($endIp);
+        
+        if (!filter_var($startIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ||
+            !filter_var($endIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ||
+            !filter_var($clientIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
+            return false;
+        }
+        
+        $clientIpLong = ip2long($clientIp);
+        $startIpLong = ip2long($startIp);
+        $endIpLong = ip2long($endIp);
+        
+        return $clientIpLong >= $startIpLong && $clientIpLong <= $endIpLong;
+    }
+    
+    /**
+     * 通配符匹配
+     * 
+     * @param string $clientIp
+     * @param string $pattern
+     * @return bool
+     */
+    private static function matchWildcard(string $clientIp, string $pattern): bool
+    {
+        $pattern = str_replace('.', '\.', $pattern);
+        $pattern = str_replace('*', '[0-9]+', $pattern);
+        $pattern = '/^' . $pattern . '$/';
+        
+        return preg_match($pattern, $clientIp) === 1;
+    }
+    
+    /**
+     * 获取客户端真实IP
+     * 
+     * @return string
+     */
+    public static function getRealIp(): string
+    {
+        $headers = [
+            'HTTP_X_FORWARDED_FOR',
+            'HTTP_X_REAL_IP',
+            'HTTP_CLIENT_IP',
+            'HTTP_CF_CONNECTING_IP',
+            'HTTP_X_CLUSTER_CLIENT_IP',
+            'REMOTE_ADDR'
+        ];
+        
+        foreach ($headers as $header) {
+            if (!empty($_SERVER[$header])) {
+                $ips = explode(',', $_SERVER[$header]);
+                $ip = trim($ips[0]);
+                
+                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
+                    return $ip;
+                }
+            }
+        }
+        
+        return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
+    }
+    
+    /**
+     * 验证IP白名单格式
+     * 
+     * @param string $whiteListIp
+     * @return array [bool $isValid, string $message, array $parsedList]
+     */
+    public static function validateWhiteListFormat(string $whiteListIp): array
+    {
+        if (empty($whiteListIp)) {
+            return [true, '白名单为空,表示不限制IP访问', []];
+        }
+        
+        $items = self::parseWhiteListIp($whiteListIp);
+        $validItems = [];
+        $errors = [];
+        
+        foreach ($items as $item) {
+            $validation = self::validateSingleIpFormat($item);
+            if ($validation['valid']) {
+                $validItems[] = [
+                    'input' => $item,
+                    'type' => $validation['type'],
+                    'description' => $validation['description']
+                ];
+            } else {
+                $errors[] = "'{$item}': " . $validation['message'];
+            }
+        }
+        
+        $isValid = empty($errors);
+        $message = $isValid ? '白名单格式正确' : '白名单格式错误: ' . implode(', ', $errors);
+        
+        return [$isValid, $message, $validItems];
+    }
+    
+    /**
+     * 验证单个IP配置格式
+     * 
+     * @param string $ipConfig
+     * @return array
+     */
+    private static function validateSingleIpFormat(string $ipConfig): array
+    {
+        // 精确IP
+        if (filter_var($ipConfig, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
+            return [
+                'valid' => true,
+                'type' => 'exact',
+                'description' => "精确IP: {$ipConfig}"
+            ];
+        }
+        
+        // CIDR格式
+        if (strpos($ipConfig, '/') !== false) {
+            list($subnet, $mask) = explode('/', $ipConfig);
+            if (filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && 
+                is_numeric($mask) && $mask >= 0 && $mask <= 32) {
+                return [
+                    'valid' => true,
+                    'type' => 'cidr',
+                    'description' => "CIDR网段: {$ipConfig}"
+                ];
+            }
+        }
+        
+        // IP范围
+        if (strpos($ipConfig, '-') !== false) {
+            list($startIp, $endIp) = explode('-', $ipConfig, 2);
+            $startIp = trim($startIp);
+            $endIp = trim($endIp);
+            
+            if (filter_var($startIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) &&
+                filter_var($endIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
+                return [
+                    'valid' => true,
+                    'type' => 'range',
+                    'description' => "IP范围: {$startIp} 到 {$endIp}"
+                ];
+            }
+        }
+        
+        // 通配符
+        if (strpos($ipConfig, '*') !== false) {
+            $parts = explode('.', $ipConfig);
+            if (count($parts) === 4) {
+                $valid = true;
+                foreach ($parts as $part) {
+                    if ($part !== '*' && (!is_numeric($part) || $part < 0 || $part > 255)) {
+                        $valid = false;
+                        break;
+                    }
+                }
+                if ($valid) {
+                    return [
+                        'valid' => true,
+                        'type' => 'wildcard',
+                        'description' => "通配符IP: {$ipConfig}"
+                    ];
+                }
+            }
+        }
+        
+        return [
+            'valid' => false,
+            'message' => '不支持的IP格式'
+        ];
+    }
+    
+    /**
+     * 获取IP地理位置信息(简单实现)
+     * 
+     * @param string $ip
+     * @return array
+     */
+    public static function getIpInfo(string $ip): array
+    {
+        // 内网IP检测
+        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
+            return [
+                'ip' => $ip,
+                'type' => 'private',
+                'location' => '内网IP',
+                'isp' => '内网'
+            ];
+        }
+        
+        // 这里可以集成第三方IP地理位置服务
+        // 如:百度、腾讯、阿里云等IP查询API
+        return [
+            'ip' => $ip,
+            'type' => 'public',
+            'location' => '未知',
+            'isp' => '未知'
+        ];
+    }
+}

+ 6 - 2
config/middleware.php

@@ -2,7 +2,11 @@
 // 中间件配置
 return [
     // 别名或分组
-    'alias'    => [],
+    'alias'    => [
+        'ipwhitelist' => app\middleware\IpWhiteListMiddleware::class,
+    ],
     // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
-    'priority' => [],
+    'priority' => [
+        app\middleware\IpWhiteListMiddleware::class,
+    ],
 ];

+ 42 - 0
route/app.php

@@ -15,3 +15,45 @@ Route::get('think', function () {
 });
 
 Route::get('hello/:name', 'index/hello');
+
+// 用户相关路由
+Route::group('user', function () {
+    Route::post('login', 'User/login');
+    Route::post('logout', 'User/logout');
+    Route::post('createUser', 'User/createUser');
+    Route::get('list', 'User/list');
+    Route::get('detail', 'User/detail');
+    Route::post('update', 'User/update');
+    Route::post('delete', 'User/delete');
+    // IP白名单相关
+    Route::post('validateIpWhiteList', 'User/validateIpWhiteList');
+    Route::get('getCurrentIp', 'User/getCurrentIp');
+    Route::get('checkIpWhiteList', 'User/checkIpWhiteList');
+});
+
+// 角色相关路由
+Route::group('role', function () {
+    Route::get('list', 'UserRole/list');
+    Route::get('detail', 'UserRole/detail');
+    Route::post('create', 'UserRole/create');
+    Route::post('update', 'UserRole/update');
+    Route::post('delete', 'UserRole/delete');
+    Route::get('permissions', 'UserRole/getPermissions');
+});
+
+// 菜单相关路由
+Route::group('menu', function () {
+    Route::get('getMenuTree', 'Menu/getMenuTree');
+    Route::get('getBreadcrumb', 'Menu/getBreadcrumb');
+    Route::get('getControllerPermissions', 'Menu/getControllerPermissions');
+    Route::get('checkMenuPermission', 'Menu/checkMenuPermission');
+});
+
+// 权限相关路由
+Route::group('permission', function () {
+    Route::get('list', 'Permission/list');
+    Route::get('getGroups', 'Permission/getGroups');
+    Route::get('getPermissionsByGroup', 'Permission/getPermissionsByGroup');
+    Route::post('validatePermissions', 'Permission/validatePermissions');
+    Route::post('formatPermissions', 'Permission/formatPermissions');
+});