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的版本
- 在Windows上,您必须运行Windows 10周年纪念版(v1607)或更高版本,或者运行Windows Server 2016的等效版本(我认为)。
-
在Linux上,您必须具有支持HTTP / 2的
libcurl 版本。 -
在macOS上,您必须编译支持HTTP / 2的
libcurl ,然后使用DYLD_INSERT_LIBRARIES 环境变量来加载自定义版本的libcurl 。
如果确实需要,您应该能够在.NET Framework中使用.NET Core的
我不知道在Mono,Xamarin或UWP上会发生什么。
那么您需要做三件事:
-
在Windows上,您可以使用CNG API。解析密钥文件的base64编码的DER部分后,可以使用
new ECDsaCng(CngKey.Import(data, CngKeyBlobFormat.Pkcs8PrivateBlob)) 创建密钥。 - 在macOS或Linux上,没有受支持的API,您必须自己解析DER结构或使用第三方库。
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; } |
然后只需将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项目使用
要从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!
祝你好运!
您可以使用
这是指向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 (); |