PHP项目Laravel私有频道权限控制

wen PHP项目 3

本文目录导读:

PHP项目Laravel私有频道权限控制

  1. 基础配置
  2. 私有频道权限控制实现
  3. 前端权限控制
  4. 安全最佳实践
  5. 测试示例
  6. 常见问题处理

在Laravel中实现私有频道(Private Channel)的权限控制,主要通过Channel授权路由广播认证两部分来完成,以下是详细的实现方案:

基础配置

1 安装广播器

composer require pusher/pusher-php-server

2 配置广播驱动(.env)

BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your_app_id
PUSHER_APP_KEY=your_app_key
PUSHER_APP_SECRET=your_app_secret
PUSHER_APP_CLUSTER=mt1

私有频道权限控制实现

1 定义频道授权路由

routes/channels.php 中定义:

<?php
use Illuminate\Support\Facades\Broadcast;
use App\Models\User;
use App\Models\Conversation;
use App\Models\Group;
// 用户私聊频道
Broadcast::channel('private-chat.{conversationId}', function (User $user, $conversationId) {
    $conversation = Conversation::find($conversationId);
    if (!$conversation) {
        return false;
    }
    // 验证用户是否属于该对话
    return $conversation->users->contains('id', $user->id);
});
// 群组频道
Broadcast::channel('group.{groupId}', function (User $user, $groupId) {
    $group = Group::find($groupId);
    if (!$group) {
        return false;
    }
    // 验证用户是否是群组成员且未被禁言
    $membership = $group->members()->where('user_id', $user->id)->first();
    if (!$membership) {
        return false;
    }
    // 返回用户数据(可用于前端显示)
    return [
        'id' => $user->id,
        'name' => $user->name,
        'role' => $membership->role ?? 'member'
    ];
});
// 带角色权限的频道 (需要管理员权限)
Broadcast::channel('admin.notifications', function (User $user) {
    return $user->isAdmin() || $user->hasRole('super-admin');
});

2 使用中间件进行权限控制

<?php
// app/Http/Middleware/CheckChannelAccess.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckChannelAccess
{
    public function handle(Request $request, Closure $next)
    {
        // 获取需要检查的频道参数
        $channelName = $request->input('channel_name');
        $socketId = $request->input('socket_id');
        // 解析频道名称并验证
        if (str_contains($channelName, 'private-')) {
            $result = $this->validatePrivateChannel($channelName, $request->user());
            if (!$result) {
                return response()->json(['message' => 'Unauthorized'], 403);
            }
        }
        return $next($request);
    }
    private function validatePrivateChannel($channelName, $user)
    {
        // 自定义验证逻辑
        preg_match('/private-chat\.(\d+)/', $channelName, $matches);
        if (isset($matches[1])) {
            $conversationId = $matches[1];
            return $user->conversations()->where('id', $conversationId)->exists();
        }
        return false;
    }
}

3 注册中间件

// app/Http/Kernel.php
protected $routeMiddleware = [
    // ...
    'channel.access' => \App\Http\Middleware\CheckChannelAccess::class,
];

4 自定义广播认证逻辑

创建自定义认证类:

<?php
// app/Services/BroadcastAuthService.php
namespace App\Services;
use App\Models\Channel;
use App\Models\ChannelMember;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class BroadcastAuthService
{
    /**
     * 验证私有频道访问权限
     */
    public function authorizeChannel($user, $channelName): bool
    {
        try {
            // 根据频道类型进行验证
            if (Str::startsWith($channelName, 'private-user.')) {
                return $this->validateUserChannel($user, $channelName);
            }
            if (Str::startsWith($channelName, 'private-group.')) {
                return $this->validateGroupChannel($user, $channelName);
            }
            if (Str::startsWith($channelName, 'private-project.')) {
                return $this->validateProjectChannel($user, $channelName);
            }
            return false;
        } catch (\Exception $e) {
            Log::error('Channel authorization failed: ' . $e->getMessage());
            return false;
        }
    }
    /**
     * 验证用户一对一频道
     */
    private function validateUserChannel($user, $channelName): bool
    {
        // 解析频道名称获取目标用户ID
        $targetUserId = substr($channelName, strrpos($channelName, '.') + 1);
        // 验证用户状态
        if ($user->status !== 'active') {
            return false;
        }
        // 检查是否被限制
        return !$this->isBlocked($user->id, $targetUserId);
    }
    /**
     * 验证群组频道
     */
    private function validateGroupChannel($user, $channelName): bool
    {
        $groupId = substr($channelName, strrpos($channelName, '.') + 1);
        $group = Group::find($groupId);
        if (!$group) {
            return false;
        }
        $membership = $group->members()->where('user_id', $user->id)->first();
        if (!$membership) {
            return false;
        }
        // 检查成员状态
        if ($membership->status === 'banned' || $membership->status === 'inactive') {
            return false;
        }
        // 检查群组状态
        return $group->status === 'active';
    }
    /**
     * 验证项目频道
     */
    private function validateProjectChannel($user, $channelName): bool
    {
        $projectId = substr($channelName, strrpos($channelName, '.') + 1);
        // 检查用户是否在项目中且有权限
        return $user->associatedProjects()
            ->where('project_id', $projectId)
            ->whereIn('role_id', [1, 2]) // 只允许特定角色
            ->exists();
    }
    /**
     * 检查用户是否被屏蔽
     */
    private function isBlocked($userId, $blockedUserId): bool
    {
        return BlockList::where('user_id', $userId)
            ->where('blocked_user_id', $blockedUserId)
            ->exists();
    }
}

前端权限控制

1 Vue.js + Laravel Echo

// resources/js/bootstrap.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Echo = new Echo({
    broadcaster: 'pusher',
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
    authEndpoint: '/broadcasting/auth', // 默认端点
    auth: {
        headers: {
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
            'Authorization': `Bearer ${localStorage.getItem('token')}`
        }
    }
});
// Vue 组件中使用私有频道
export default {
    mounted() {
        // 加入私有频道并处理权限
        this.joinPrivateChannel();
    },
    methods: {
        joinPrivateChannel() {
            const conversationId = this.$route.params.conversationId;
            // 尝试订阅频道
            const channel = Echo.private(`private-chat.${conversationId}`)
                .listen('.MessageSent', (event) => {
                    this.handleNewMessage(event);
                })
                .listen('.Typing', (event) => {
                    this.handleTyping(event);
                });
            // 添加错误处理
            channel.subscription_error = (error) => {
                console.error('Channel subscription failed:', error);
                this.showAccessDenied();
            };
        }
    }
}

2 频道权限状态管理

// store/modules/channel.js
import { useAuthStore } from './auth';
export const useChannelStore = defineStore('channel', {
    state: () => ({
        activeChannels: [],
        pendingChannels: [],
        deniedChannels: [],
    }),
    actions: {
        async subscribeToChannel(channelName) {
            const authStore = useAuthStore();
            // 检查用户权限
            const hasAccess = await this.checkChannelPermission(channelName);
            if (!hasAccess) {
                this.deniedChannels.push(channelName);
                throw new Error('No permission to access this channel');
            }
            const channel = window.Echo.private(channelName);
            this.activeChannels.push(channel);
            return channel;
        },
        async checkChannelPermission(channelName) {
            try {
                const response = await axios.post('/broadcasting/auth', {
                    channel_name: channelName
                });
                return response.status === 200;
            } catch (error) {
                return false;
            }
        }
    }
});

安全最佳实践

1 认证端点的加固

<?php
// app/Http/Controllers/BroadcastAuthController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class BroadcastAuthController extends Controller
{
    public function authenticate(Request $request)
    {
        try {
            // 获取已认证用户
            $user = Auth::user();
            if (!$user) {
                return response()->json(['message' => 'Unauthenticated'], 401);
            }
            // 验证 socket_id
            if (!$request->has('socket_id') || !$request->has('channel_name')) {
                return response()->json(['message' => 'Missing parameters'], 400);
            }
            // 自定义频道路由逻辑
            $channelName = $request->input('channel_name');
            // 通过频道授权服务的完整验证
            $authService = app(BroadcastAuthService::class);
            $authData = $this->getChannelData($user, $channelName);
            if (!$authData) {
                // 记录失败日志
                Log::warning('Channel authorization failed', [
                    'user_id' => $user->id,
                    'channel_name' => $channelName,
                    'ip' => $request->ip()
                ]);
                return response()->json(['message' => 'Unauthorized'], 403);
            }
            // 生成认证响应
            return Broadcast::socket($request->input('socket_id'))
                ->to($channelName)
                ->auth($authData);
        } catch (\Exception $e) {
            Log::error('Broadcast auth error: ' . $e->getMessage());
            return response()->json(['message' => 'Server error'], 500);
        }
    }
    private function getChannelData($user, $channelName)
    {
        // 根据业务逻辑返回适当的用户数据
        return [
            'id' => $user->id,
            'name' => $user->name,
            'avatar' => $user->avatar
        ];
    }
}

2 速率限制

// routes/channels.php 或 RouteServiceProvider 中配置
Route::post('/broadcasting/auth', function (Request $request) {
    // 使用中间件组
})->middleware(['throttle:10,1', 'channel.access']);

3 订阅日志审计

<?php
// app/Listeners/BroadcastEventListener.php
namespace App\Listeners;
use Illuminate\Broadcasting\BroadcastEvent;
use Illuminate\Support\Facades\Log;
class BroadcastEventListener
{
    public function handle($event)
    {
        if ($event instanceof SubscriptionRequested) {
            Log::info('Channel subscription requested', [
                'user_id' => $event->user->id,
                'channel' => $event->channelName,
                'timestamp' => now()->toDateTimeString()
            ]);
        }
    }
}

测试示例

<?php
// tests/Feature/ChannelAuthTest.php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ChannelAuthTest extends TestCase
{
    use RefreshDatabase;
    public function test_user_can_subscribe_to_authorized_channel()
    {
        $user = User::factory()->create();
        $conversation = Conversation::factory()->create();
        $conversation->users()->attach($user->id);
        $response = $this->actingAs($user)
            ->post('/broadcasting/auth', [
                'channel_name' => "private-chat.{$conversation->id}",
                'socket_id' => str_random(20)
            ]);
        $response->assertStatus(200)
            ->assertJsonStructure(['auth']);
    }
    public function test_user_cannot_subscribe_to_unauthorized_channel()
    {
        $user = User::factory()->create();
        $otherUser = User::factory()->create();
        $conversation = Conversation::factory()->create();
        $conversation->users()->attach($otherUser->id); // 只有其他用户
        $response = $this->actingAs($user)
            ->post('/broadcasting/auth', [
                'channel_name' => "private-chat.{$conversation->id}",
                'socket_id' => str_random(20)
            ]);
        $response->assertStatus(403);
    }
}

常见问题处理

事务性权限改变时主动断开频道

<?php
use Pusher\Pusher;
$pusher = new Pusher(
    config('broadcasting.connections.pusher.key'),
    config('broadcasting.connections.pusher.secret'),
    config('broadcasting.connections.pusher.app_id'),
    ['cluster' => config('broadcasting.connections.pusher.options.cluster')]
);
// 当用户被移除群组时踢出频道
$pusher->terminateUserConnections($userId);

通过以上实现,你可以构建安全、灵活的私有频道权限控制系统,记住要定期审查权限规则,并始终遵循最小权限原则。

抱歉,评论功能暂时关闭!