• AST.NET Core 服务器端对接苹果推送通知 APNs


    本文主要介绍三个问题:

    1. 如何建立基于 Token 的连接到 APNs?
    2. 生成 token
    3. 如何发送通知到 APNs?

    一、建立基于 Token 的连接到 APNs

    要和 APNs 通信,连接就要认证。目前苹果提供的认证方式有两种,一种是基于 Token,一种是基于证书。本文只介绍基于 Token 的认证。基于 Token 的认证主要有以下优点:

    • 无状态的通信要比基于证书的认证要快,因为不需要查询证书
    • 你可以使用来自不同提供服务器的同一种类型的 token
    • 你可以使用一种 token 为你公司的所有 APP 发布通知

    我们要生成 token,首先要从苹果获取一个加密密钥和一个密钥 ID。

    登录开发者账号,找到 keys,点击新增:

    输入名称,勾选通知服务

     

    点击注册 

    点击下载,下载的文件存储的是以 p8 格式文件的密钥,我们需要的有两个信息,一个是 Key ID(下载的文件名及下图都有),一个是文件中的密钥。

     下一步就是用这个密钥生成 token。

    二、生成 token

    这里生成的 token 为 JWT Token,目前 APNs 只支持 ES256 算法加密,也就是我们需要使用 ES256 来加密生成 JWT Token。生成的 token 中需要包含的信息如下,必须要按照以下格式指定。

    其中分为两部分,header 和 payload 部分,其中 header 指定了加密算法和上一步我们获取到的 Key Id,格式如下:

    {

        "alg": "ES256", // 固定的

        "kid": "你自己的 Key Id"

    }

    payload 部分要包含以下信息:

    {

        "iss": "DEF123GHIJ", // 签发者,是你的开发者账号的 Id,Team Id

        "iat": 1437179036 // 签发时间,1970 至今的秒数,这里苹果要求 token 的刷新时间不能超过一小时,最小不能低于 20 分钟

    }

    我们要做到就是对以上信息加密生成 JWT Token。

    这里主要注意的是

    1. 读取密钥
    2. 使用密钥生成 JWT Token

    这里密钥我这里是把密钥从 p8 文件里面复制出来,放到了 appsettings.json 文件里,通过配置获取。注意,文件中的-----BEGIN PRIVATE KEY-----和 -----END PRIVATE KEY----- 中间部分就是密钥,需要删除中间的换行符。p8 文件的固定格式。代码如下:

    var securityKey = _configuration["apple:securityKey"].Replace("\n", "");

    当然,这里读取密钥也可以用其他方法直接从 p8 文件读取,这里没做研究,也就不做介绍了。

    获取到密钥后,我们需要生成 JWT Token,具体代码如下,完整代码在最后:

            var eCDsa = ECDsa.Create();
    
            eCDsa.ImportPkcs8PrivateKey(Convert.FromBase64String(securityKey), out _);
    
            var key = new ECDsaSecurityKey(eCDsa);
    
            key.KeyId = kid;
    
            var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256);
            var jwtHeader = new JwtHeader(signingCredentials);
            var jwtPayload = new JwtPayload(claims);
    
            var jwtSecurityToken = new JwtSecurityToken(jwtHeader, jwtPayload);
    
            APNsService.token = tokenHandler.WriteToken(jwtSecurityToken);

    这样我们就生成了 JWT Token。发送请求的时候放入头部就可以,格式为 authorization = bearer token(注意,bearer 和 token 之间使用空格分割)

    三、发送通知

    发送通知,也就是往 APNs 发送一个 POST 请求。具体就是使用 C# 发送一个 Request 请求,具体不做过多介绍,详见后面完整代码。

    需要注意的是

    1. APNs Uri 这里参见下面代码,官方的文档错误,其中 :path 不能添加
    2. 请求头部除了 Authorization 之外,apns-topic 也必须添加,也就是 app bundle id,其他的可以参照官方文档

    完整代码:

    IAPNsService
    1. namespace Hzg.Services;
    2. public interface IAPNsService
    3. {
    4. ///
    5. /// 生成 APNs JWT token
    6. ///
    7. ///
    8. string GetnerateAPNsJWTToken();
    9. ///
    10. /// 发送推送通知
    11. ///
    12. /// APP Id
    13. /// 设备标识
    14. /// 通知类型
    15. /// 标题
    16. /// 子标题
    17. /// 通知内容
    18. ///
    19. Task<string> PushNotification(string apnsTopic, string deviceToken, NotificationType type, string title, string subtitle, string body);
    20. }
    APNsService
    
    1. using System.Security.Claims;
    2. using System.Security.Cryptography;
    3. using Microsoft.IdentityModel.Tokens;
    4. using System.IdentityModel.Tokens.Jwt;
    5. using static System.Net.Mime.MediaTypeNames;
    6. using Microsoft.Net.Http.Headers;
    7. using Microsoft.Extensions.Configuration;
    8. using Hzg.Tool;
    9. using Hzg.Const;
    10. namespace Hzg.Services;
    11. public enum NotificationType: int
    12. {
    13. Alert = 0,
    14. Sound = 1,
    15. Badge = 2,
    16. Silent = 3
    17. }
    18. ///
    19. /// APNs 生成 JWT token,添加服务的时候,使用单利
    20. ///
    21. public class APNsService : IAPNsService
    22. {
    23. static string token = null;
    24. static string baseUrl = null;
    25. private readonly IConfiguration _configuration;
    26. private readonly IHttpClientFactory _httpClientFactory;
    27. public APNsService(IConfiguration configuration, IHttpClientFactory httpClientFactory)
    28. {
    29. this._configuration = configuration;
    30. this._httpClientFactory = httpClientFactory;
    31. APNsService.baseUrl = this._configuration["apple:pushNotificationServer"];
    32. }
    33. ///
    34. /// 生成 APNs JWT token
    35. ///
    36. ///
    37. public string GetnerateAPNsJWTToken()
    38. {
    39. return this.GetnerateAPNsJWTToken(APNsService.token);
    40. }
    41. ///
    42. /// 生成 APNs JWT token
    43. ///
    44. ///
    45. private string GetnerateAPNsJWTToken(string oldToken)
    46. {
    47. var tokenHandler = new JwtSecurityTokenHandler();
    48. var iat = ((DateTime.UtcNow.Ticks - new DateTime(1970, 1, 1).Ticks) / TimeSpan.TicksPerSecond);
    49. // 判断原 token 是否超过 50 分钟,如果未超过,直接返回
    50. if (string.IsNullOrWhiteSpace(oldToken) == false)
    51. {
    52. JwtPayload oldPayload = tokenHandler.ReadJwtToken(oldToken).Payload;
    53. var oldIat = oldPayload.Claims.FirstOrDefault(c => c.Type == "iat");
    54. if (oldIat != null)
    55. {
    56. if (long.TryParse(oldIat.Value, out long oldIatValue) == true)
    57. {
    58. // 两次间隔小于 50 分钟,使用原 token
    59. if ((iat - oldIatValue) < (50 * 60))
    60. {
    61. return oldToken;
    62. }
    63. }
    64. }
    65. }
    66. var kid = _configuration["apple:kid"];
    67. var securityKey = _configuration["apple:securityKey"].Replace("\n", "");
    68. var iss = _configuration["apple:iss"];
    69. var claims = new Claim[]
    70. {
    71. new Claim("iss", iss),
    72. new Claim("iat", iat.ToString())
    73. };
    74. var eCDsa = ECDsa.Create();
    75. eCDsa.ImportPkcs8PrivateKey(Convert.FromBase64String(securityKey), out _);
    76. var key = new ECDsaSecurityKey(eCDsa);
    77. key.KeyId = kid;
    78. var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256);
    79. var jwtHeader = new JwtHeader(signingCredentials);
    80. var jwtPayload = new JwtPayload(claims);
    81. var jwtSecurityToken = new JwtSecurityToken(jwtHeader, jwtPayload);
    82. APNsService.token = tokenHandler.WriteToken(jwtSecurityToken);
    83. return APNsService.token;
    84. }
    85. ///
    86. /// 发送推送通知
    87. ///
    88. /// APP Id
    89. /// 设备标识
    90. /// 通知类型
    91. /// 标题
    92. /// 子标题
    93. /// 通知内容
    94. ///
    95. public async Task<string> PushNotification(string apnsTopic, string deviceToken, NotificationType type, string title, string subtitle, string body)
    96. {
    97. var responseData = ResponseTool.FailedResponseData();
    98. var token = this.GetnerateAPNsJWTToken();
    99. var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, APNsService.baseUrl + deviceToken)
    100. {
    101. Headers =
    102. {
    103. { HeaderNames.Authorization, "bearer " + token },
    104. { "apns-topic", apnsTopic },
    105. { "apns-expiration", "0" }
    106. },
    107. Version = new Version(2, 0)
    108. };
    109. var notContent = new
    110. {
    111. aps = new
    112. {
    113. alert = new
    114. {
    115. title = title,
    116. subtitle = subtitle,
    117. body = body
    118. }
    119. }
    120. };
    121. var content = new StringContent(JsonSerializerTool.SerializeDefault(notContent), System.Text.Encoding.UTF8, Application.Json);
    122. httpRequestMessage.Content = content;
    123. var httpClient = _httpClientFactory.CreateClient();
    124. try
    125. {
    126. var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
    127. if (httpResponseMessage.IsSuccessStatusCode)
    128. {
    129. responseData.Code = ErrorCode.Success;
    130. return JsonSerializerTool.SerializeDefault(responseData);
    131. }
    132. else
    133. {
    134. responseData.Data = httpResponseMessage.StatusCode;
    135. return JsonSerializerTool.SerializeDefault(responseData);
    136. }
    137. }
    138. catch (Exception e)
    139. {
    140. responseData.Data = e.Message;
    141. return JsonSerializerTool.SerializeDefault(responseData);
    142. }
    143. }
    144. }

     
    

  • 相关阅读:
    机器学习第十一课--K-Means聚类
    JavaScript、Java、C#标记过时方法
    QT Creator 添加快捷生成函数、文件注释脚本
    C++ 基础与深度分析 Chapter8 动态内存管理(动态内存基础、智能指针、相关问题)
    pandas常用数据操作记录
    Java-面向对象之(抽象类+接口)
    实战 || 某软件股份有限公司通用漏洞挖掘
    vue 方法按照顺序执行
    PHP中的命名空间和自动加载机制
    vue 项目运行过程中浏览器报Out of Memory
  • 原文地址:https://blog.csdn.net/hzgisme/article/details/127584728