Поделиться через


Учебник. Добавление проверки подлинности и разрешений в приложение при использовании Azure Web PubSub

Из учебника по созданию приложения чата вы узнали, как использовать API WebSocket для отправки и получения данных с помощью Azure Web PubSub. Обратите внимание, что для простоты он не требует проверки подлинности. Хотя в Azure Web PubSub для подключения необходим маркер доступа, интерфейс API negotiate, который использовался в этом учебнике для создания маркера доступа, не требует проверки подлинности. Любой пользователь может вызвать этот API для получения маркера доступа.

В реальных приложениях обычно требуется, чтобы пользователь сначала выполнил вход, чтобы получить возможность использовать ваше приложение. Из этого учебника вы узнаете, как интегрировать Web PubSub с системой проверки подлинности и авторизации приложения, чтобы сделать его более безопасным.

Полный пример кода для работы с этим учебником можно найти на сайте GitHub.

В этом руководстве описано следующее:

  • Включение проверки подлинности GitHub
  • Добавление ПО промежуточного слоя для проверки подлинности в приложение
  • Добавление разрешений в клиенты

Добавление проверки подлинности для приложения комнаты чата

В этом учебнике используется приложение чата, созданное в учебнике по созданию приложения чата. Вы также можете клонировать полный пример кода для приложения чата с GitHub.

Из этого учебника вы узнаете, как добавить проверку подлинности в приложение чата и интегрировать ее с Web PubSub.

Сначала добавьте проверку подлинности GitHub в комнату чата, чтобы пользователь мог использовать для входа учетную запись GitHub.

  1. Установите зависимости.

    npm install --save cookie-parser
    npm install --save express-session
    npm install --save passport
    npm install --save passport-github2
    
  2. Найдите файл в каталоге server.js и включите проверку подлинности GitHub, добавив следующий код server.js:

    const app = express();
    
    const users = {};
    passport.use(
      new GitHubStrategy({
        clientID: process.argv[3],
        clientSecret: process.argv[4]
      },
      (accessToken, refreshToken, profile, done) => {
        users[profile.id] = profile;
        return done(null, profile);
      }
    ));
    
    passport.serializeUser((user, done) => {
      done(null, user.id);
    });
    
    passport.deserializeUser((id, done) => {
      if (users[id]) return done(null, users[id]);
      return done(`invalid user id: ${id}`);
    });
    
    app.use(cookieParser());
    app.use(session({
      resave: false,
      saveUninitialized: true,
      secret: 'keyboard cat'
    }));
    app.use(passport.initialize());
    app.use(passport.session());
    app.get('/auth/github', passport.authenticate('github', { scope: ['user:email'] }));
    app.get('/auth/github/callback', passport.authenticate('github', { successRedirect: '/' }));
    

    В приведенном выше коде для включения проверки подлинности GitHub используется Passport.js. Ниже приведена простая иллюстрация принципов работы.

    1. /auth/github перенаправляет пользователя на github.com для входа.
    2. После входа GitHub перенаправит вас на страницу /auth/github/callback с кодом для приложения, чтобы завершить проверку подлинности. (Чтобы узнать, как профиль, возвращенный из GitHub, проверяется и сохраняется на сервере, см. пример обратного вызова в passport.use().)
    3. После завершения проверки подлинности вы будете перенаправлены на домашнюю страницу (/) сайта.

    Дополнительные сведения об OAuth и Passport.js на GitHub см. в следующих статьях:

    Для тестирования сначала необходимо создать приложение OAuth GitHub:

    1. Перейдите на сайт https://www.github.com, откройте свой профиль и выберите Settings>Developer settings (Параметры > Параметры разработчика).
    2. Перейдите к приложениям OAuth, а затем выберите New OAuth App (Создать приложение OAuth).
    3. Укажите имя приложения и URL-адрес домашней страницы (URL-адрес может быть любым) и задайте для параметра Authorization callback URL (URL-адрес обратного вызова авторизации) значение http://localhost:8080/auth/github/callback. Этот URL-адрес соответствует API обратного вызова, который вы указывали на сервере.
    4. После регистрации приложения скопируйте идентификатор клиента и щелкните Generate a new client secret (Сгенерировать новый секрет клиента).

    Выполните приведенную ниже команду, чтобы проверить настройки. Не забудьте заменить <connection-string>, <client-id> и <client-secret> своими значениями.

    export WebPubSubConnectionString="<connection-string>"
    export GitHubClientId="<client-id>"
    export GitHubClientSecret="<client-secret>"
    node server
    

    Теперь откройте http://localhost:8080/auth/github. Система перенаправит вас на GitHub для входа. После входа вы будете перенаправлены в приложение чата.

  3. Обновите комнату чата, чтобы использовать удостоверение, полученное от GitHub, вместо запроса имени пользователя.

    Обновите public/index.html для прямого вызова /negotiate без передачи идентификатора пользователя.

    let messages = document.querySelector('#messages');
    let res = await fetch(`/negotiate`);
    if (res.status === 401) {
      let m = document.createElement('p');
      m.innerHTML = 'Not authorized, click <a href="/auth/github">here</a> to login';
      messages.append(m);
      return;
    }
    let data = await res.json();
    let ws = new WebSocket(data.url);
    

    При входе пользователя в систему запрос будет автоматически передавать удостоверение пользователя через файл cookie. Поэтому вам нужно лишь проверить, существует ли пользователь в объекте req, и добавить имя пользователя в маркер доступа Web PubSub:

    app.get('/negotiate', async (req, res) => {
      if (!req.user || !req.user.username) {
        res.status(401).send('missing user id');
        return;
      }
      let options = {
        userId: req.user.username
      };
      let token = await serviceClient.getClientAccessToken(options);
      res.json({
        url: token.url
      });
    });
    

    Теперь повторно запустите сервер, и вы увидите сообщение "не авторизовано" в первый раз, когда вы открываете комнату чата. Выберите ссылку для входа, а затем вы увидите, что она работает раньше.

Работа с разрешениями

В предыдущих учебниках вы узнали, как использовать WebSocket.send(), чтобы напрямую публиковать сообщения в других клиентах с помощью подпротокола. В реальном приложении может быть так, что у клиента не должно быть возможности выполнять публикацию в любой группе или подписываться на нее без контроля разрешений. В этом разделе описано, как управлять клиентами с помощью системы разрешений Web PubSub.

В Web PubSub клиент может выполнять следующие типы операций с подпротоколом:

  • отправка событий на сервер;
  • публикация сообщений в группе;
  • присоединение к группе (подписка на нее).

Отправка события на сервер является для клиента операцией по умолчанию. Протокол не используется, поэтому он всегда разрешен. Для публикации в группе и подписки на нее клиенту необходимо получить разрешение. Существует два способа предоставления разрешения клиентам со стороны сервера:

  • указание ролей при подключении клиента (роль — это концепция, представляющая исходные разрешения при подключении клиента);
  • использование интерфейса API для предоставления разрешения клиенту после его подключения.

В случае с разрешением на присоединение к группе клиент по-прежнему должен присоединиться к группе с помощью соответствующего сообщения после того, как он получит разрешение. Другой вариант предусматривает, что сервер может также использовать интерфейс API для добавления клиента в группу, даже если у него нет разрешения на присоединение.

Теперь давайте используем эту систему разрешений для добавления новой функции в комнату чата. Вы добавляете в комнату чата новый тип пользователя, вызываемого администратором . Администратор может отправлять системные сообщения (сообщения, начинающиеся с "[SYSTEM]") непосредственно от клиента.

Сначала необходимо разделить системные и пользовательские сообщения на две разные группы, чтобы соответствующие разрешения можно было контролировать отдельно.

Измените server.js для отправки разных сообщений в разные группы:

let handler = new WebPubSubEventHandler(hubName, {
  path: '/eventhandler',
  handleConnect: (req, res) => {
    res.success({
      groups: ['system', 'message'],
    });
  },
  onConnected: req => {
    console.log(`${req.context.userId} connected`);
    serviceClient.group('system').sendToAll(`${req.context.userId} joined`, { contentType: 'text/plain' });
  },
  handleUserEvent: (req, res) => {
    if (req.context.eventName === 'message') {
      serviceClient.group('message').sendToAll({
        user: req.context.userId,
        message: req.data
      });
    }
    res.success();
  }
});

Приведенный выше код использует WebPubSubServiceClient.group().sendToAll() для отправки сообщения в группу, а не в концентратор.

Так как сообщение теперь отправляется в группы, нам нужно добавить в группы клиенты, чтобы они могли продолжать получать сообщения. Используйте обработчик handleConnect, чтобы добавить клиенты в группы.

Примечание.

handleConnect активируется, когда клиент пытается подключиться к Web PubSub. В этом обработчике можно вернуть группы и роли, чтобы служба могла добавить подключение к группам или предоставить роли, как только подключение будет установлено. Служба также может использовать res.fail() для запрета подключения.

Для активации handleConnect перейдите к параметрам обработчика событий на портале Azure и выберите пункт Подключить в системных событиях.

Кроме того, необходимо обновить код HTML клиента, так как теперь сервер отправляет сообщения в формате JSON вместо обычного текста:

let ws = new WebSocket(data.url, 'json.webpubsub.azure.v1');
ws.onopen = () => console.log('connected');

ws.onmessage = event => {
  let m = document.createElement('p');
  let message = JSON.parse(event.data);
  switch (message.type) {
    case 'message':
      if (message.group === 'system') m.innerText = `[SYSTEM] ${message.data}`;
      else if (message.group === 'message') m.innerText = `[${message.data.user}] ${message.data.message}`;
      break;
  }
  messages.appendChild(m);
};

let message = document.querySelector('#message');
message.addEventListener('keypress', e => {
  if (e.charCode !== 13) return;
  ws.send(JSON.stringify({
    type: 'event',
    event: 'message',
    dataType: 'text',
    data: message.value
  }));
  message.value = '';
});

Затем измените код клиента для отправки сообщения в системную группу, когда пользователь выбирает системное сообщение:

<button id="system">system message</button>
...
<script>
  (async function() {
    ...
    let system = document.querySelector('#system');
    system.addEventListener('click', e => {
      ws.send(JSON.stringify({
        type: 'sendToGroup',
        group: 'system',
        dataType: 'text',
        data: message.value
      }));
      message.value = '';
    });
  })();
</script>

По умолчанию клиент не имеет разрешения на отправку в какую-либо группу. Обновите код сервера для предоставления разрешения администратору (для простоты идентификатор администратора указывается в виде аргумента командной строки).

app.get('/negotiate', async (req, res) => {
  ...
  if (req.user.username === process.argv[2]) options.claims = { role: ['webpubsub.sendToGroup.system'] };
  let token = await serviceClient.getClientAccessToken(options);
});

Теперь воспользуйтесь командой node server <admin-id>. Вы видите, что при входе <admin-id>в систему можно отправить системное сообщение каждому клиенту.

Однако если вы войдете в систему от имени другого пользователя, то при выборе системного сообщения ничего не произойдет. Возможно, потребуется, чтобы служба выдавала ошибку с сообщением о том, что операция не разрешена. Чтобы обеспечить такой отзыв, можно задать ackId при публикации сообщения. При ackId каждом указании Web PubSub возвращает сообщение с соответствующим ackId значением, чтобы указать, выполнена ли операция успешно или нет.

Измените код для отправки системного сообщения на следующий:

let ackId = 0;
system.addEventListener('click', e => {
  ws.send(JSON.stringify({
    type: 'sendToGroup',
    group: 'system',
    ackId: ++ackId,
    dataType: 'text',
    data: message.value
    }));
  message.value = '';
});

Кроме того, измените код для обработки сообщений так, чтобы он обрабатывал сообщение ack:

ws.onmessage = event => {
  ...
  switch (message.type) {
    case 'ack':
      if (!message.success && message.error.name === 'Forbidden') m.innerText = 'No permission to send system message';
      break;
  }
};

Теперь перезапустите сервер и выполните вход от имени другого пользователя. При попытке отправить системное сообщение появится сообщение об ошибке.

Полный пример кода для этого руководства см. на сайте GitHub.

Следующие шаги

В этом учебнике показано, как подключиться к службе Web PubSub и публиковать сообщения для подключенных клиентов с помощью подпротокола.

Дополнительные сведения об использовании службы Web PubSub см. в других учебниках, доступных в документации.