为啥我的 iOS 应用无法使用 Node.js Agora Token Server 创建的令牌通过 AgoraRtcEngineKit 进行身份验证?

Posted

技术标签:

【中文标题】为啥我的 iOS 应用无法使用 Node.js Agora Token Server 创建的令牌通过 AgoraRtcEngineKit 进行身份验证?【英文标题】:Why can't my iOS app authenticate with AgoraRtcEngineKit using tokens created by a Node.js Agora Token Server?为什么我的 iOS 应用无法使用 Node.js Agora Token Server 创建的令牌通过 AgoraRtcEngineKit 进行身份验证? 【发布时间】:2022-01-05 22:16:53 【问题描述】:

问题总结

我的目标是使用一个用 SwiftUI 编写的 ios 应用来连接 AgoraRtcEngineKit。我想创建一个纯音频应用,允许主持人广播音频并允许听众收听。

Agora 要求使用代币。

我根据此处的 Agora 教程使用 Node.js 创建了一个 Agora Token Server:https://www.agora.io/en/blog/how-to-build-a-token-server-for-agora-applications-using-nodejs/

这是来自我的 Agora-Node-TokenServer 的 index.js。此代码基于此处的 Agora 教程:https://github.com/digitallysavvy/Agora-Node-TokenServer/blob/master/index.js

const express = require('express')
const path = require('path')
const RtcTokenBuilder, RtcRole = require('agora-access-token');

const PORT = process.env.PORT || 5000

if (!(process.env.APP_ID && process.env.APP_CERTIFICATE)) 
  throw new Error('You must define an APP_ID and APP_CERTIFICATE');

const APP_ID = process.env.APP_ID;
const APP_CERTIFICATE = process.env.APP_CERTIFICATE;

const app = express();

const nocache = (req, resp, next) => 
  resp.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
  resp.header('Expires', '-1');
  resp.header('Pragma', 'no-cache');
  next();
  ;


const generateAccessToken = (req, resp) =>  
  resp.header('Access-Control-Allow-Origin', '*');

  const channelName = req.query.channelName;if (!channelName) 
    return resp.status(500).json( 'error': 'channel is required' );
  

  
  // get uid 
  let uid = req.query.uid;
  if(!uid || uid == '') 
    uid = 0;
  

  // get rtc role
  let rtcrole = RtcRole.SUBSCRIBER;
  if (req.query.rtcrole == 'publisher') 
    rtcrole = RtcRole.PUBLISHER;
  


  // get the expire time
  let expireTime = req.query.expireTime;
  if (!expireTime || expireTime == '') 
    expireTime = 3600;
   else 
    expireTime = parseInt(expireTime, 10);
  
  // calculate privilege expire time
  const currentTime = Math.floor(Date.now() / 1000);
  const privilegeExpireTime = currentTime + expireTime;

  const rtctoken = RtcTokenBuilder.buildTokenWithUid(APP_ID, APP_CERTIFICATE, channelName, uid, rtcrole, privilegeExpireTime);
 
  return resp.json( 'rtctoken': rtctoken );

;

app.get('/access_token', nocache, generateAccessToken);

app.listen(PORT, () => 
  console.log(`Listening on port: $PORT`);
);

我的预期 我的期望是能够通过 Agora 成功验证我的 iOS 应用。

实际结果 我无法通过 Agora 进行身份验证。

    我的 Node.js 令牌服务器成功地将令牌传送到我的 iOS 应用程序。 但是,当我使用 rtckit.joinChannel(byToken:...) 向 Agora 提交所述令牌时,没有任何反应。永远不会到达 joinChannel 的完成块。 作为一个实验,我使用 rtckit.joinChannel(byToken:...) 从 Agora 控制台向 Agora 提交了“临时令牌”,该令牌被成功接受并到达完成块。

我的尝试

我的 iOS 应用可以通过使用 Agora 控制台创建的 Temporary Tokens 连接到 Agora。 Agora 的控制台允许开发人员创建临时令牌来测试他们的应用程序。由于我的应用程序能够使用这些临时令牌进行身份验证,因此向我建议问题出在我创建的 NodeJS 令牌服务器的某个地方?

我在 Stack Overflow 上查看过类似的问题,例如:

    how to generate token for agora RTC for live stream and join channel 按照此处的建议,确保我的 APP_ID 和 APP_CERTIFICATE 与 Agora 控制台中的匹配:Agora Video Calling android get error code 101

以下是我的 iOS 应用程序中的相关代码供参考。我将此代码基于此处的 Agora 教程:https://github.com/maxxfrazer/Agora-iOS-Swift-Example/blob/main/Agora-iOS-Example/AgoraToken.swift 和此处:https://www.agora.io/en/blog/creating-live-audio-chat-rooms-with-swiftui/

AgoraToken

import Foundation

class AgoraToken 

    /// Error types to expect from fetchToken on failing ot retrieve valid token.
    enum TokenError: Error 
        case noData
        case invalidData
        case invalidURL
    

    /// Requests the token from our backend token service
    /// - Parameter urlBase: base URL specifying where the token server is located
    /// - Parameter channelName: Name of the channel we're requesting for
    /// - Parameter uid: User ID of the user trying to join (0 for any user)
    /// - Parameter callback: Callback method for returning either the string token or error
    static func fetchToken(
        urlBase: String, channelName: String, uid: UInt,
        callback: @escaping (Result<String, Error>) -> Void
    ) 
        guard let fullURL = URL(string: "\(urlBase)/?channelName=\(channelName)/&uid=\(uid)/") else 
            callback(.failure(TokenError.invalidURL))
            return
        
        print("fullURL yields \(fullURL)")
        var request = URLRequest(
            url: fullURL,
            timeoutInterval: 10
        )
        request.httpMethod = "GET"

        let task = URLSession.shared.dataTask(with: request)  data, response, err in
            print("Within URLSession.shared.dataTask response is \(String(describing: response)) and err is \(String(describing: err))")
            if let dataExists = data 
                print(String(bytes: dataExists, encoding: String.Encoding.utf8) ?? "URLSession no data exists")
            

            guard let data = data else 
                if let err = err 
                    callback(.failure(err))
                 else 
                    callback(.failure(TokenError.noData))
                
                return
            
            let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
            if let responseDict = responseJSON as? [String: Any], let rtctoken = responseDict["rtctoken"] as? String 
                print("rtc token is \(rtctoken)")
                callback(.success(rtctoken))
             else 
                callback(.failure(TokenError.invalidData))
            
        

        task.resume()
    
    

Podfile

# Uncomment the next line to define a global platform for your project
platform :ios, '14.8.1'

target 'AgoraPractice' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for AgoraPractice
  pod 'AgoraRtm_iOS' 
  pod 'AgoraAudio_iOS' 

  target 'AgoraPracticeTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'AgoraPracticeUITests' do
    # Pods for testing
  end

end

内容视图

import SwiftUI
import CoreData
import AgoraRtcKit



struct ContentView: View 
    @Environment(\.managedObjectContext) private var viewContext

    @State var joinedChannel: Bool = false
    @ObservedObject var agoraObservable = AgoraObservable()
    
       var body: some View 
           Form 
               Section(header: Text("Channel Information")) 
                   TextField(
                       "Channel Name", text: $agoraObservable.channelName
                   ).disabled(joinedChannel)
                   TextField(
                       "Username", text: $agoraObservable.username
                   ).disabled(joinedChannel)
               
               Button(action: 
                   joinedChannel.toggle()
                   if !joinedChannel 
                       self.agoraObservable.leaveChannel()
                    else 
                       self.agoraObservable.checkIfChannelTokenExists()
                   
               , label: 
                   Text("\(joinedChannel ? "Leave" : "Join") Channel")
                       .accentColor(joinedChannel ? .red : .blue)
               )
               if joinedChannel 

                   Button(action: 
                       agoraObservable.rtckit.setClientRole(.audience)
                   , label: 
                       Text("Become audience member")
                   )

                   Button(action: 
                       agoraObservable.rtckit.setClientRole(.broadcaster)
                   , label: 
                       Text("Become broadcasting member")
                   )

               
           
       
   

   struct UserData: Codable 
       var rtcId: UInt
       var username: String
       func toJSONString() throws -> String? 
           let jsonData = try JSONEncoder().encode(self)
           return String(data: jsonData, encoding: .utf8)
       
   

   class AgoraObservable: NSObject, ObservableObject 
       
       @Published var remoteUserIDs: Set<UInt> = []
       @Published var channelName: String = ""
       @Published var username: String = ""

       
       // Temp Token and Channel Name
       var tempToken:String = "XXXXXXX"
       var tempChannelName:String = "XXXXX"
       
       var rtcId: UInt = 0
       //var rtcId: UInt = 1264211369
       
       let tokenBaseURL:String = Secrets.baseUrl
       
       // channelToken is rtctoken.  I should change this variable name at some point to rtctoken...
       var channelToken:String = ""
       
       lazy var rtckit: AgoraRtcEngineKit = 
           let engine = AgoraRtcEngineKit.sharedEngine(
            withAppId: Secrets.agoraAppId, delegate: self
           )
           engine.setChannelProfile(.liveBroadcasting)
           engine.setClientRole(.audience)
           return engine
       ()
    
       
   

   extension AgoraObservable 
       
       func checkIfChannelTokenExists() 
           if channelToken.isEmpty 
               joinChannelWithFetch()
            else 
               joinChannel()
           
       
       
       func joinChannelWithFetch() 
           
           AgoraToken.fetchToken(
                       urlBase: tokenBaseURL,
                       channelName: self.channelName,
                       uid: self.rtcId
                   )  result in
                       switch result 
                       case .success(let tokenExists):
                           self.channelToken = tokenExists
                           print("func joinChannelWithFetch(): channelToken = \(self.channelToken) and rtcuid = \(self.rtcId)")
                           self.joinChannel()
                       case .failure(let err):
                           print(err)
                           // To Do: Handle this error with an alert
                           self.leaveChannel()
                           
                       
       
       
       func joinChannel()
           print("func joinChannel(): channelToken = \(self.channelToken) and channelName = \(self.channelName)  and rtcuid = \(self.rtcId)")

           self.rtckit.joinChannel(byToken: self.channelToken, channelId: self.channelName, info: nil, uid: self.rtcId)  [weak self] (channel, uid, errCode) in
               print("within rtckit.joinchannel yields: channel:\(channel) and uid:\(uid) and error:\(errCode)")
               
               self?.rtcId = uid
               
           
           // I need to error handle if user cannot loginto rtckit
       
       
       func updateToken(_ newToken:String)
           channelToken = newToken
           self.rtckit.renewToken(newToken)
           print("Updating token now...")
       
       
       func leaveChannel() 
           self.rtckit.leaveChannel()
       
       
  

extension AgoraObservable: AgoraRtcEngineDelegate 
    /// Called when the user role successfully changes
       /// - Parameters:
       ///   - engine: AgoraRtcEngine of this session.
       ///   - oldRole: Previous role of the user.
       ///   - newRole: New role of the user.
       func rtcEngine(
           _ engine: AgoraRtcEngineKit,
           didClientRoleChanged oldRole: AgoraClientRole,
           newRole: AgoraClientRole
       ) 
           
           print("AgoraRtcEngineDelegate didClientRoleChanged triggered...old role: \(oldRole), new role: \(newRole)")

       

       func rtcEngine(
           _ engine: AgoraRtcEngineKit,
           didJoinedOfUid uid: UInt,
           elapsed: Int
       ) 
           // Keeping track of all people in the session
           print("rtcEngine didJoinedOfUid triggered...")
           remoteUserIDs.insert(uid)
       

       func rtcEngine(
           _ engine: AgoraRtcEngineKit,
           didOfflineOfUid uid: UInt,
           reason: AgoraUserOfflineReason
       ) 
           print("rtcEngine didOfflineOfUid triggered...")
           // Removing on quit and dropped only
           // the other option is `.becomeAudience`,
           // which means it's still relevant.
           if reason == .quit || reason == .dropped 
               remoteUserIDs.remove(uid)
            else 
               // User is no longer hosting, need to change the lookups
               // and remove this view from the list
               // userVideoLookup.removeValue(forKey: uid)
           
       

       func rtcEngine(
           _ engine: AgoraRtcEngineKit,
           tokenPrivilegeWillExpire token: String
       ) 
           print("tokenPrivilegeWillExpire delegate method called...")
           AgoraToken.fetchToken(
            urlBase: tokenBaseURL, channelName: self.channelName, uid: self.rtcId)  result in
               switch result 
               case .failure(let err):
                   fatalError("Could not refresh token: \(err)")
               case .success(let newToken):
                   print("token successfully updated")
                   self.updateToken(newToken)
               
           
       



struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    


【问题讨论】:

【参考方案1】:

当我尝试为我的 http 请求定义“fullURL”以获取令牌时,我发现我犯了一个错误。我是 http 请求的新手,所以我不知道我犯了一个错误。

在我的 AgoraToken.swift 文件中,我错误的 fullURL 定义是:

        guard let fullURL = URL(string: "\(urlBase)/?channelName=\(channelName)/&uid=\(uid)/") else ...

您将能够看到您是否不是像我这样的菜鸟,使用此代码 channelName 和 uid 都将使用尾部斜杠发送到我的令牌服务器。因此,如果我的 channelName 是“TupperwareParty”,我的令牌服务器将获得“TupperwareParty/”,如果我的 uid 是“123456”,我的令牌服务器将获得“123456/”。

我用这个新的 fullURL 定义修复了它...

        guard let fullURL = URL(string: "\(urlBase)/?channelName=\(channelName)&uid=\(uid)") else ...

叹息...

【讨论】:

以上是关于为啥我的 iOS 应用无法使用 Node.js Agora Token Server 创建的令牌通过 AgoraRtcEngineKit 进行身份验证?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Heroku 无法检测到 Node.js buildpack?

Socket.io无法在Node.js前面使用Nginx反向代理

为啥当我输入 node main.js 时我的不和谐机器人无法上线?

为啥我的 cpanel 没有在软件中显示设置 node.js 应用程序图标?

为啥我的 Flask 应用程序在 Heroku 上被检测为 node.js

无法从Win Forms C#app连接到远程Node.js Socket.IO服务器