Laravel Echo - Allow guests to connect to presence channel

蹲街弑〆低调 提交于 2021-02-06 10:57:20

问题


I am using laravel-echo-server to run Laravel Echo to broadcast events.

I have a user counter channel which shows all the users on the app. For this I am using a presence channel. This works fine for logged in users, but guests just never get connected.

I've setup the below in the BroadcastServiceProvider:

Broadcast::channel('global', function () { return ['name' => 'guest']; });

Which from what I can tell, should allow everyone in as 'guests'. I'm guessing there's some middleware or auth that's being checked before this that I need to disable for this channel.

Any help on getting all clients joining this presence channel would be much appreciated!


回答1:


For anyone looking for answers to this. It is indeed possible to auth guests into presence channels you just need to override the Broadcast::routes() from the service provider with your own.

As an example my presence channel 'global' accepts guests:

Route::post('/broadcasting/auth', function(Illuminate\Http\Request $req) { if($req->channel_name == 'presence-global'){return 'global';} return abort(403); });

This could be extended in various directions, or could continue to pass other presence and private channels through to the default Broadcast::auth method




回答2:


The other solutions didn't work for me for a guest presence channel, this is what I ended up with:

// routes/channels.php

<?php
use Illuminate\Auth\GenericUser;

/*
|--------------------------------------------------------------------------
| Broadcast Channels
|--------------------------------------------------------------------------
|
| Here you may register all of the event broadcasting channels that your
| application supports. The given channel authorization callbacks are
| used to check if an authenticated user can listen to the channel.
|
*/

Route::post('/custom/broadcast/auth/route', function () {
    $user = new GenericUser(['id' => microtime()]);

    request()->setUserResolver(function () use ($user) {
        return $user;
    });

    return Broadcast::auth(request());
});

Broadcast::channel('online.{uuid}', function ($user, $uuid) {
    return [
        'id' => $user->id,
        'uuid' => $uuid
    ];
});






回答3:


You may create a temporary user with factory(User::class)->make(...) and authenticate it with a middleware to use it as a guest.

Step 1: Creating the middleware

Run: php artisan make:middleware AuthenticateGuest

In app/Http/Middleware/AuthenticateGuest.php:

public function handle($request, Closure $next)
{
    Auth::login(factory(User::class)->make([
        'id' => (int) str_replace('.', '', microtime(true))
    ]));

    return $next($request);
}

Now setup the AuthenticateGuest middleware in Kernel.php.

In app\Http\Kernel.php:

protected $routeMiddleware = [
    ...
    'authenticate-guest' => \App\Http\Middleware\AuthenticateGuest::class,
];

Step 2: Setup Broadcast::channel route

In routes/channels.php:

Broadcast::channel('chatroom', function ($user) {
    return $user; // here will return the guest user object
});

More at: https://laravel.com/docs/8.x/broadcasting#authorizing-presence-channels




回答4:


With the help of Renan Coelho i got it to work. The missing part for me was to override the Broadcast::routes() method with the following:

Route::post('/broadcasting/auth', function (Illuminate\Http\Request $req) {
    return Broadcast::auth($req);
});

Route::post('/broadcasting/auth'... is actually a route that gets added through the "Broadcast::routes()" method. This is why we override it here. You can see the active routes by typing php artisan route:list in your terminal.

Then, Renan Coelho already said, i had to add a custom Middleware (AuthenticateGuest) that creates a random user for me. (This is the hacky part) and add it to the $middleware array in the kernel.php:

protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\TrustProxies::class,
        \Barryvdh\Cors\HandleCors::class,

        \App\Http\Middleware\AuthenticateGuest::class
    ];

The AuthenticateGuest Middleware looks like the following:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;
use App\User;

class AuthenticateGuest
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        Auth::login(factory(User::class)->make([
            'id' => (int)str_replace('.', '', microtime(true))
        ]));

        return $next($request);
    }
}

Hope that helps someone,

Sebastian




回答5:


You can create your own auth guard and it's also pretty simple but more complex.

  1. Create a class which will implement Authenticable Interface.
  2. Create UserProvider.
  3. Create a new Guard.
  4. Register Guard and UserProvider in AuthServiceProvider.
  5. Add provider and guard in config/auth.php
  6. Use your new guard.

Advantages

  • You don't have to modify auth endpoint
  • You don't have to change default guard
  • You base on Laravel Auth system
  • Keep support of multiple tabs in the browser
  • Can be used with web guard at the same time
  • Keep all the advantages of using PresenceChannel

Disadvantages

  • A lot to code

So,

1. Create a new class which will implement Authenticable interface.

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use JsonSerializable;

/**
 * @property string $id
 * @property string $name
 */
class Session implements Authenticatable, Jsonable, Arrayable, JsonSerializable
{

    private $id;

    private $attributes = [];

    public function __construct($id)
    {
        $this->id = $id;
        $this->name = "Guest";
    }

    /**
     * Get the name of the unique identifier for the user.
     *
     * @return string
     */
    public function getAuthIdentifierName()
    {
        return 'id';
    }

    /**
     * Get the unique identifier for the user.
     *
     * @return mixed
     */
    public function getAuthIdentifier()
    {
        return $this->{$this->getAuthIdentifierName()};
    }

    /**
     * Get the password for the user.
     *
     * @return string
     */
    public function getAuthPassword()
    {
        return "";
    }

    /**
     * Get the token value for the "remember me" session.
     *
     * @return string
     */
    public function getRememberToken()
    {
        return $this->{$this->getAuthIdentifierName()};
    }

    /**
     * Set the token value for the "remember me" session.
     *
     * @param  string $value
     * @return void
     */
    public function setRememberToken($value)
    {
        $this->{$this->getRememberToken()} = $value;
    }

    /**
     * Get the column name for the "remember me" token.
     *
     * @return string
     */
    public function getRememberTokenName()
    {
        return "token";
    }

    public function __get($name)
    {
        return $this->attributes[$name];
    }

    public function __set($name, $value)
    {
        $this->attributes[$name] = $value;
    }

    /**
     * Convert the object to its JSON representation.
     *
     * @param  int $options
     * @return string
     */
    public function toJson($options = 0)
    {
        return json_encode($this);
    }

    /**
     * Get the instance as an array.
     *
     * @return array
     */
    public function toArray()
    {
        return $this->attributes;
    }

    /**
     * Specify data which should be serialized to JSON
     * @link https://php.net/manual/en/jsonserializable.jsonserialize.php
     * @return mixed data which can be serialized by <b>json_encode</b>,
     * which is a value of any type other than a resource.
     * @since 5.4.0
     */
    public function jsonSerialize()
    {
        return $this->attributes;
    }
}

Modify this as you wish, but you shouldn't serialize $id property

2. Create UserProvider

<?php namespace App\Extensions;

use App\Models\Session;
use Illuminate\Cache\Repository;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Support\Fluent;
use Illuminate\Support\Str;

class SessionUserProvider implements UserProvider
{

    private $store;

    /**
     * SessionUserProvider constructor.
     * @param Repository $store
     */
    public function __construct(Repository $store)
    {
        $this->store = $store;
    }


    /**
     * Retrieve a user by their unique identifier.
     *
     * @param  mixed $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveById($identifier)
    {
        return new Session(
            $this->getUniqueTokenForSession($identifier)
        );
    }

    /**
     * Retrieve a user by their unique identifier and "remember me" token.
     *
     * @param  mixed $identifier
     * @param  string $token
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByToken($identifier, $token)
    {
        return null;
    }

    /**
     * Update the "remember me" token for the given user in storage.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable $user
     * @param  string $token
     * @return void
     */
    public function updateRememberToken(Authenticatable $user, $token)
    {
        return;
    }

    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        return null;
    }

    private function unpack($data)
    {
        return json_decode($data);
    }

    private function getUniqueTokenForSession($id)
    {
        return $this->retrieveCacheDataForSession($id)
            ->get('uuid');
    }

    private function retrieveCacheDataForSession($id)
    {
        $fluent = new Fluent(
            $this->unpack(
                $this->store->has($id) ? $this->store->get($id) : "[]"
            )
        );

        if(!$fluent->__isset('uuid')) {
            $fluent->__set('uuid', Str::random(128));
        }

        $this->store->put($id, $fluent->toJson(), 60 * 60 * 60);

        return $fluent;

    }

    /**
     * Validate a user against the given credentials.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable $user
     * @param  array $credentials
     * @return bool
     */
    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        return null;
    }
}

Identifier property in retrieveById method is always session id if you are using broadcasting so you can also use this as a token.

3. Create new Guard

<?php namespace App\Services\Auth;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;

class GuestGuard implements Guard
{

    private $user;
    protected $request;
    protected $provider;

    /**
     * GuestGuard constructor.
     * @param UserProvider $provider
     * @param Request $request
     */
    public function __construct(UserProvider $provider, Request $request)
    {
        $this->provider = $provider;
        $this->request = $request;
    }


    /**
     * Determine if the current user is authenticated.
     *
     * @return bool
     */
    public function check()
    {
        return !is_null($this->user);
    }
    
    /**
     * Determine if the current user is a guest.
     *
     * @return bool
     */
    public function guest()
    {
        return !$this->check();
    }

    /**
     * Get the currently authenticated user.
     *
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function user()
    {
        if($this->check()) {
            return $this->user;
        }

        $this->setUser(
            $this->provider->retrieveById(
                $this->request->session()->getId()
            )
        );

        return $this->user;
    }

    /**
     * Get the ID for the currently authenticated user.
     *
     * @return int|null
     */
    public function id()
    {
        return !is_null($this->user) ? $this->user->id : null;
    }

    /**
     * Validate a user's credentials.
     *
     * @param  array $credentials
     * @return bool
     */
    public function validate(array $credentials = [])
    {
        return false;
    }

    /**
     * Set the current user.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable $user
     * @return void
     */
    public function setUser(Authenticatable $user)
    {
        $this->user = $user;
    }
}

Here in user method you pass session id as identifier, using broadcasting only this method is nessesary.

4. Register Guard and UserProvider in AuthServiceProvider.

// app/Providers/AuthServiceProvider.php

   /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Auth::provider('sessions', function (Application $app) {
            return new SessionUserProvider(
                $app->make('cache.store')
            );
        });

        Auth::extend('guest', function (Application $app, $name, array $config) {
            return new GuestGuard(Auth::createUserProvider($config['provider']), $app->make('request'));
        });
    }

5.1 Add provider in config/auth.php

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],
        
        // New
        'sessions' => [
         'driver' => 'sessions',
         'model' => App\Models\Session::class,
        ],
    ],

5.2 Add guard in config/auth.php

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],

        // New
        'guest' => [
            'driver' => 'guest',
            'provider' => 'sessions'
        ]
    ],

6. Use your new guard

// routes/channels.php

Broadcast::channel('chat.{id}', function (Authenticatable $user){
    return $user;
}, ['guards' => ['guest']]);

Notice that you can use 'web' as a guard at the same time ('web' should be before 'guest'). It allows you to find out who is a guest and who is a logged in user - you can just check instance of Authenticable in channel callback.

And that how it looks in the laravel-echo-server database




回答6:


My solution to issue:

BroadcastServiceProvider.php (~/app/Providers/)

public function boot()
{
    if (request()->hasHeader('V-Auth')) { /* Virtual client. */
        Broadcast::routes(['middleware' => 'client_chat.broadcast.auth']);
    } else {
        Broadcast::routes();
    }

    require base_path('routes/channels.php');
}

Kernel.php (~/app/Http/)

protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    ...
    'client_chat.broadcast.auth' => \App\Http\Middleware\ClientChatBroadcasting::class,
];

ClientChatBroadcasting.php (~/app/Http/Middleware/)

public function handle($request, Closure $next)
{
    if (/** your condition **/) {
        $fakeUser = new User;
        $fakeUser->virtual_client = true;
        $fakeUser->id = /** whatever you want **/;
        $fakeUser->name = '[virtual_client]';
        $fakeUser->asdasdasdasdasd = 'asdasdasdasdasd';

        $request->merge(['user' => $fakeUser]);
        $request->setUserResolver(function () use ($fakeUser) {
            return $fakeUser;
        });
    }

    return $next($request);
}

ChatChannel.php (~/app/Broadcasting/Chat/)

Broadcast::channel('chat.{chatId}', ChatChannel::class); Channel Classes

public function join($member/**($fakeUser)**/, $chatId)
{
    $memberData = [/** your data **/];

    /* If there is no member data (null), then there will be an authentication error. */
    return $memberData;
}

[place in your js file, where you want connect to broadcasting]

this.Echo = new Echo({
        broadcaster: 'socket.io',
        host: /** your host **/,
        reconnectionAttempts: 60,
        encrypted: true,
        auth: {
            headers: {
                'V-Auth': true,
                'Access-Token': accessToken,
                'Virtual-Id': virtualId,
                'Chat-Id': chatId
            }
        }
    });


来源:https://stackoverflow.com/questions/43341820/laravel-echo-allow-guests-to-connect-to-presence-channel

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!