分享方式:


教學課程:在使用 Azure Web PubSub 時,將驗證和權限新增至您的應用程式

建置聊天應用程式中,您已了解如何使用 WebSocket API 搭配 Azure Web PubSub 傳送和接收資料。 您注意到,為了簡單起見,它不需要任何驗證。 雖然 Azure Web PubSub 需要連線存取權杖,但教學課程中用來產生存取權杖的 negotiate API 不需要驗證。 任何人都可以呼叫這個 API 以取得存取權杖。

在真實世界應用程式中,一般會希望使用者先登入,才能使用您的應用程式。 在這個教學課程中,您了解如何將 Azure 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 檔案,並將下列程式代碼新增至 server.js,啟用 GitHub 驗證:

    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: '/' }));
    

    上述程式代碼使用 Passport.js 啟用 GitHub 驗證。 以下是其運作方式的簡單圖示:

    1. /auth/github 重新導向至 github.com 進行登入。
    2. 登入之後,GitHub 會使用應用程式的程式碼,將您重新導向至 /auth/github/callback 完成驗證。 (若要查看從 GitHub 傳回的設定檔如何驗證及保存於伺服器,請參閱 passport.use() 中已驗證的回呼。)
    3. 驗證完成後,系統會將您重新導向至網站的首頁 (/)。

    如需 GitHub OAuth 和Passport.js 的詳細資料,請參閱下列文章:

    若要測試這個項目,您必須先建立 GitHub OAuth 應用程式:

    1. 移至 https://www.github.com,開啟您的設定檔,然後選取[設定] >[開發人員設定]。
    2. 移至 OAuth 應用程式,然後選取[新增 OAuth 應用程式]。
    3. 填入應用程式名稱和首頁 URL (URL 可以是任何您想要的專案),並將 [授權回呼 URL]設定為 http://localhost:8080/auth/github/callback。 這個 URL 符合您在伺服器公開的回呼 API。
    4. 註冊應用程式之後,複製用戶端識別碼,然後選取[產生新的用戶端密碼]。

    執行下列命令來測試設定,而且別忘了以您的值取代 <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 處理常式將用戶端新增至群組。

注意

用戶端嘗試連線到 Web PubSub 時會觸發 handleConnect。 在這個處理常式,您可以傳回群組和角色,因此服務可以在建立連線後立即將連線新增至群組或授與角色。 服務也可以使用 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 服務,請閱讀文件提供的其他教學課程。