iOS APNS推送:基于Token和基于证书的推送

2,155 阅读15分钟

APNs(苹果推送通知服务)设置指南

APNs 是 Apple 的推送通知服务,它使第三方应用程序开发人员能够向安装在 Apple 设备上的应用程序发送通知数据。

此文主要介绍如何让 Apple 设备上的应用程序支持 APNs 以及如何在开发者中心设置相关的配置。

启用推送功能

应用要启用推送功能,分为两步:

  1. 开发项目开启推送的权限。
  2. 在苹果开发者网站为对应的 App ID 启用推送功能。

开启开发项目的推送权限

要在应用程序中添加所需的权限,请在 Xcode 项目中启用推送通知功能。

打开 Xcode 项目,在 Project > Target > Capabilities 页面中点击红框中的加号按钮,然后选择并添加 Push Notifications,添加后的结果如图中黄框所示:

image.png

启用苹果 App ID 的推送功能

登录苹果开发者网站,进入 Certificates, Identifiers & Profiles 页面,点击侧边栏中的 Identifiers,然后在列表中找到项目对应的 App ID(即 Xcode 项目中的 Bundle Identifier),点击并进入配置编辑页面,然后选择 Push Notifications 旁边的复选框,最后点击右上的 Save(保存)按钮,结果如下图所示:

image.png

选择推送方式

苹果提供了两种方式来发送通知,这两种方式各有优点和缺点,可以根据需要选择其中一种推送方式。

  1. 基于Token的推送方式(推荐)。
  • 理论上它比基于证书的方式更快。
  • 支持多个服务应用使用同一个Key。
  • 支持用同一个Key给苹果开发者账号下的多个应用推送通知。
  • 支持用同一个Key给苹果开发者账号下的测试、上架生产应用推送通知。
  • 生产的Key不再有过期时间,无需像证书方式那样需要定期重新生成证书。
  1. 基于证书的推送方式。
  • 证书和苹果的App ID绑定,一个证书只能向其绑定的苹果应用推送通知。
  • APNS有开发、生产两个环境,可能需要为不同环境下的苹果应用配置对应的证书。
  • 证书有过期时间,需要定期重新生成并配置。

总的来说,基于Token的推送方式在配置步骤、易用性以及功能性上,都要基于证书的推送方式,因此我们推荐使用基于Token的推送方式。(如果同时配置了两种方式,那么基于Token的方式优先级高于证书方式)

基于Token的推送方式(推荐)

优点:

APNs(苹果推送通知服务)设置指南

APNs 是 Apple 的推送通知服务,它使第三方应用程序开发人员能够向安装在 Apple 设备上的应用程序发送通知数据。

此文主要介绍如何让 Apple 设备上的应用程序支持 APNs 以及如何在开发者中心设置相关的配置。

启用推送功能

应用要启用推送功能,分为两步:

  1. 开发项目开启推送的权限。
  2. 在苹果开发者网站为对应的 App ID 启用推送功能。

开启开发项目的推送权限

要在应用程序中添加所需的权限,请在 Xcode 项目中启用推送通知功能。

打开 Xcode 项目,在 Project > Target > Capabilities 页面中点击红框中的加号按钮,然后选择并添加 Push Notifications,添加后的结果如图中黄框所示:

image.png

启用苹果 App ID 的推送功能

登录苹果开发者网站,进入 Certificates, Identifiers & Profiles 页面,点击侧边栏中的 Identifiers,然后在列表中找到项目对应的 App ID(即 Xcode 项目中的 Bundle Identifier),点击并进入配置编辑页面,然后选择 Push Notifications 旁边的复选框,最后点击右上的 Save(保存)按钮,结果如下图所示:

image.png

选择推送方式

苹果提供了两种方式来发送通知,这两种方式各有优点和缺点,可以根据需要选择其中一种推送方式。

  1. 基于Token的推送方式(推荐)。
  • 理论上它比基于证书的方式更快。
  • 支持多个服务应用使用同一个Key。
  • 支持用同一个Key给苹果开发者账号下的多个应用推送通知。
  • 支持用同一个Key给苹果开发者账号下的测试、上架生产应用推送通知。
  • 生产的Key不再有过期时间,无需像证书方式那样需要定期重新生成证书。
  1. 基于证书的推送方式。
  • 证书和苹果的App ID绑定,一个证书只能向其绑定的苹果应用推送通知。
  • APNS有开发、生产两个环境,可能需要为不同环境下的苹果应用配置对应的证书。
  • 证书有过期时间,需要定期重新生成并配置。

总的来说,基于Token的推送方式在配置步骤、易用性以及功能性上,都要基于证书的推送方式,因此我们推荐使用基于Token的推送方式。(如果同时配置了两种方式,那么基于Token的方式优先级高于证书方式)

基于Token的推送方式(推荐)

优点:
  • 更安全:基于Token的推送采用JWT(JSON Web Token)认证,相比于证书,Token的安全性更高,因为其加密和签名机制能够有效的防止伪造。
  • 长效性:Token的有效期通常可以长达一小时,在过期前自动续期,不需要频繁更新,这样省去了证书定期更新的麻烦。
  • 方便管理和分发: 通过Token,可以在一个苹果开发者账号下支持多个应用或者服务使用同一个Key认证,不需要为每个APP生成单独的证书。
  • 支持多设备部署: Token可以通过配置支持不同的服务器环境(如开发、生产),方便在多台服务器之间部署,提升可扩展性。
缺点:
  • 配置稍微复杂:相较于证书,Token的生成和配置稍显复杂,需要在开发者账号中生成密钥和Token信息。
  • 额外开发工作:基于Token的推送需要开发者在代码中实现JWT的生成和管理,尤其在服务端推送时,需要额外实现Token生成和更新逻辑。

iOS 流程简介

image.png

第一步:

CSR文件:首先我们要通过证书助手生成一个Certificate Signing Request(也就是CSR)的请求文件。如果你已经有证书了,跳过该步骤 image.png

image.png 继续之后选择保存位置,点击保存

这时该位置上会有一个CertificateSigningRequest.certSigningRequest的请求文件,也就是我们说的CSR文件。

第二步:

生成带有Push Notifications功能的AppID

image.png

image.png

image.png 左边边框取个名称,最好是取个能够辨别自己app的名称,不能输入特殊符号

右边是输入自己的bundle ID

image.png 这里要把Push Notifications勾选上,应用才能带有推送能力。

全部设定好之后继续,保存,生成了一个新的APP ID

image.png 生成完了之后,点击该AppID 给该AppID的Push Notifications配置CSR:

image.png 可以看到配置部分有两个:

 Development SSL Certificate:开发推送证书配置,开发环境可以收到推送。

 Production SSL Certificate:生产推送证书配置,线上环境可以收到推送。 

点击Create Certificate, 添加如我们刚才的生成好的证书 CertificateSigningRequest.certSigningRequest

image.png

添加好之后下载下来 会生成一个aps.cer的文件。双击之后会在钥匙串中看到这个证书

image.png 至此,ios端需要的东西都已经准备结束。 我们ios端现在不需要给后台p12证书 也无需生成pem文件。(2021年之后就不需要生成pem文件了)

第三步:

接下来介绍一下后台的工作,后台需要配置Key ID和.p8密钥文件

要使用基于Token的推送方式,首先需从苹果开发者网站上生成并下载密钥.p8文件(Auth Key),之后把密钥传给服务端配置。

image.png

image.png

image.png

image.png

后台需要Keys: image.png

创建好后会生成Key ID与密钥.p8文件。 需注意的是:密钥.p8文件只能下载一次,丢失就需要重新生成密钥文件。

image.png

写到这里基本差不多了 接下来是后台去配置其他token 推送内容之类的东西了。

还有我们自己去代码里面配置以及发送deviceToken给后台。还有接收到推送的代码

代码如下

在Appdelegate.m导入头文件

#import <UserNotifications/UserNotifications.h>

image.png

image.png

最后这是关于推送后台服务器那边需要配置的:developer.apple.com/documentati…

以后推送不成功!!:

1、让后台检查下是否发送成功(后台无需更改任何文件)。

2、iOS端检查下证书是否过期。

基于证书的推送方式

准备工作 在苹果后台给对应的App ID开通Push Notifications权限,生成profile(用于真机测试),生成APNs证书(用于服务端),

推送流程

image.png

image.png

deviceToken:手机的UDID + APP的BundleID (每个手机的UDID都不一样)

要使用基于证书的推送方式,需要从苹果开发者网站上为每个苹果应用生成单独的推送证书,每个苹果应用的推送证书又可分为开发环境推送证书和开发&生产环境推送证书,之后在开发者中心上传对应的推送证书,成功后即可使用该方式推送通知。

生成证书

登录苹果开发者网站,进入 Certificates, Identifiers & Profiles 页面,点击侧边栏中的 Certificates,然后点击左上方的添加按钮(+),接着按以下步骤操作:

选择对应的证书类型。一般选择 Apple Push Notification service SSL 类型的推送证书,这类证书又分为 Sandbox 和 Sandbox & Production 两种,其中 Sandbox 证书只能用于开发环境的苹果应用,而 Sandbox & Production 证书既可用于开发环境,也可用于生产环境的苹果应用。具体区别可以看选项下的描述,如下图所示: image.png 选择完证书后,点击继续,接着选择 App ID(即 Xcode 项目中的 Bundle Identifier),选完后点击继续进入下一页面,会要求上传一个 CSR 文件。

在你的 Mac 上生成一个 CSR(Certificate Signing Request)文件,生成步骤如下:

启动位于 /Applications/Utilities 的 Keychain Access(钥匙串访问)。

选择 Keychain Access > Certificate Assistant > Request a Certificate From a Certificate Authority…(钥匙串访问 > 证书助理 > 从证书颁发机构请求证书…)。

证书助理对话框中,在 User Email Address(用户电子邮件地址)栏中输入一个电子邮件地址;在 Common Name(通用名称)字段中,输入密钥的名称(例如,Gita Kumar Dev Key)。

将 CA Email Address(CA 电子邮件地址)字段留空。

选择 Saved to disk(保存到磁盘),然后点击继续。 image.png 上传你的 CSR 文件(上一步中保存到本地的 .certSigningRequest 文件)并点击继续,最后下载生成的证书。

设置证书

证书生成后,需要将下载的证书和私钥上传到开发者中心。请按以下步骤操作:

在生成 CSR 文件的 Mac 上,双击下载的证书,macOS 会将其导入到 Keychain 中并和之前创建的 CSR 的密钥归为一组,如下图所示: image.png 在 Keychain Access > login > My Certificates 中右键点击导入的证书(点击证书,不要点击对应的密钥),选择 Export(导出),将证书以 .p12 格式保存到磁盘,这时会有弹窗提示输入密码来保护导出的证书,请不要设置密码,将两个输入框留空,点击 OK;之后可能还有一个弹窗,要求输入 macOS 的 login(登录)密码以允许让证书导出,请输入密码并点击 Allow。

key转换:

需要将上面的2个.p12文件转成.pem格式:

openssl pkcs12 -clcerts -nokeys -out cert.pem -in Push_Chat_cert.p12

openssl pkcs12 -nocerts -out key.pem -in Push_Chat_cert_key.p12

如果需要对key不进行加密:

openssl rsa -in key.pem -out key.unencrypted.pem​

然后就可以 合并两个.pem文件, 这个ck.pem就是服务端需要的证书了: cat cert.pem key.unencrypted.pem > apns-dev.pem​

iOS代码

注册推送: 首先引入UserNotifications.framework,引入头文件

#import <UserNotifications/UserNotifications.h>

在didFinishLaunchingWithOptions方法中注册通知:

if (@available(iOS 10.0, *)) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        center.delegate = self;
        [center requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge completionHandler:^(BOOL granted, NSError *_Nullable error) {            if (granted) {                dispatch_async(dispatch_get_main_queue(), ^{                                   [[UIApplication sharedApplication] registerForRemoteNotifications];
                               });
            } else {
                NSLog(@"APNs注册失败");
            }
        }];
    } else if (@available(iOS 8.0, *)) {
        UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
        UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
        [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
        //register for deviceToken
        [[UIApplication sharedApplication] registerForRemoteNotifications];
    }

获取deviceToken 一般来说会将获取到的deviceToken发送给后台,用于指定设备发送通知。

//注册成功
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
    NSString *deviceTokenStr = [[[[deviceToken description]
    stringByReplacingOccurrencesOfString:@"<" withString:@""]
    stringByReplacingOccurrencesOfString:@">" withString:@""]
    stringByReplacingOccurrencesOfString:@" " withString:@""];
    NSLog(@"deviceTokenStr:\n%@",deviceTokenStr);
}
//注册失败
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{
    NSLog(@"error -- %@",error);
}

注意,如果注册失败,先检查证书是否配置正确,工程是否打开了push开关。另外,app只有在第一次安装时才会弹出系统权限提示。如果没有弹出,可以把app删除,重新build运行一次。

处理推送过来的消息,iOS10及以上:

//在前台是否展示
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler{
      // 这个方法表明应用在前台也会通知声音、角标和提示
      completionHandler(UNNotificationPresentationOptionBadge|
      UNNotificationPresentationOptionSound|
      UNNotificationPresentationOptionAlert);
}
//点击收到的消息,会触发下面方法,处理收到的数据
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{
    //  [self handlePushMessage:response.notification.request.content.userInfo];
      completionHandler();       
}

iOS8以上iOS10以下:

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary * _Nonnull)userInfo fetchCompletionHandler:(void (^ _Nonnull)(UIBackgroundFetchResult))completionHandler{
      NSLog(@"didReceiveRemoteNotification:%@",userInfo);
      /* 
       UIApplicationStateActive 应用程序处于前台
      UIApplicationStateBackground 应用程序在后台,用户从通知中心点击消息将程序从后台    调至前台
      UIApplicationStateInactive 用用程序处于关闭状态(不在前台也不在后台),用户通过点击通知中心的消息将客户端从关闭状态调至前台
      */
      //应用程序在前台给一个提示特别消息
      if (application.applicationState == UIApplicationStateActive) {
            //应用程序在前台
            [self createAlertViewControllerWithPushDict:userInfo];
      }else{
            //其他两种情况,一种在后台程序没有被杀死,另一种是在程序已经杀死。用户点击推送的消息进入app的情况处理。
              [self handlePushMessage:userInfo];
      }
      completionHandler(UIBackgroundFetchResultNewData);
}

后台的证书是.pem格式,可通过命令将.p12文件转为.pem文件:

openssl pkcs12 -in origin.p12 -out dis.pem -nodes

AppDelegate.m参考

#import "AppDelegate.h"
#import <UserNotifications/UserNotifications.h>

@interface AppDelegate ()<UNUserNotificationCenterDelegate>

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0) {
        
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        center.delegate = self;
        [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) {
            
            if (!error) {
                NSLog(@"succeeded!");
            }
        }];
        
    } else if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0){
        
        //ios8以后的
        /**
         UIUserNotificationTypeNone = 0,
         UIUserNotificationTypeBadge = 1 << 0,
         UIUserNotificationTypeSound = 1 << 1,
         UIUserNotificationTypeAlert = 1 << 2
         */
        UIUserNotificationType type = UIUserNotificationTypeBadge | UIUserNotificationTypeSound|UIUserNotificationTypeAlert;
        UIUserNotificationSettings *setting = [UIUserNotificationSettings settingsForTypes:type categories:nil];
        //注册通知类型
        [application registerUserNotificationSettings:setting];
        //申请使用通知
        [application registerForRemoteNotifications];
        
    }else{
        /**
         *枚举类型
         UIRemoteNotificationTypeNone    = 0,   //不接收推送消息
         UIRemoteNotificationTypeBadge   = 1 << 0, 接收图标数字
         UIRemoteNotificationTypeSound   = 1 << 1, 接收时的音效
         UIRemoteNotificationTypeAlert   = 1 << 2, 接收消息文字弹窗
         UIRemoteNotificationTypeNewsstandContentAvailability = 1 << 3, 接收订阅消息
         */
        UIRemoteNotificationType  type = UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert;
        /**
         当用户第一次启动程序的时候就获取deviceToken
         该方法在iOS8就过期le
         */
        //调用该方法,系统就会自动发送UDID和当前程序的BundleID到苹果的APNS服务器
        [application registerForRemoteNotificationTypes:type];
    }
    
    
    //取出推送通知:
    //当程序被杀死或者退出了,接受到的推送信息会保留在launchOptions中,当我们重新打开软件的时候再取出来
    //取出在程序退出的时候接受到的推送消息
    NSDictionary *userInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
    if (userInfo) {
        NSLog(@"%@", userInfo);
    }
    
    return YES;
}


/**
 获取到用户当前程序的deviceToken后会会调这个方法
 */
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
    
    NSLog(@"%@",deviceToken);
    
    //deviceToken:就是我们需要上传服务器的deviceToken;

}

/*
 *iOS7以前
 *ios7以前苹果支持多任务, iOS7以前的多任务是假的多任务
 *而iOS7开始苹果才真正的推出了多任务
 *接收到远程服务器推送过来的内容就会调用
 *注意: 只有应用程序是打开状态(前台/后台(程序没有杀死)), 才会调用该方法
 *如果应用程序是关闭状态会调用didFinishLaunchingWithOptions
 */
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    /*
     如果应用程序在后台 , 只有用户点击了通知之后才会调用
     如果应用程序在前台, 会直接调用该方法
     即便应用程序关闭也可以接收到远程通知,但是改方法不会调用
     */
    NSLog(@"%@", userInfo);
    
}
/**
 *ios7以后
 *ios7以后用这个处理后台任务接收到得远程通知
 *接收到远程服务器推送过来的内容就会调用
 */
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    
    NSLog(@"%@", userInfo);
    NSNumber *contentid =  userInfo[@"content-id"];
    if (contentid) {
        //注意: 在此方法中一定要调用这个调用block, 告诉系统是否处理成功.
        // 以便于系统在后台更新UI等操作
        /*
         UIBackgroundFetchResultNewData, 成功接收到数据
         UIBackgroundFetchResultNoData, 没有;接收到数据
         UIBackgroundFetchResultFailed 接收失败
         */
        completionHandler(UIBackgroundFetchResultNewData);
    }else
    {
        completionHandler(UIBackgroundFetchResultFailed);
    }
    
}


/**
 *iOS10.0以后的方法
 */
//App在后台运行及程序退出杀死 会调用的方法

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(nonnull UNNotificationResponse *)response withCompletionHandler:(nonnull void (^)(void))completionHandler{
    
    
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    NSLog(@"App在后台时候-%@", userInfo);
    
    completionHandler();
    
}
// App在前台时候回调:用户正在使用状态
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
    
    NSDictionary *userInfo = notification.request.content.userInfo;
    NSLog(@"App在前台时候回调-%@", userInfo);
    
    //可以设置当收到通知后, 有哪些效果呈现(声音/提醒/数字角标)
    completionHandler(UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert);
    
}


@end

参考文档: docs.leancloud.cn/en/sdk/push…

www.jianshu.com/p/0ab8c91e3…

blog.csdn.net/weixin_3081…

blog.csdn.net/u011303663/…

www.jianshu.com/p/caf5086cd…

www.jianshu.com/p/074eb2df5…