如何在C#中实现基于Apple令牌的推送通知(使用p8文件)?

How to implement apple token based push notifications (using p8 file) in C#?

对于具有某些基于聊天功能的应用程序,我想添加推送通知支持以接收新消息。
我想做的是使用来自Apple的新的基于令牌的身份验证(.p8文件),但是我找不到有关服务器部分的更多信息。

我遇到了以下帖子:

如何在C#中使用APNs身份验证密钥(.p8文件)?

但是答案并不令人满意,因为有关如何操作的细节不多:

  • 与APN建立连接
  • 使用p8文件(某种编码除外)
  • 将数据发送到Apple Push Notification Service

您目前无法在原始.NET Framework上真正做到这一点。新的基于JWT的APNS服务器仅使用HTTP / 2,.NET Framework尚不支持。
但是,如果您满足以下先决条件,则

.NET Core的版本System.Net.Http可以:

  • 在Windows上,您必须运行Windows 10周年纪念版(v1607)或更高版本,或者运行Windows Server 2016的等效版本(我认为)。
  • 在Linux上,您必须具有支持HTTP / 2的libcurl版本。
  • 在macOS上,您必须编译支持HTTP / 2的libcurl,然后使用DYLD_INSERT_LIBRARIES环境变量来加载自定义版本的libcurl

如果确实需要,您应该能够在.NET Framework中使用.NET Core的System.Net.Http版本。

我不知道在Mono,Xamarin或UWP上会发生什么。

那么您需要做三件事:

  • 解析已获得的私钥。当前这是一个ECDSA密钥,您可以将其加载到System.Security.Cryptography.ECDsa对象中。
    • 在Windows上,您可以使用CNG API。解析密钥文件的base64编码的DER部分后,可以使用new ECDsaCng(CngKey.Import(data, CngKeyBlobFormat.Pkcs8PrivateBlob))创建密钥。
    • 在macOS或Linux上,没有受支持的API,您必须自己解析DER结构或使用第三方库。
  • 创建一个JSON Web令牌/承载令牌。如果您使用NuGet中的System.IdentityModel.Tokens.Jwt包,则这非常简单。您将需要Apple的密钥ID和团队ID。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public static string CreateToken(ECDsa key, string keyID, string teamID)
    {
        var securityKey = new ECDsaSecurityKey(key) { KeyId = keyID };
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);

        var descriptor = new SecurityTokenDescriptor
        {
              IssuedAt = DateTime.Now,
              Issuer = teamID,
              SigningCredentials = credentials
        };

        var handler = new JwtSecurityTokenHandler();
        var encodedToken = handler.CreateEncodedJwt(descriptor);
        return encodedToken;
    }
  • 发送一个HTTP / 2请求。这是正常现象,但是您需要做两件事:
  • yourRequestMessage.Version设置为new Version(2, 0)以便使用HTTP / 2发出请求。
  • yourRequestMessage.Headers.Authorization设置为new AuthenticationHeaderValue("bearer", token)以便为您的请求提供承载认证令牌/ JWT。
  • 然后只需将JSON放入HTTP请求中,然后将其发布到正确的URL。


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    private string GetToken()
        {
            var dsa = GetECDsa();
            return CreateJwt(dsa,"keyId","teamId");
        }
       
        private ECDsa GetECDsa()
        {
            using (TextReader reader = System.IO.File.OpenText("AuthKey_xxxxxxx.p8"))
            {
            var ecPrivateKeyParameters =
                (ECPrivateKeyParameters)new Org.BouncyCastle.OpenSsl.PemReader(reader).ReadObject();

            var q = ecPrivateKeyParameters.Parameters.G.Multiply(ecPrivateKeyParameters.D).Normalize();
            var qx = q.AffineXCoord.GetEncoded();
            var qy = q.AffineYCoord.GetEncoded();
            var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();

            // Convert the BouncyCastle key to a Native Key.
            var msEcp = new ECParameters {Curve = ECCurve.NamedCurves.nistP256, Q = {X = qx, Y = qy}, D = d};
            return ECDsa.Create(msEcp);
            }
        }
       
        private string CreateJwt(ECDsa key, string keyId, string teamId)
        {
            var securityKey = new ECDsaSecurityKey(key) { KeyId = keyId };
            var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);

            var descriptor = new SecurityTokenDescriptor
            {
                IssuedAt = DateTime.Now,
                Issuer = teamId,
                SigningCredentials = credentials,
               
            };

            var handler = new JwtSecurityTokenHandler();
            var encodedToken = handler.CreateEncodedJwt(descriptor);
            return encodedToken;
        }


    因为令牌(.p8)APN仅在HTTP / 2中有效,因此大多数解决方案仅在.net Core中有效。由于我的项目使用的是.net Framework,因此需要进行一些调整。如果您像我一样使用.net Framework,请继续阅读。

    我到处搜索,遇到了几个问题,我设法解决了这些问题并将它们拼凑在一起。

    下面是实际起作用的APNs类。我为此创建了一个新的类库,并将.P8文件放在该类库的AuthKeys文件夹中。请记住,右键单击.P8文件并将其设置为"始终复制"。请参阅在Web项目正在引用的类库项目中获取相对文件路径。

    此后,要获取P8文件的位置,请对Web项目使用AppDomain.CurrentDomain.RelativeSearchPath或对于win应用程序使用AppDomain.CurrentDomain.BaseDirectory。请参阅为什么AppDomain.CurrentDomain.BaseDirectory在asp.net应用程序中不包含" bin"?

    要从P8获得令牌,您需要使用BouncyCastle类,请从Nuget下载它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    using Jose;
    using Newtonsoft.Json;
    using Org.BouncyCastle.Crypto.Parameters;
    using Org.BouncyCastle.OpenSsl;
    using Security.Cryptography;
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Security.Cryptography;
    using System.Text;
    using System.Threading.Tasks;

        namespace PushLibrary
        {
            public class ApplePushNotificationPush
            {
                //private const string WEB_ADDRESS ="https://api.sandbox.push.apple.com:443/3/device/{0}";
                private const string WEB_ADDRESS ="https://api.push.apple.com:443/3/device/{0}";
       
                private string P8_PATH = AppDomain.CurrentDomain.RelativeSearchPath + @"\\AuthKeys\\APNs_AuthKey.p8";
       
                public ApplePushNotificationPush()
                {
       
                }
       
                public async Task<bool> SendNotification(string deviceToken, string title, string content, int badge = 0, List<Tuple<string, string>> parameters = null)
                {
                    bool success = true;
       
                    try
                    {
                        string data = System.IO.File.ReadAllText(P8_PATH);
                        List<string> list = data.Split('\
    '
    ).ToList();
       
                        parameters = parameters ?? new List<Tuple<string, string>>();
       
                        string prk = list.Where((s, i) => i != 0 && i != list.Count - 1).Aggregate((agg, s) => agg + s);
                        ECDsaCng key = new ECDsaCng(CngKey.Import(Convert.FromBase64String(prk), CngKeyBlobFormat.Pkcs8PrivateBlob));
       
                        string token = GetProviderToken();
       
                        string url = string.Format(WEB_ADDRESS, deviceToken);
                        HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url);
       
                        httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
       
                        httpRequestMessage.Headers.TryAddWithoutValidation("apns-push-type","alert"); // or background
                        httpRequestMessage.Headers.TryAddWithoutValidation("apns-id", Guid.NewGuid().ToString("D"));
                        //Expiry
                        //
                        httpRequestMessage.Headers.TryAddWithoutValidation("apns-expiration", Convert.ToString(0));
                        //Send imediately
                        httpRequestMessage.Headers.TryAddWithoutValidation("apns-priority", Convert.ToString(10));
                        //App Bundle
                        httpRequestMessage.Headers.TryAddWithoutValidation("apns-topic","com.xxx.yyy");
                        //Category
                        httpRequestMessage.Headers.TryAddWithoutValidation("apns-collapse-id","test");
       
                        //
                        var body = JsonConvert.SerializeObject(new
                        {
                            aps = new
                            {
                                alert = new
                                {
                                    title = title,
                                    body = content,
                                    time = DateTime.Now.ToString()
                                },
                                badge = 1,
                                sound ="default"
                            },
                            acme2 = new string[] {"bang","whiz" }
                        });
       
                        httpRequestMessage.Version = new Version(2, 0);
       
                        using (var stringContent = new StringContent(body, Encoding.UTF8,"application/json"))
                        {
                            //Set Body
                            httpRequestMessage.Content = stringContent;
       
                            Http2Handler.Http2CustomHandler handler = new Http2Handler.Http2CustomHandler();
       
                            handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls;
       
                            //handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
       
                            //Continue
                            using (HttpClient client = new HttpClient(handler))
                            {
                                HttpResponseMessage resp = await client.SendAsync(httpRequestMessage).ContinueWith(responseTask =>
                                {
                                    return responseTask.Result;
                                });
       
                                if (resp != null)
                                {
                                    string apnsResponseString = await resp.Content.ReadAsStringAsync();
       
                                    handler.Dispose();
                                }
       
                                handler.Dispose();
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        success = false;
                    }
       
                    return success;
                }
       
                private string GetProviderToken()
                {
                    double epochNow = (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
                    Dictionary<string, object> payload = new Dictionary<string, object>()
                    {
                        {"iss","YOUR APPLE TEAM ID" },
                        {"iat", epochNow }
                    };
                    var extraHeaders = new Dictionary<string, object>()
                    {
                        {"kid","YOUR AUTH KEY ID" },
                        {"alg","ES256" }
                    };
       
                    CngKey privateKey = GetPrivateKey();
       
                    return JWT.Encode(payload, privateKey, JwsAlgorithm.ES256, extraHeaders);
                }
       
                private CngKey GetPrivateKey()
                {
                    using (var reader = File.OpenText(P8_PATH))
                    {
                        ECPrivateKeyParameters ecPrivateKeyParameters = (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
       
                        var x = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded();
                        var y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded();
       
                        var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();
       
                        return EccKey.New(x, y, d);
                    }
                }
            }
        }

    第二,如果您注意到了,我正在使用自定义WinHTTPHandler来使代码基于如何使.net HttpClient使用http 2.0来支持HTTP / 2。我正在使用另一个类库创建此库,请记住从Nuget下载WinHTTPHandler。

    1
    2
    3
    4
    5
    6
    7
    8
    9
        public class Http2CustomHandler : WinHttpHandler
        {
            protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
            {
                request.Version = new Version("2.0");

                return base.SendAsync(request, cancellationToken);
            }
        }

    在那之后,只需在ApplePushNotificationPush类上调用" SendNotification",您就应该在iPhone上收到消息。


    它已在ASP.NET CORE 2.1和2.2上尝试了上述方法,但无济于事。我始终得到的响应是启用了HttpVersion20的"收到的消息意外或格式错误",这使我怀疑http2实现是否具体。

    下面是在ASP.NET CORE 3.0上工作的内容;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
     var teamId ="YOURTEAMID";
     var keyId ="YOURKEYID";

                try
                {
                    //
                    var data = await System.IO.File.ReadAllTextAsync(Path.Combine(_environment.ContentRootPath,"apns/"+config.P8FileName));
                    var list = data.Split('\
    '
    ).ToList();
                    var prk = list.Where((s, i) => i != 0 && i != list.Count - 1).Aggregate((agg, s) => agg + s);
                    //
                    var key = new ECDsaCng(CngKey.Import(Convert.FromBase64String(prk), CngKeyBlobFormat.Pkcs8PrivateBlob));
                    //
                    var token = CreateToken(key, keyId, teamId);
                    //
                    var deviceToken ="XXXXXXXXXXXXXXXXXXXXXXXXXXXX";
                    var url = string.Format("https://api.sandbox.push.apple.com/3/device/{0}", deviceToken);
                    var request = new HttpRequestMessage(HttpMethod.Post, url);
                    //
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
                    //

                    request.Headers.TryAddWithoutValidation("apns-push-type","alert"); // or background
                    request.Headers.TryAddWithoutValidation("apns-id", Guid.NewGuid().ToString("D"));
                    //Expiry
                    //
                    request.Headers.TryAddWithoutValidation("apns-expiration", Convert.ToString(0));
                    //Send imediately
                    request.Headers.TryAddWithoutValidation("apns-priority", Convert.ToString(10));
                    //App Bundle
                    request.Headers.TryAddWithoutValidation("apns-topic","com.xx.yy");
                    //Category
                    request.Headers.TryAddWithoutValidation("apns-collapse-id","test");

                    //
                    var body = JsonConvert.SerializeObject(new
                    {
                        aps = new
                        {
                            alert = new
                            {
                                title ="Test",
                                body ="Sample Test APNS",
                                time = DateTime.Now.ToString()
                            },
                            badge = 1,
                            sound ="default"
                        },
                        acme2 = new string[] {"bang","whiz" }
                    })
                    //
                    request.Version = HttpVersion.Version20;
                    //
                    using (var stringContent = new StringContent(body, Encoding.UTF8,"application/json"))
                    {
                        //Set Body
                        request.Content = stringContent;
                        _logger.LogInformation(request.ToString());
                        //
                        var handler = new HttpClientHandler();
                        //
                        handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
                        //
                        handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;

                        //Continue
                        using (HttpClient client = new HttpClient(handler))
                        {
                            //
                            HttpResponseMessage resp = await client.SendAsync(request).ContinueWith(responseTask =>
                            {
                                return responseTask.Result;
                                //

                            });
                            //
                            _logger.LogInformation(resp.ToString());
                            //
                            if (resp != null)
                            {
                                string apnsResponseString = await resp.Content.ReadAsStringAsync();
                                //
                                handler.Dispose();
                                //ALL GOOD ....
                                return;
                            }
                            //
                            handler.Dispose();
                        }
                    }
                }
                catch (HttpRequestException e)
                {
                    _logger.LogError(5, e.StackTrace, e);
                }

    对于CreateToken(),请参见yaakov,

    的上述推荐解决方案


    我有一个像你这样的问题。我看到了@gorniv的答案。这样就可以和我一起工作!

    也许您可以使用:https://www.nuget.org/packages/Apple.Auth.Signin!

    祝你好运!


    您可以使用PushSharp,这是一个nuget程序包,它支持Apple以及Google和Microsoft的推送通知。

    这是指向github和nuget的链接。

    这是向Apple发送推送通知的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    // Configuration (NOTE: .pfx can also be used here)
    var config = new ApnsConfiguration (ApnsConfiguration.ApnsServerEnvironment.Sandbox,
    "push-cert.p12","push-cert-pwd");

    // Create a new broker
    var apnsBroker = new ApnsServiceBroker (config);

    // Wire up events
    apnsBroker.OnNotificationFailed += (notification, aggregateEx) => {

    aggregateEx.Handle (ex => {

        // See what kind of exception it was to further diagnose
        if (ex is ApnsNotificationException) {
            var notificationException = (ApnsNotificationException)ex;

            // Deal with the failed notification
            var apnsNotification = notificationException.Notification;
            var statusCode = notificationException.ErrorStatusCode;

            Console.WriteLine ($"Apple Notification Failed: ID={apnsNotification.Identifier}, Code={statusCode}");

        } else {
            // Inner exception might hold more useful information like an ApnsConnectionException          
            Console.WriteLine ($"Apple Notification Failed for some unknown reason : {ex.InnerException}");
        }

            // Mark it as handled
            return true;
        });
    };

        apnsBroker.OnNotificationSucceeded += (notification) => {
        Console.WriteLine ("Apple Notification Sent!");
    };

    // Start the broker
    apnsBroker.Start ();

    foreach (var deviceToken in MY_DEVICE_TOKENS) {
    // Queue a notification to send
    apnsBroker.QueueNotification (new ApnsNotification {
        DeviceToken = deviceToken,
        Payload = JObject.Parse ("{"aps":{"badge":7}}")
        });
    }

    // Stop the broker, wait for it to finish  
    // This isn't done after every message, but after you're
    // done with the broker
    apnsBroker.Stop ();