Azure 通知中心 - 使用 .NET 后端通知 iOS 用户

概述

利用 Azure 中的推送通知支持,你可以访问易于使用且向外扩展的多平台推送基础结构,这大大简化了为移动平台的使用者应用程序和企业应用程序实现推送通知的过程。本教程说明如何使用 Azure 通知中心将推送通知发送到特定设备上的特定应用程序用户。ASP.NET WebAPI 后端用于对客户端进行身份验证并生成通知,如指南主题从应用后端注册中所述。

Note

本教程假设您已根据通知中心入门 (iOS) 中所述创建并配置了通知中心。此外,只有在学习本教程后,才可以学习安全推送 (iOS) 教程。

创建 WebAPI 项目

新的 ASP.NET WebAPI 后端将会在后续部分中创建,该后端有三个主要用途:

  1. 对客户端进行身份验证:稍后将会添加消息处理程序,以便对客户端请求进行身份验证,并将用户与请求相关联。
  2. 客户端通知注册:稍后,你将要添加一个控制器来处理新的注册,使客户端设备能够接收通知。经过身份验证的用户名将作为标记自动添加到注册。
  3. 将通知发送到客户端:稍后,你还要添加一个控制器,以便用户对与标记关联的设备和客户端触发安全推送。

以下步骤说明了如何创建新的 ASP.NET WebAPI 后端:

Note

重要提示:在开始本教程之前,请确保已安装最新版本的 NuGet 程序包管理器。若要进行检查,请启动 Visual Studio。从“工具”菜单,单击“扩展和更新”。搜索“适用于 Visual Studio 2013 的 NuGet 程序包管理器”,并且确保具有版本 2.8.50313.46 或更高版本。否则,请卸载并重新安装 NuGet 程序包管理器。

Note

请确保已安装 Visual Studio Azure SDK 以便进行 Web 应用部署。

  1. 启动 Visual Studio 或 Visual Studio Express。单击“服务器资源管理器”并登录到你的 Azure 帐户。Visual Studio 需要你登录才能在你的帐户中创建 Web 应用资源。
  2. 在 Visual Studio 中,依次单击“文件”、“新建”和“项目”,依次展开“模板”和“Visual C#”,然后依次单击“Web”和“ASP.NET Web 应用程序”,键入名称 AppBackend,然后单击“确定”。

  3. 在“新建 ASP.NET 项目”对话框中,单击“Web API”,然后单击“确定”。

  4. 在“配置 Azure Web 应用”对话框中,选择订阅和你已创建的 App Service 计划。你也可以选择“创建新的 App Service 计划”,并通过对话框创建一个计划。在本教程中,你不需要使用数据库。选择 App Service 计划后,单击“确定”以创建项目。

在 WebAPI 后端上对客户端进行身份验证

在本部分,你将为新的后端创建名为 AuthenticationTestHandler 的新消息处理程序类。此类衍生自 DelegatingHandler 并已添加为消息处理程序,以便处理传入后端的所有请求。

  1. 在“解决方案资源管理器”中,右键单击“AppBackend”项目,单击“添加”,然后单击“类”。将新类命名为 AuthenticationTestHandler.cs,然后单击“添加”以生成该类。通过此类可简单地使用基本身份验证 对用户进行身份验证。请注意,您的应用可以使用所有身份验证方案。

  2. 在 AuthenticationTestHandler.cs 中,添加以下 using 语句:

    using System.Net.Http;
    using System.Threading;
    using System.Security.Principal;
    using System.Net;
    using System.Web;
    
  3. 在 AuthenticationTestHandler.cs 中,将 AuthenticationTestHandler 类定义替换为以下代码。

    当以下三个条件都成立时,此处理程序将授权给请求:

    • 请求包含 Authorization 标头。
    • 请求使用基本 身份验证。
    • 用户名字符串和密码字符串是相同的字符串。

      否则,将会拒绝该请求。这不是真正的身份验证和授权方法。它只是本教程中一个非常简单的示例。

      如果请求消息已经过 AuthenticationTestHandler 的身份验证和授权,则基本身份验证用户将附加到 HttpContext 上的当前请求。然后,另一个控制器 (RegisterController) 会使用 HttpContext 中的用户信息,将标记添加到通知注册请求。

      public class AuthenticationTestHandler : DelegatingHandler
      {
        protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var authorizationHeader = request.Headers.GetValues("Authorization").First();
      
            if (authorizationHeader != null && authorizationHeader
                .StartsWith("Basic ", StringComparison.InvariantCultureIgnoreCase))
            {
                string authorizationUserAndPwdBase64 =
                    authorizationHeader.Substring("Basic ".Length);
                string authorizationUserAndPwd = Encoding.Default
                    .GetString(Convert.FromBase64String(authorizationUserAndPwdBase64));
                string user = authorizationUserAndPwd.Split(':')[0];
                string password = authorizationUserAndPwd.Split(':')[1];
      
                if (verifyUserAndPwd(user, password))
                {
                    // Attach the new principal object to the current HttpContext object
                    HttpContext.Current.User =
                        new GenericPrincipal(new GenericIdentity(user), new string[0]);
                    System.Threading.Thread.CurrentPrincipal =
                        System.Web.HttpContext.Current.User;
                }
                else return Unauthorized();
            }
            else return Unauthorized();
      
            return base.SendAsync(request, cancellationToken);
        }
      
        private bool verifyUserAndPwd(string user, string password)
        {
            // This is not a real authentication scheme.
            return user == password;
        }
      
        private Task<HttpResponseMessage> Unauthorized()
        {
            var response = new HttpResponseMessage(HttpStatusCode.Forbidden);
            var tsc = new TaskCompletionSource<HttpResponseMessage>();
            tsc.SetResult(response);
            return tsc.Task;
        }
      }
      
      Note

      安全说明AuthenticationTestHandler 类不提供真正的身份验证。它仅用于模拟基本身份验证并且是不安全的。您必须在生产应用程序和服务中实现安全的身份验证机制。

  4. App_Start/WebApiConfig.cs 类中 Register 方法的末尾添加以下代码,以注册消息处理程序:

    config.MessageHandlers.Add(new AuthenticationTestHandler());
    
  5. 保存所做更改。

使用 WebAPI 后端注册通知

在本部分,我们要将新的控制器添加到 WebAPI 后端来处理请求,以使用通知中心的客户端库为用户和设备注册通知。控制器将为已由 AuthenticationTestHandler 验证并已附加到 HttpContext 的用户添加用户标记。该标记采用以下字符串格式:"username:<actual username>"

  1. 在“解决方案资源管理器”中,右键单击“AppBackend”项目,然后单击“管理 NuGet 程序包”。

  2. 在左侧,单击“联机”,然后在“搜索”框中搜索 Microsoft.Azure.NotificationHubs

  3. 在结果列表中,单击“Azure 通知中心”,然后单击“安装”。完成安装后,关闭“NuGet 程序包管理器”窗口。

    这将使用 Microsoft.Azure.Notification Hubs NuGet 程序包添加对 Azure 通知中心 SDK 的引用。

  4. 现在,我们要创建新的类文件,用于表示所要发送的不同安全通知。在完整的实现中,这些通知存储在某个数据库中。为简单起见,本教程将这些通知存储在内存中。在“解决方案资源管理器”中,右键单击“Models”文件夹,单击“添加”,然后单击“类”。将新类命名为 Notifications.cs,然后单击“添加”以生成该类。

  5. 在 Notifications.cs 中,在文件顶部添加以下 using 语句:

    using Microsoft.Azure.NotificationHubs;
    
  6. Notifications 类定义替换为以下内容并确保将两个占位符替换为通知中心的连接字符串(具有完全访问权限)和中心名称(可在 Azure 经典管理门户中找到):

    public class Notifications
    {
        public static Notifications Instance = new Notifications();
    
        public NotificationHubClient Hub { get; set; }
    
        private Notifications() {
            Hub = NotificationHubClient.CreateClientFromConnectionString("<your hub's DefaultFullSharedAccessSignature>", 
                                                                         "<hub name>");
        }
    }
    
  7. 接下来,我们将创建一个名为 RegisterController 的新控制器。在“解决方案资源管理器”中,右键单击“Controllers”文件夹,然后依次单击“添加”和“控制器”。单击“Web API 2 Controller -- Empty”项目,然后单击“添加”。将新类命名为 RegisterController,然后再次单击“添加”以生成该控制器。

  8. 在 RegisterController.cs 中,添加以下 using 语句:

    using Microsoft.Azure.NotificationHubs;
    using Microsoft.Azure.NotificationHubs.Messaging;
    using AppBackend.Models;
    using System.Threading.Tasks;
    using System.Web;
    
  9. RegisterController 类定义中添加以下代码:请注意,在此代码中,我们将为已附加到 HttpContext 的用户添加用户标记。添加的消息筛选器 AuthenticationTestHandler 将对该用户进行身份验证并将其附加到 HttpContext。还可以通过添加可选复选框来验证用户是否有权注册以获取请求标记。

    private NotificationHubClient hub;
    
    public RegisterController()
    {
        hub = Notifications.Instance.Hub;
    }
    
    public class DeviceRegistration
    {
        public string Platform { get; set; }
        public string Handle { get; set; }
        public string[] Tags { get; set; }
    }
    
    // POST api/register
    // This creates a registration id
    public async Task<string> Post(string handle = null)
    {
        string newRegistrationId = null;
    
        // make sure there are no existing registrations for this push handle (used for iOS and Android)
        if (handle != null)
        {
            var registrations = await hub.GetRegistrationsByChannelAsync(handle, 100);
    
            foreach (RegistrationDescription registration in registrations)
            {
                if (newRegistrationId == null)
                {
                    newRegistrationId = registration.RegistrationId;
                }
                else
                {
                    await hub.DeleteRegistrationAsync(registration);
                }
            }
        }
    
        if (newRegistrationId == null) 
            newRegistrationId = await hub.CreateRegistrationIdAsync();
    
        return newRegistrationId;
    }
    
    // PUT api/register/5
    // This creates or updates a registration (with provided channelURI) at the specified id
    public async Task<HttpResponseMessage> Put(string id, DeviceRegistration deviceUpdate)
    {
        RegistrationDescription registration = null;
        switch (deviceUpdate.Platform)
        {
            case "mpns":
                registration = new MpnsRegistrationDescription(deviceUpdate.Handle);
                break;
            case "wns":
                registration = new WindowsRegistrationDescription(deviceUpdate.Handle);
                break;
            case "apns":
                registration = new AppleRegistrationDescription(deviceUpdate.Handle);
                break;
            case "gcm":
                registration = new GcmRegistrationDescription(deviceUpdate.Handle);
                break;
            default:
                throw new HttpResponseException(HttpStatusCode.BadRequest);
        }
    
        registration.RegistrationId = id;
        var username = HttpContext.Current.User.Identity.Name;
    
        // add check if user is allowed to add these tags
        registration.Tags = new HashSet<string>(deviceUpdate.Tags);
        registration.Tags.Add("username:" + username);
    
        try
        {
            await hub.CreateOrUpdateRegistrationAsync(registration);
        }
        catch (MessagingException e)
        {
            ReturnGoneIfHubResponseIsGone(e);
        }
    
        return Request.CreateResponse(HttpStatusCode.OK);
    }
    
    // DELETE api/register/5
    public async Task<HttpResponseMessage> Delete(string id)
    {
        await hub.DeleteRegistrationAsync(id);
        return Request.CreateResponse(HttpStatusCode.OK);
    }
    
    private static void ReturnGoneIfHubResponseIsGone(MessagingException e)
    {
        var webex = e.InnerException as WebException;
        if (webex.Status == WebExceptionStatus.ProtocolError)
        {
            var response = (HttpWebResponse)webex.Response;
            if (response.StatusCode == HttpStatusCode.Gone)
                throw new HttpRequestException(HttpStatusCode.Gone.ToString());
        }
    }
    
  10. 保存所做更改。

从 WebAPI 后端发送通知

在本部分,你将添加新的控制器,以便客户端设备使用 ASP.NET WebAPI 后端中的 Azure 通知中心服务管理库根据用户名标记发送通知。

  1. 创建另一个名为 NotificationsController 的新控制器。以你在上一节中创建 RegisterController 的相同方式来创建新控制器。

  2. 在 NotificationsController.cs 中,添加以下 using 语句:

    using AppBackend.Models;
    using System.Threading.Tasks;
    using System.Web;
    
  3. NotificationsController 类中添加以下方法。

    此代码将会根据平台通知服务 (PNS) pns 参数发送相应类型的通知。to_tag 的值用于设置消息中的 username 标记。此标记必须与活动的通知中心注册的用户名标记相匹配。将从 POST 请求正文提取通知消息,并根据目标 PNS 将其格式化。

    根据受支持设备用来接收通知的平台通知服务 (PNS),支持使用不同的格式接收不同的通知。例如,在 Windows 设备上,可以使用其他 PNS 不能直接支持的 toast 通知和 WNS。因此,后端需要将通知格式化为你打算使用的设备 PNS 所支持的通知。然后,对 NotificationHubClient 类使用相应的 send API

    public async Task<HttpResponseMessage> Post(string pns, [FromBody]string message, string to_tag)
    {
        var user = HttpContext.Current.User.Identity.Name;
        string[] userTag = new string[2];
        userTag[0] = "username:" + to_tag;
        userTag[1] = "from:" + user;
    
        Microsoft.Azure.NotificationHubs.NotificationOutcome outcome = null;
        HttpStatusCode ret = HttpStatusCode.InternalServerError;
    
        switch (pns.ToLower())
        {
            case "wns":
                // Windows 8.1 / Windows Phone 8.1
                var toast = @"<toast><visual><binding template=""ToastText01""><text id=""1"">" + 
                            "From " + user + ": " + message + "</text></binding></visual></toast>";
                outcome = await Notifications.Instance.Hub.SendWindowsNativeNotificationAsync(toast, userTag);
                break;
            case "apns":
                // iOS
                var alert = "{"aps":{"alert":"" + "From " + user + ": " + message + ""}}";
                outcome = await Notifications.Instance.Hub.SendAppleNativeNotificationAsync(alert, userTag);
                break;
            case "gcm":
                // Android
                var notif = "{ "data" : {"message":"" + "From " + user + ": " + message + ""}}";
                outcome = await Notifications.Instance.Hub.SendGcmNativeNotificationAsync(notif, userTag);
                break;
        }
    
        if (outcome != null)
        {
            if (!((outcome.State == Microsoft.Azure.NotificationHubs.NotificationOutcomeState.Abandoned) ||
                (outcome.State == Microsoft.Azure.NotificationHubs.NotificationOutcomeState.Unknown)))
            {
                ret = HttpStatusCode.OK;
            }
        }
    
        return Request.CreateResponse(ret);
    }
    
  4. F5 运行应用程序并确保到目前为止操作的准确性。该应用应启动 Web 浏览器,然后显示 ASP.NET 主页。

发布新的 WebAPI 后端

  1. 现在,我们将此应用部署到 Azure Web 应用,以便可以从任意设备访问它。右键单击 AppBackend 项目,然后选择“发布”。

  2. 选择“Azure Web Apps”作为发布目标。

  3. 使用你的 Azure 帐户登录,然后选择一个现有的或新的 Web 应用。

  4. 记下“连接”选项卡中的“目标 URL”属性。在本教程后面的部分中,我们将此 URL 称为后端终结点。单击“发布”。

修改 iOS 应用

  1. 打开你在通知中心入门 (iOS) 教程中创建的“单页视图”应用。

    Note

    在本节中我们假定你的项目已配置了空的组织名称。如果未配置,你将需要在所有类名前面追加组织名称。

  2. 在 Main.storyboard 中添加对象库中的组件,如下面的屏幕截图中所示。

    • 用户名:包含占位符文本“输入用户名”的 UITextField,直接位于发送结果标签的下面并受左边距和右边距约束。
    • 密码:包含占位符文本“输入密码”的 UITextField,直接位于用户名文本字段的下面并受左边距和右边距约束。选中属性检查器中“返回密钥”下的“安全文本输入”选项。
    • 登录:在密码文本字段的直接下方标记的 UIButton 并取消选中属性检查器中“控件内容”下的“已启用”选项
    • WNS:标签和开关,用于已在中心设置 Windows 通知服务时,启用将通知发送到 Windows 通知服务。请参阅 Windows 入门教程。
    • GCM:标签和开关,用于已在中心设置 Google Cloud Messaging 时,启用将通知发送到 Google Cloud Messaging。
    • APNS:标签和开关,用于启用将通知发送到 Apple 平台通知服务。
    • 收件人用户名:包含占位符文本“收件人用户名标记”的 UITextField,直接位于 GCM 标签的下面并受左边距和右边距约束。

      某些组件已在通知中心入 (iOS) 教程中添加。

  3. Ctrl 的同时从视图中的组件拖至 ViewController.h 并添加这些新插座。

    @property (weak, nonatomic) IBOutlet UITextField *UsernameField;
    @property (weak, nonatomic) IBOutlet UITextField *PasswordField;
    @property (weak, nonatomic) IBOutlet UITextField *RecipientField;
    @property (weak, nonatomic) IBOutlet UITextField *NotificationField;
    
    // Used to enable the buttons on the UI
    @property (weak, nonatomic) IBOutlet UIButton *LogInButton;
    @property (weak, nonatomic) IBOutlet UIButton *SendNotificationButton;
    
    // Used to enabled sending notifications across platforms
    @property (weak, nonatomic) IBOutlet UISwitch *WNSSwitch;
    @property (weak, nonatomic) IBOutlet UISwitch *GCMSwitch;
    @property (weak, nonatomic) IBOutlet UISwitch *APNSSwitch;
    
    - (IBAction)LogInAction:(id)sender;
    
  4. 在 ViewController.h 中,在 import 语句的正下方添加以下 #define。将 < 输入你的后端终结点> 占位符替换为在上一节中用于部署应用后端的目标 URL。例如,http://you_backend.chinacloudsites.cn

    #define BACKEND_ENDPOINT @"<Enter Your Backend Endpoint>"
    
  5. 在你的项目中,创建一个名为 RegisterClient 的新 Cocoa Touch 类,以便与你创建的 ASP.NET 后端交互。创建继承自 NSObject 的类。然后在 RegisterClient.h 中添加以下代码。

    @interface RegisterClient : NSObject
    
    @property (strong, nonatomic) NSString* authenticationHeader;
    
    -(void) registerWithDeviceToken:(NSData*)token tags:(NSSet*)tags
        andCompletion:(void(^)(NSError*))completion;
    
    -(instancetype) initWithEndpoint:(NSString*)Endpoint;
    
    @end
    
  6. 在 RegisterClient.m 中,更新 @interface 节:

    @interface RegisterClient ()
    
    @property (strong, nonatomic) NSURLSession* session;
    @property (strong, nonatomic) NSURLSession* endpoint;
    
    -(void) tryToRegisterWithDeviceToken:(NSData*)token tags:(NSSet*)tags retry:(BOOL)retry
                andCompletion:(void(^)(NSError*))completion;
    -(void) retrieveOrRequestRegistrationIdWithDeviceToken:(NSString*)token
                completion:(void(^)(NSString*, NSError*))completion;
    -(void) upsertRegistrationWithRegistrationId:(NSString*)registrationId deviceToken:(NSString*)token
                tags:(NSSet*)tags andCompletion:(void(^)(NSURLResponse*, NSError*))completion;
    
    @end
    
  7. 将 RegisterClient.m 中的 @implementation 节替换为以下代码。

    @implementation RegisterClient
    
    // Globals used by RegisterClient
    NSString *const RegistrationIdLocalStorageKey = @"RegistrationId";
    
    -(instancetype) initWithEndpoint:(NSString*)Endpoint
    {
        self = [super init];
        if (self) {
            NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
            _session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
            _endpoint = Endpoint;
        }
        return self;
    }
    
    -(void) registerWithDeviceToken:(NSData*)token tags:(NSSet*)tags
                andCompletion:(void(^)(NSError*))completion
    {
        [self tryToRegisterWithDeviceToken:token tags:tags retry:YES andCompletion:completion];
    }
    
    -(void) tryToRegisterWithDeviceToken:(NSData*)token tags:(NSSet*)tags retry:(BOOL)retry
                andCompletion:(void(^)(NSError*))completion
    {
        NSSet* tagsSet = tags?tags:[[NSSet alloc] init];
    
        NSString *deviceTokenString = [[token description]
            stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
        deviceTokenString = [[deviceTokenString stringByReplacingOccurrencesOfString:@" " withString:@""]
                                uppercaseString];
    
        [self retrieveOrRequestRegistrationIdWithDeviceToken: deviceTokenString
            completion:^(NSString* registrationId, NSError *error) {
            NSLog(@"regId: %@", registrationId);
            if (error) {
                completion(error);
                return;
            }
    
            [self upsertRegistrationWithRegistrationId:registrationId deviceToken:deviceTokenString
                tags:tagsSet andCompletion:^(NSURLResponse * response, NSError *error) {
                if (error) {
                    completion(error);
                    return;
                }
    
                NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
                if (httpResponse.statusCode == 200) {
                    completion(nil);
                } else if (httpResponse.statusCode == 410 && retry) {
                    [self tryToRegisterWithDeviceToken:token tags:tags retry:NO andCompletion:completion];
                } else {
                    NSLog(@"Registration error with response status: %ld", (long)httpResponse.statusCode);
    
                    completion([NSError errorWithDomain:@"Registration" code:httpResponse.statusCode
                                userInfo:nil]);
                }
    
            }];
        }];
    }
    
    -(void) upsertRegistrationWithRegistrationId:(NSString*)registrationId deviceToken:(NSData*)token
                tags:(NSSet*)tags andCompletion:(void(^)(NSURLResponse*, NSError*))completion
    {
        NSDictionary* deviceRegistration = @{@"Platform" : @"apns", @"Handle": token,
                                                @"Tags": [tags allObjects]};
        NSData* jsonData = [NSJSONSerialization dataWithJSONObject:deviceRegistration
                            options:NSJSONWritingPrettyPrinted error:nil];
    
        NSLog(@"JSON registration: %@", [[NSString alloc] initWithData:jsonData
                                            encoding:NSUTF8StringEncoding]);
    
        NSString* endpoint = [NSString stringWithFormat:@"%@/api/register/%@", _endpoint,
                                registrationId];
        NSURL* requestURL = [NSURL URLWithString:endpoint];
        NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:requestURL];
        [request setHTTPMethod:@"PUT"];
        [request setHTTPBody:jsonData];
        NSString* authorizationHeaderValue = [NSString stringWithFormat:@"Basic %@",
                                                self.authenticationHeader];
        [request setValue:authorizationHeaderValue forHTTPHeaderField:@"Authorization"];
        [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    
        NSURLSessionDataTask* dataTask = [self.session dataTaskWithRequest:request
            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
        {
            if (!error)
            {
                completion(response, error);
            }
            else
            {
                NSLog(@"Error request: %@", error);
                completion(nil, error);
            }
        }];
        [dataTask resume];
    }
    
    -(void) retrieveOrRequestRegistrationIdWithDeviceToken:(NSString*)token
                completion:(void(^)(NSString*, NSError*))completion
    {
        NSString* registrationId = [[NSUserDefaults standardUserDefaults]
                                    objectForKey:RegistrationIdLocalStorageKey];
    
        if (registrationId)
        {
            completion(registrationId, nil);
            return;
        }
    
        // request new one & save
        NSURL* requestURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/api/register?handle=%@",
                                _endpoint, token]];
        NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:requestURL];
        [request setHTTPMethod:@"POST"];
        NSString* authorizationHeaderValue = [NSString stringWithFormat:@"Basic %@",
                                                self.authenticationHeader];
        [request setValue:authorizationHeaderValue forHTTPHeaderField:@"Authorization"];
    
        NSURLSessionDataTask* dataTask = [self.session dataTaskWithRequest:request
            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
        {
            NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*) response;
            if (!error && httpResponse.statusCode == 200)
            {
                NSString* registrationId = [[NSString alloc] initWithData:data
                    encoding:NSUTF8StringEncoding];
    
                // remove quotes
                registrationId = [registrationId substringWithRange:NSMakeRange(1,
                                    [registrationId length]-2)];
    
                [[NSUserDefaults standardUserDefaults] setObject:registrationId
                    forKey:RegistrationIdLocalStorageKey];
                [[NSUserDefaults standardUserDefaults] synchronize];
    
                completion(registrationId, nil);
            }
            else
            {
                NSLog(@"Error status: %ld, request: %@", (long)httpResponse.statusCode, error);
                if (error)
                    completion(nil, error);
                else {
                    completion(nil, [NSError errorWithDomain:@"Registration" code:httpResponse.statusCode
                                userInfo:nil]);
                }
            }
        }];
        [dataTask resume];
    }
    
    @end
    

    上面的代码使用 NSURLSession 对应用后端执行 REST 调用并使用 NSUserDefaults 在本地存储通知中心返回的 registrationId,实现了指南文章从应用后端注册中所述的逻辑。

    请注意,此类需要设置其属性 authorizationHeader,才能正常工作。登录后,由 ViewController 类设置此属性。

  8. 在 ViewController.h 中,为 RegisterClient.h 添加 #import 语句。然后,在 @interface 中添加设备令牌的声明和对 RegisterClient 实例的引用:

    #import "RegisterClient.h"
    
    @property (strong, nonatomic) NSData* deviceToken;
    @property (strong, nonatomic) RegisterClient* registerClient;
    
  9. 在 ViewController.m 的 @interface 中添加私有方法声明:

    @interface ViewController () <UITextFieldDelegate, NSURLConnectionDataDelegate, NSXMLParserDelegate>
    
    // create the Authorization header to perform Basic authentication with your app back-end
    -(void) createAndSetAuthenticationHeaderWithUsername:(NSString*)username
                    AndPassword:(NSString*)password;
    
    @end
    
    Note

    下面的代码段不是安全的身份验证方案,你应将 createAndSetAuthenticationHeaderWithUsername:AndPassword: 的实现替换为你的特定身份验证机制,该机制将生成要供注册客户端类(例如,OAuth、Active Directory)使用的身份验证令牌。

  10. 然后在 ViewController.m 的 @implementation 节中添加以下代码,以添加用于设置设备令牌的实现和身份验证标头。

    -(void) setDeviceToken: (NSData*) deviceToken
    {
        _deviceToken = deviceToken;
        self.LogInButton.enabled = YES;
    }
    
    -(void) createAndSetAuthenticationHeaderWithUsername:(NSString*)username
                    AndPassword:(NSString*)password;
    {
        NSString* headerValue = [NSString stringWithFormat:@"%@:%@", username, password];
    
        NSData* encodedData = [[headerValue dataUsingEncoding:NSUTF8StringEncoding] base64EncodedDataWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn];
    
        self.registerClient.authenticationHeader = [[NSString alloc] initWithData:encodedData
                                                    encoding:NSUTF8StringEncoding];
    }
    
    -(BOOL)textFieldShouldReturn:(UITextField *)textField
    {
        [textField resignFirstResponder];
        return YES;
    }
    

    请注意设置设备令牌时如何启用登录按钮。这是因为在登录操作过程中,视图控制器将使用应用后端注册推送通知。因此,我们不希望在正确设置设备令牌前可以访问登录操作。可以只要登录在推送注册之前发生,就将前者与后者解耦。

  11. 在 ViewController.m 中,使用以下代码段实现“登录”按钮的操作方法以及使用 ASP.NET 后端发送通知消息的方法。

    - (IBAction)LogInAction:(id)sender {
        // create authentication header and set it in register client
        NSString* username = self.UsernameField.text;
        NSString* password = self.PasswordField.text;
    
        [self createAndSetAuthenticationHeaderWithUsername:username AndPassword:password];
    
        __weak ViewController* selfie = self;
        [self.registerClient registerWithDeviceToken:self.deviceToken tags:nil
            andCompletion:^(NSError* error) {
            if (!error) {
                dispatch_async(dispatch_get_main_queue(),
                ^{
                    selfie.SendNotificationButton.enabled = YES;
                    [self MessageBox:@"Success" message:@"Registered successfully!"];
                });
            }
        }];
    }
    
    - (void)SendNotificationASPNETBackend:(NSString*)pns UsernameTag:(NSString*)usernameTag
                Message:(NSString*)message
    {
        NSURLSession* session = [NSURLSession
            sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:nil
            delegateQueue:nil];
    
        // Pass the pns and username tag as parameters with the REST URL to the ASP.NET backend
        NSURL* requestURL = [NSURL URLWithString:[NSString
            stringWithFormat:@"%@/api/notifications?pns=%@&to_tag=%@", BACKEND_ENDPOINT, pns,
            usernameTag]];
    
        NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:requestURL];
        [request setHTTPMethod:@"POST"];
    
        // Get the mock authenticationheader from the register client
        NSString* authorizationHeaderValue = [NSString stringWithFormat:@"Basic %@",
            self.registerClient.authenticationHeader];
        [request setValue:authorizationHeaderValue forHTTPHeaderField:@"Authorization"];
    
        //Add the notification message body
        [request setValue:@"application/json;charset=utf-8" forHTTPHeaderField:@"Content-Type"];
        [request setHTTPBody:[message dataUsingEncoding:NSUTF8StringEncoding]];
    
        // Execute the send notification REST API on the ASP.NET Backend
        NSURLSessionDataTask* dataTask = [session dataTaskWithRequest:request
            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
        {
            NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*) response;
            if (error || httpResponse.statusCode != 200)
            {
                NSString* status = [NSString stringWithFormat:@"Error Status for %@: %d\nError: %@\n",
                                    pns, httpResponse.statusCode, error];
                dispatch_async(dispatch_get_main_queue(),
                ^{
                    // Append text because all 3 PNS calls may also have information to view
                    [self.sendResults setText:[self.sendResults.text stringByAppendingString:status]];
                });
                NSLog(status);
            }
    
            if (data != NULL)
            {
                xmlParser = [[NSXMLParser alloc] initWithData:data];
                [xmlParser setDelegate:self];
                [xmlParser parse];
            }
        }];
        [dataTask resume];
    }
    
  12. 更新“发送通知”按钮的操作以使用 ASP.NET 后端,发送开关启用的任何 PNS。

    - (IBAction)SendNotificationMessage:(id)sender
    {
        //[self SendNotificationRESTAPI];
        [self SendToEnabledPlatforms];
    }
    
    -(void)SendToEnabledPlatforms
    {
        NSString* json = [NSString stringWithFormat:@""%@"",self.notificationMessage.text];
    
        [self.sendResults setText:@""];
    
        if ([self.WNSSwitch isOn])
            [self SendNotificationASPNETBackend:@"wns" UsernameTag:self.RecipientField.text Message:json];
    
        if ([self.GCMSwitch isOn])
            [self SendNotificationASPNETBackend:@"gcm" UsernameTag:self.RecipientField.text Message:json];
    
        if ([self.APNSSwitch isOn])
            [self SendNotificationASPNETBackend:@"apns" UsernameTag:self.RecipientField.text Message:json];
    }
    
  13. 在函数 ViewDidLoad 中,添加以下内容来实例化 RegisterClient 实例并设置文本字段的委托。

    self.UsernameField.delegate = self;
    self.PasswordField.delegate = self;
    self.RecipientField.delegate = self;
    self.registerClient = [[RegisterClient alloc] initWithEndpoint:BACKEND_ENDPOINT];
    
  14. 现在,在 AppDelegate.m 中,删除方法 application:didRegisterForPushNotificationWithDeviceToken: 的所有内容并将其替换为以下内容,以确保视图控制器包含从 APN 中检索到的最新设备令牌:

    // Add import to the top of the file
    #import "ViewController.h"
    
    - (void)application:(UIApplication *)application
                didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
    {
        ViewController* rvc = (ViewController*) self.window.rootViewController;
        rvc.deviceToken = deviceToken;
    }
    
  15. 最后,在 AppDelegate.m 中,确保你使用了以下方法:

    - (void)application:(UIApplication *)application didReceiveRemoteNotification: (NSDictionary *)userInfo {
        NSLog(@"%@", userInfo);
        [self MessageBox:@"Notification" message:[[userInfo objectForKey:@"aps"] valueForKey:@"alert"]];
    }
    

测试应用程序

  1. 在 XCode 中,在物理 iOS 设备上运行此应用(推送通知将无法在模拟器中正常工作)。

  2. 在 iOS 应用 UI 中,输入用户名和密码。这些信息可以是任意字符串,但必须是相同的字符串值。然后,单击“登录”。

  3. 你应看到弹出窗口通知你注册成功。单击“确定”

  4. 在*“收件人用户名标记”文本字段中,输入用于从另一台设备注册的用户名标记。

  5. 输入通知消息,然后单击“发送通知”。只有使用该用户名标记注册的设备才会收到通知消息。该消息将只发送给那些用户。