websocket 401(Unauthorized)当订阅Pusher(Vue/Laravel)中的私有通道时

zsohkypk  于 2024-01-08  发布在  其他
关注(0)|答案(2)|浏览(120)

bounty将在2天后过期。回答此问题可获得+300声望奖励。Artur Müller Romanov希望引起更多关注此问题。

我已经通过Pusher设置了一个实时聊天的Vue3/Laravel应用程序,它可以通过非私人通道chat工作。在下一步中,我想使用私人通道,但奇怪的事情发生了。试图向/api/pusher/auth发送请求的pusher.subscribe函数似乎无法正确处理sanctum authorization,导致:

POST http://localhost:8000/api/pusher/auth 401 (Unauthorized)
ajax @ pusher-js.js?v=1974b27b:676
(anonymous) @ pusher-js.js?v=1974b27b:3548
authorize @ pusher-js.js?v=1974b27b:1860
subscribe @ pusher-js.js?v=1974b27b:1828
subscribe @ pusher-js.js?v=1974b27b:3960
subscribeAll @ pusher-js.js?v=1974b27b:3951
(anonymous) @ pusher-js.js?v=1974b27b:3868
emit @ pusher-js.js?v=1974b27b:1230
updateState @ pusher-js.js?v=1974b27b:2341
connected @ pusher-js.js?v=1974b27b:2281
callback @ pusher-js.js?v=1974b27b:2176
cb @ pusher-js.js?v=1974b27b:2619
tryNextStrategy @ pusher-js.js?v=1974b27b:2459
(anonymous) @ pusher-js.js?v=1974b27b:2507
(anonymous) @ pusher-js.js?v=1974b27b:3399
finish @ pusher-js.js?v=1974b27b:1752
onMessage @ pusher-js.js?v=1974b27b:1729
emit @ pusher-js.js?v=1974b27b:1230
onMessage @ pusher-js.js?v=1974b27b:1327
socket.onmessage @ pusher-js.js?v=1974b27b:1343
Show 20 more frames
Show less
pusher-js.js?v=1974b27b:979 Pusher :  : ["Error: Unable to retrieve auth string from channel-authorization endpoint - received status: 401 from http://localhost:8000/api/pusher/auth. Clients must be authorized to join private or presence channels. See: https://pusher.com/docs/channels/server_api/authorizing-users/"]

字符串
这个问题是pusher路由特有的,所有其他API路由都可以正常工作。

前端设置

pusher.js

import Pusher from 'pusher-js'

Pusher.logToConsole = true

const pusher = new Pusher('bf29be46d8eb2ea8ccd4', {
  cluster: 'eu',
  forceTLS: true,
  authEndpoint: 'http://localhost:8000/api/pusher/auth',
  withCredentials: true,
  wsPort: 443,
  wssPort: 443,
  enableStats: false,
  enabledTransports: ['ws', 'wss'],
  auth: {
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
    },
  }
})

export default pusher

xiaos.js

import axios from 'axios'

axios.defaults.withCredentials = true

if (import.meta.env.DEV) {
  axios.defaults.baseURL = 'http://localhost:8000'
}


用户在登录组件中被认证:

const signIn = () => {
  axios.get('/sanctum/csrf-cookie').then(() => {
    axios
      .post('/login', form)
      .then(() => {
        store.auth = sessionStorage.auth = 1
        store.signInModal = false
      })
      .catch((er) => {
        state.errors = er.response.data.errors
      })
  })
}


并且该应用尝试在聊天组件中订阅推送器,只有经过身份验证的用户才能访问:

const inquireChatSession = () => {
  axios
    .get(`/api/chat/${props.id}`)
    .then((res) => {
      state.chatSessionId = res.data.id
      state.messages = res.data.messages
      state.loadingSession = false

      const channel = pusher.subscribe(`private-chat.${state.chatSessionId}`) // 401 happens here

      channel.bind('App\\Events\\ChatMessageSent', (data) => {
        state.messages.push(data.chatMessage)
      })
    })
    .catch((er) => {
      state.errors = er.response.data.errors
      state.loadingSession = false
    })
}

后台设置

/routes/API.php

Route::post('/pusher/auth', function (Request $request) {
    \Log::info('test');

    $user = $request->user();
    if (!$user) {
        abort(403, 'Unauthorized');
    }

    $pusher = new Pusher(
        env('PUSHER_APP_KEY'),
        env('PUSHER_APP_SECRET'),
        env('PUSHER_APP_ID'),
        ['cluster' => env('PUSHER_APP_CLUSTER')]
    );

    $channelName = $request->channel_name;
    $socketId = $request->socket_id;

    $auth = $pusher->socket_auth($channelName, $socketId);

    return response()->json(['auth' => $auth]);
})->middleware('auth:sanctum');


pusher正在尝试连接到此路由,但授权失败,导致'test'未被记录并将上述错误返回给客户端。这似乎是一个pusher + sanctum问题,因为连接到此诊断路由可以正常工作:

Route::post('/pusher/auth', function (Request $request) {
    \Log::info(var_export($request, true));
    \Log::info('Request headers: ', $request->header());
    \Log::info('Request cookies: ', $request->cookies->all());
    \Log::info('Session data: ', $request->session()->all());
    \Log::info('User: ', $request->user());
})


但是$request->cookies->all()是空的,而$request->user()null。由于某种原因,没有auth cookies到达推送器路由。要检查sanctum是否自己工作,连接到以下路由,返回授权用户:

Route::middleware('auth:sanctum')->get('/test-auth', function (Request $request) {
    return $request->user();
});


相关的**.env**条目:

APP_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000
SANCTUM_STATEFUL_DOMAINS=localhost:3000
SESSION_DOMAIN=localhost

PUSHER_APP_ID=1728518
PUSHER_APP_KEY=bf29be46d8eb2ea8ccd4
PUSHER_APP_SECRET=...
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=eu


就像我说的,应用程序的整个身份验证工作正常,除了推送器路由。

浏览器网络页签

验证请求#1

概述:

Request URL:       http://localhost:8000/api/pusher/auth
Request Method:    OPTIONS
Status Code:       204 No Content
Remote Address:    127.0.0.1:8000
Referrer Policy:   strict-origin-when-cross-origin


响应标题:

Access-Control-Allow-Credentials:    true
Access-Control-Allow-Headers:        x-requested-with
Access-Control-Allow-Methods:        POST
Access-Control-Allow-Origin:         http://localhost:3000
Access-Control-Max-Age:              0
Cache-Control:                       no-cache, private
Connection:                          close
Content-Type:                        text/html; charset=UTF-8
Date:                                Wed, 27 Dec 2023 02:25:13 GMT
Host:                                localhost:8000
Vary:                                Access-Control-Request-Method, Access-Control-Request-Headers
X-Powered-By:                        PHP/8.3.1


请求标题:

Accept:                            */*
Accept-Encoding:                   gzip, deflate, br
Accept-Language:                   en-GB,en;q=0.9,de;q=0.8
Access-Control-Request-Headers:    x-requested-with
Access-Control-Request-Method:     POST
Cache-Control:                     no-cache
Connection:                        keep-alive
Host:                              localhost:8000
Origin:                            http://localhost:3000
Pragma:                            no-cache
Referer:                           http://localhost:3000/
Sec-Fetch-Dest:                    empty
Sec-Fetch-Mode:                    cors
Sec-Fetch-Site:                    same-site
User-Agent:                        Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36


然后是auth request #2
概述:

Request URL:        http://localhost:8000/api/pusher/auth
Request Method:     POST
Status Code:        401 Unauthorized
Remote Address:     127.0.0.1:8000
Referrer Policy:    strict-origin-when-cross-origin


响应标题:

Access-Control-Allow-Credentials:    true
Access-Control-Allow-Origin:         http://localhost:3000
Cache-Control:                       no-cache, private
Connection:                          close
Content-Type:                        application/json
Date:                                Wed, 27 Dec 2023 02:25:13 GMT
Host:                                localhost:8000
Set-Cookie:                          XSRF-TOKEN=eyJpdiI6Inl5T2ZLbndpZG1OUTV5MmxNdDlNNWc9PSIsInZhbHVlIjoiUzdJYVkzZzJvM3FnaUlIUGxVWFBDTTZYeHQveTBWOWoxSEsvcThGM00wVDh6WExmK2RYWVBldTNxK2xKS1RrV1JSTHA2b0NEMVFtQzlzSmxyVVVRbmlrSmNRdmJQaW00cWpIQVFyZkhYM0RwampuMDZWVzJsV3NUZjVJZ1kxaG0iLCJtYWMiOiI4MzFlZjBjYWZkNDZkZDBhMGYxZDgwMDQ5YTgzY2ExNDg1NDMyNjFlNTNmZDg5NGJmZTI4MDMxNzAzMjVlNjZjIiwidGFnIjoiIn0%3D; expires=Wed, 27 Dec 2023 04:25:13 GMT; Max-Age=7200; path=/; domain=localhost; samesite=lax
Set-Cookie: soul_meatcom_session=eyJpdiI6Ii8wTktnTVFMZUZBYXVTeTFTRjd4dmc9PSIsInZhbHVlIjoicDUzNjFJVEYyTVR5cXdrTGZQZWZ1NzF4UEZ6QUJXSWF3YUsya0lUZy9qb0IwNk0rM0cwa3RwV1YyZ1Q0T0JqWW90cjZKd2d3OXNqOW13aGswc2tPMGw0d0hPRkxDZDdqamFUQWpKSktVd2ZpS1c2b3NqQm5WMVhoK2VsLzJWeEkiLCJtYWMiOiI4YjM3ZWEwZWMwNzlhNDIxMWNhNjBhMjAzNzcxNDM0NGMxNTczOTU1YWQ0ZGFjNzEyYWJkNDI2ZWJiNjI2ZTZkIiwidGFnIjoiIn0%3D; expires=Wed, 27 Dec 2023 04:25:13 GMT; Max-Age=7200; path=/; domain=localhost; httponly; samesite=lax
X-Powered-By:                        PHP/8.3.1


请求标题:

Accept:                   */*
Accept-Encoding:          gzip, deflate, br
Accept-Language:          en-GB,en;q=0.9,de;q=0.8
Cache-Control:            no-cache
Connection:               keep-alive
Content-Length:           87
Content-Type:             application/x-www-form-urlencoded
Host:                     localhost:8000
Origin:                   http://localhost:3000
Pragma:                   no-cache
Referer:                  http://localhost:3000/
Sec-Ch-Ua:                "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
Sec-Ch-Ua-Mobile:         ?0
Sec-Ch-Ua-Platform:       "macOS"
Sec-Fetch-Dest:           empty
Sec-Fetch-Mode:           cors
Sec-Fetch-Site:           same-site
User-Agent:               Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
X-Requested-With:         XMLHttpRequest

浏览器应用页签

/storage/cookies/http://localhost:3000中有两个cookie:

1 soul_meatcom_session:

eyJpdiI6ImI4dUNWL0pCRmRibFMzNUdyT0JrL3c9PSIsInZhbHVlIjoiQTN4L0djZm52bjlCSFV4TmM0QU1oNUFoT0JBUlFGcGNWMFVwSDEvTFZBWmZwVi9kSEFnUitHcit6MzRLNXVkNkxLU1o5a0VhWmJ2OTNvYUdxMkpyVDVUcVZoQWRzckVlVi84Tis3UTdxazhkR0ozU1EyeldnaFowcStTRFFJYjgiLCJtYWMiOiJkNjc3MGM1ODc2MWM1NWFiMDBlNjYzMTg0OWI3M2RiZmNmZGU5NzU4Y2QzZDA0NmViZDQzZjIzODBiMWZiYWM1IiwidGFnIjoiIn0%3D

2 XSRF-TOKEN:

eyJpdiI6Ik1xc0tCckEzS2RNNURFVWJ5aGc2Z0E9PSIsInZhbHVlIjoidVE4TElScENjRFlEbUFtVk1sVzZ1MGU5WDI2NXk2b214aEpWbU10K1hJUGZtQzdFOFBHV3JYblZiYmlFSmQvaSt2ZWQ5cWtjOXhtZXJQTmQ0NUNSVjAvQ2xmVDNwcUw0dkRFMHZnclRSc08wanVqaHdlbGFWeE5JMk1pTzRXOFgiLCJtYWMiOiJkNzQwODAwYzU2ZGE0OTVjNzQ0MjQxNzAwZDIxMGVkNGNkZTJjNWI2NjQ2YjMzZjk4NGM1YzI4MWJhOWZmMGI2IiwidGFnIjoiIn0%3D


我在阅读网络头方面不是很有经验。你知道pusher路由的问题是什么吗?或者我如何进一步调试它?提前谢谢你

编辑:

我试过@suxgri的方法:

import Pusher from 'pusher-js'

Pusher.logToConsole = true

const pusher = new Pusher('bf29be46d8eb2ea8ccd4', {
  cluster: 'eu',
  forceTLS: true,
  authEndpoint: 'http://localhost:8000/api/pusher/auth',
  withCredentials: true,
  wsPort: 443,
  wssPort: 443,
  enableStats: false,
  enabledTransports: ['ws', 'wss'],
  auth: {
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'Authorization':
        'Bearer eyJpdiI6ImpCdC96YjVqNmh3VXhic0tkeXlYQ3c9PSIsInZhbHVlIjoiN0YzMzB3QkVySDdyeFdIK0JLaXM1cWVaTDZSd3FTUkNoWHdDSEFVaTQ3ZktYMi94ak5yQVpMRkNNNjBNbWswamM0emJGa2RVRktuNU4yUXBLMUxoMU90UGxyZk94bzdUSythMlFFNmNGdFlydjdhYVI4WmlIRXk4dEdKQU9kRnYiLCJtYWMiOiIwOWI3MDg4MGZmYTAzYWY3N2QzOGM5ZmQ4MjNkMjIyNDg5OGRjZTk5YjNjNTAwZjE5MWY2YjIxZTMyMGQ3NWU0IiwidGFnIjoiIn0=',
    },
  },
})

export default pusher


但我还是得到了401

xzlaal3s

xzlaal3s1#

Laravel返回401错误代码,因为auth:sanctum中间件停止了请求,因为您没有发送auth参数(访问令牌),请参阅下面auth -> header中的新行。然后它应该工作或至少到达控制器并记录'test'

import Pusher from 'pusher-js'

Pusher.logToConsole = true

const pusher = new Pusher('bf29be46d8eb2ea8ccd4', {
  cluster: 'eu',
  forceTLS: true,
  authEndpoint: 'http://localhost:8000/api/pusher/auth',
  withCredentials: true,
  wsPort: 443,
  wssPort: 443,
  enableStats: false,
  enabledTransports: ['ws', 'wss'],
  auth: {
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'Authorization': 'Bearer your-token',
    },
  }
})

export default pusher

字符串
请注意,上述答案适用于移动的+API应用程序,但不适用于spa+API应用程序。

编辑:

来自Laravel文档(sanctum#spa authentiction)
在此请求期间,Laravel将设置一个包含当前CSRF令牌的XSRF-TOKEN cookie。然后,该令牌将在后续请求的X-XSRF-TOKEN头中传递,某些HTTP客户端库(如Axios和Angular HttpClient)将自动为您执行此操作。如果您的JavaScript HTTP库没有为您设置值,您需要手动设置X-XSRF-TOKEN标头以匹配此路由设置的XSRF-TOKEN cookie的值。
看起来axios没有在你发布的任何请求头中传递XSRF-TOKEN,所以我会修改如下:

auth: {
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'X-XSRF-TOKEN': 'your-token',
    },
}


接下来,我将检查http://localhost:8000/api/pusher/auth请求,以确保令牌通过,如果问题仍然存在,我将遵循以下步骤:
Laravel docs -> Sanctum ->授权私人广播频道

dvtswwa3

dvtswwa32#

Pusher JS代码的反射

如果使用身份验证来访问API路由,则对于涉及访问API路由的任何请求,都需要存在并传输适当的令牌。

const pusher = new Pusher('your-public-key', {
  cluster: 'eu',
  forceTLS: true,
  authEndpoint: 'http://localhost:8000/api/pusher/auth',
  auth: {
    headers: {
      Authorization: "Bearer ${token}", // here | value of "token" can get from backend
    },
  }
})

字符串

Laravel Echo解决方案

我使用了文档推荐的Laravel Echo客户端包来建立广播通信。

Laravel文档还涵盖了通道侦听所需的身份验证方法。

  • Laravel Echo授权请求
    laravel-echo包作为开发依赖项安装,与pusher-js包一起安装。
npm install --save-dev laravel-echo pusher-js

在客户端所需的位置创建必要的Echo类,以建立连接。

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
 
window.Pusher = Pusher;
 
window.Echo = new Echo({
  broadcaster: 'pusher',
  key: import.meta.env.VITE_PUSHER_APP_KEY,
  cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
  forceTLS: true,
  
  // can use encrypted connection
  // encrypted: true,

  // if using Sanctum or another method for API authentication, the broadcast listener will need a valid Bearer Token, which must be passed in the header
  auth: {
    headers: {
      Authorization: `Bearer ${token}`, // here | value of "token" can get from backend
    },
  },

  // for specifying a custom endpoint, use:
  // authEndpoint: '/custom/endpoint/auth',
  
  // for entirely custom authentication, remember to pass the token in the headers
  // authorizer: (channel, options) => { ... }
});


在此之后,通过调用全局window.Echo,您可以连接到通道。

const inquireChatSession = () => {
  axios
    .get(`/api/chat/${props.id}`)
    .then((res) => {
      state.chatSessionId = res.data.id
      state.messages = res.data.messages
      state.loadingSession = false

      const channel = window.Echo.private(`private-chat.${state.chatSessionId}`)

      channel.listen('App\\Events\\ChatMessageSent', (event) => {
        state.messages.push(event.chatMessage)
      })
    })
    .catch((error) => {
      state.errors = error.response.data.errors
      state.loadingSession = false
    })
}


我不知道你的意图,但我必须指出,一旦执行then()函数,在代码中监听channel将立即停止。因此,建议定义一个外部channel变量,并在执行then()分支时将监听通道写入其中。

let channel; // here

const inquireChatSession = () => {
  axios
    .get(`/api/chat/${props.id}`)
    .then((res) => {
      state.chatSessionId = res.data.id
      state.messages = res.data.messages
      state.loadingSession = false

      channel = window.Echo.private(`private-chat.${state.chatSessionId}`)

      channel.listen('App\\Events\\ChatMessageSent', (event) => {
        state.messages.push(event.chatMessage)
      })
    })
    .catch((error) => {
      state.errors = error.response.data.errors
      state.loadingSession = false
    })
}

额外

在Laravel中创建channel

建议根据文档定义事件。之后,您可以在应用程序中的任何位置在定义的通道上创建事件。

  • 使用PrivateChannel创建Laravel事件- Laravel
  • 使用event()辅助程序的事件- Laravel
    app/Events/ChatMessageSent.php
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ChatMessageSent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $chatMessage;

    public function __construct($chatMessage)
    {
        $this->chatMessage = $chatMessage;
    }

    public function broadcastOn()
    {
        return new PrivateChannel('private-chat.' . $this->chatMessage['chatSessionId']);
    }
}

ChatController.php

use App\Events\ChatMessageSent;

public function sendMessage(Request $request)
{
    event(new ChatMessageSent($chatMessage));

    return response()->json(['status' => 'Message sent']);
}

相关问题