如何在C#中实现基于apple令牌的推送通知(使用p8文件)?
对于具有某种基于聊天功能的应用程序,我想添加推送通知支持以接收新消息。 我想做的是使用Apple提供的新的基于令牌的身份验证(.p8文件),但是我找不到关于服务器部分的更多信息 我看到了以下帖子: 然而,答案并不令人满意,因为关于如何:如何在C#中实现基于apple令牌的推送通知(使用p8文件)?,c#,apple-push-notifications,C#,Apple Push Notifications,对于具有某种基于聊天功能的应用程序,我想添加推送通知支持以接收新消息。 我想做的是使用Apple提供的新的基于令牌的身份验证(.p8文件),但是我找不到关于服务器部分的更多信息 我看到了以下帖子: 然而,答案并不令人满意,因为关于如何: 与APN建立连接 使用p8文件(某种编码除外) 向Apple推送通知服务发送数据 您可以使用PushSharp,这是一个nuget软件包,支持苹果、谷歌和微软的推送通知 以下是和的链接 这是为Apple发送推送通知的示例: // Configuration
- 与APN建立连接
- 使用p8文件(某种编码除外)
- 向Apple推送通知服务发送数据
PushSharp
,这是一个nuget软件包,支持苹果、谷歌和微软的推送通知
以下是和的链接
这是为Apple发送推送通知的示例:
// 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 ();
目前,您无法在原始的.NET框架上真正做到这一点。新的基于JWT的APNS服务器只使用HTTP/2,而.NET Framework还不支持HTTP/2 但是,只要满足以下先决条件,.NET Core版本的
System.NET.Http
,就可以:
- 在Windows上,您必须运行Windows 10周年纪念版(v1607)或更高版本,或者Windows Server 2016的等效版本(我认为)
- 在Linux上,必须具有支持HTTP/2的
版本libcurl
- 在macOS上,您必须编译支持HTTP/2的
,然后使用libcurl
环境变量来加载自定义的DYLD\u INSERT\u库
libcurl
System.NET.Http
我不知道Mono、Xamarin或UWP会发生什么
然后,您必须做三件事:
System.Security.Cryptography.ECDSA
对象中- 在Windows上,您可以使用CNG API。解析密钥文件的base64编码DER部分后,您可以使用
创建一个密钥新建ECDsaCng(CngKey.Import(data,CngKeyBlobFormat.Pkcs8PrivateBlob))
- 在macOS或Linux上没有受支持的API,您必须自己解析DER结构,或者使用第三方库
System.IdentityModel.Tokens.Jwt
包,这相当简单。您需要Apple提供的密钥ID和团队IDyourRequestMessage.Version
设置为新版本(2,0)
,以便使用HTTP/2发出请求yourRequestMessage.Headers.Authorization
设置为new AuthenticationHeaderValue(“承载者”,令牌)
,以便为您的请求提供承载者身份验证令牌/JWT然后将您的JSON放入HTTP请求并将其发送到正确的URL。它在ASP.NET CORE 2.1和2.2上尝试了上述方法,但均无效。我总是得到的回答是“收到的消息是意外的或格式不正确的”,启用了HttpVersion20,这让我怀疑http2实现是否具体
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;
}
下面是ASP.NET CORE 3.0的工作原理
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('\n').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推荐的解决方案,,因为令牌(.p8)APN仅在HTTP/2中工作,因此大多数解决方案仅在.net Core中工作。因为我的项目使用的是.net Framework,所以需要进行一些调整。如果您像我一样使用.net Framework,请继续阅读
我到处搜索,遇到了几个问题,我设法解决了,并把它们拼凑在一起
下面是实际工作的APNs类。我为它创建了一个新的类库,并将.P8文件放在类库的AuthKeys文件夹中。记住右键单击.P8文件并将其设置为“始终复制”。参考
之后,要获取P8文件的位置,请对web项目使用AppDomain.CurrentDomain.RelativeSearchPath
,或对win应用程序使用AppDomain.CurrentDomain.BaseDirectory
。提及
要从P8获取令牌,您需要使用BouncyCastle类,请从Nuget下载
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('\n').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);
}
}
}
}
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);
}
}
使用Jose;
使用Newtonsoft.Json;
使用Org.BouncyCastle.Crypto.Parameters;
使用Org.BouncyCastle.OpenSsl;
使用安全加密技术;
使用制度;
使用System.Collections.Generic;
使用System.IO;
使用System.Linq;
使用System.Net.Http;
使用System.Net.Http.Header;
使用System.Security.Cryptography;
使用系统文本;
使用System.Threading.Tasks;
命名空间库
{
公共类ApplePushNotificationPush
{
//私有常量字符串WEB_地址=”https://api.sandbox.push.apple.com:443/3/device/{0}";
私有常量字符串WEB_地址=”https://api.push.apple.com:443/3/device/{0}";
私有字符串P8_PATH=AppDomain.CurrentDomain.RelativeSearchPath+@“\AuthKeys\APNs_AuthKey.P8”;
公共应用程序PushNotificationPush()
{
}
公共异步任务SendNotification(字符串deviceToken、字符串标题、字符串内容、int-badge=0、列表参数=null)
{
布尔成功=真;
尝试
{
字符串数据=System.IO.File.ReadAllText(P8_路径);
列表=data.Split('\n').ToList();
参数=参数??新列表();
字符串prk=list.Where((s,i)=>i!=0&&i!=list.Count-1)。聚合((agg,s)=>agg+s);
ECDsaCng key=newecdsacng(CngKey.Import(Convert.FromBase64String(prk),CngKeyBlobFormat.Pkcs8PrivateBlob));
字符串标记=GetProviderToken();
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);
}
}